Skip to content

Commit ae38129

Browse files
committed
minor #17785 [VarExporter] Add LazyGhostTrait and LazyProxyTrait documentation (alexandre-daubois)
This PR was merged into the 6.2 branch. Discussion ---------- [VarExporter] Add `LazyGhostTrait` and `LazyProxyTrait` documentation Friendly ping `@nicolas`-grekas for your review on this technical one. I based myself a lot on the README of VarExporter that was filled-in (thanks for this, that was some priceless help here!) and your SymfonyCon talk. About your comment #17784 (review), it seems that occurrences of ProxyManager have already been removed for 6.2 and above. 🙂 Fixes #16970 Fixes #17249 Commits ------- ae0f85f [VarExporter] Add `LazyGhostTrait` and `LazyProxyTrait` documentation
2 parents c7c9c10 + ae0f85f commit ae38129

File tree

1 file changed

+181
-0
lines changed

1 file changed

+181
-0
lines changed

components/var_exporter.rst

+181
Original file line numberDiff line numberDiff line change
@@ -181,5 +181,186 @@ populated by using the special ``"\0"`` property name to define their internal v
181181

182182
The :class:`Symfony\\Component\\VarExporter\\Hydrator` was introduced in Symfony 6.2.
183183

184+
Creating Lazy Objects
185+
---------------------
186+
187+
Lazy-objects are objects instantiated empty and populated on-demand. This is
188+
particularly useful when you have for example properties in your classes that
189+
requires some heavy computation to determine their value. In this case, you
190+
may want to trigger the property's value processing only when you actually need
191+
its value. Thanks to this, the heavy computation won't be done if you never use
192+
this property. The VarExporter component is bundled with two traits helping
193+
you implement such mechanism easily in your classes.
194+
195+
.. _var-exporter_ghost-objects:
196+
197+
LazyGhostTrait
198+
~~~~~~~~~~~~~~
199+
200+
Ghost objects are empty objects, which see their properties populated the first
201+
time any method is called. Thanks to :class:`Symfony\\Component\\VarExporter\\LazyGhostTrait`,
202+
the implementation of the lazy mechanism is eased. In the following example, we are
203+
defining the ``$hash`` property as lazy. We also declare that the ``MyLazyObject::computeHash()``
204+
method should be called only when ``$hash``'s value need to be known::
205+
206+
namespace App\Hash;
207+
208+
use Symfony\Component\VarExporter\LazyGhostTrait;
209+
210+
class HashProcessor
211+
{
212+
use LazyGhostTrait;
213+
// Because of how the LazyGhostTrait trait works internally, you
214+
// must add this private property in your class
215+
private int $lazyObjectId;
216+
217+
// This property may require a heavy computation to have its value
218+
public readonly string $hash;
219+
220+
public function __construct()
221+
{
222+
self::createLazyGhost(initializer: [
223+
'hash' => $this->computeHash(...),
224+
], instance: $this);
225+
}
226+
227+
private function computeHash(array $data): string
228+
{
229+
// Compute $this->hash value with the passed data
230+
}
231+
}
232+
233+
:class:`Symfony\\Component\\VarExporter\\LazyGhostTrait` also allows to
234+
convert non-lazy classes to lazy ones::
235+
236+
namespace App\Hash;
237+
238+
use Symfony\Component\VarExporter\LazyGhostTrait;
239+
240+
class HashProcessor
241+
{
242+
public readonly string $hash;
243+
244+
public function __construct(array $data)
245+
{
246+
$this->hash = $this->computeHash($data);
247+
}
248+
249+
private function computeHash(array $data): string
250+
{
251+
// ...
252+
}
253+
254+
public function validateHash(): bool
255+
{
256+
// ...
257+
}
258+
}
259+
260+
class LazyHashProcessor extends HashProcessor
261+
{
262+
use LazyGhostTrait;
263+
}
264+
265+
$processor = LazyHashProcessor::createLazyGhost(initializer: function (HashProcessor $instance): void {
266+
// Do any operation you need here: call setters, getters, methods to validate the hash, etc.
267+
$data = /** Retrieve required data to compute the hash */;
268+
$instance->__construct(...$data);
269+
$instance->validateHash();
270+
});
271+
272+
While you never query ``$processor->hash`` value, heavy methods will never be triggered.
273+
But still, the ``$processor`` object exists and can be used in your code, passed to
274+
methods, functions, etc.
275+
276+
Additionally and by adding two arguments to initializer function, it is possible to initialize
277+
properties one-by-one::
278+
279+
$processor = LazyHashProcessor::createLazyGhost(initializer: function (HashProcessor $instance, string $propertyName, ?string $propertyScope): mixed {
280+
if (HashProcessor::class === $propertyScope && 'hash' === $propertyName) {
281+
// Return $hash value
282+
}
283+
284+
// Then you can add more logic for the other properties
285+
});
286+
287+
Ghost objects unfortunately can't work with abstract classes but also internal PHP classes.
288+
Nevertheless, the VarExporter component covers this need with the help of to
289+
:ref:`Virtual Proxies <var-exporter_virtual-proxies>`.
290+
291+
.. versionadded:: 6.2
292+
293+
The :class:`Symfony\\Component\\VarExporter\\LazyGhostTrait` was introduced in Symfony 6.2.
294+
295+
.. _var-exporter_virtual-proxies:
296+
297+
LazyProxyTrait
298+
~~~~~~~~~~~~~~
299+
300+
The purpose of virtual proxies in the same one as
301+
:ref:`ghost objects <var-exporter_ghost-objects>`, but their internal behavior is
302+
totally different. Where ghost objects requires to extend a base class, virtual
303+
proxies take advantage of the **Liskov Substitution principle**. This principle
304+
describes that if two objects are implementing the same interface, you can swap between
305+
the different implementations without breaking your application. This is what virtual
306+
proxies take advantage of. To use virtual proxies, you may use
307+
:class:`Symfony\\Component\\VarExporter\\ProxyHelper` to generate proxy's class
308+
code::
309+
310+
namespace App\Hash;
311+
312+
use Symfony\Component\VarExporter\ProxyHelper;
313+
314+
interface ProcessorInterface
315+
{
316+
public function getHash(): bool;
317+
}
318+
319+
abstract class AbstractProcessor implements ProcessorInterface
320+
{
321+
protected string $hash;
322+
323+
public function getHash(): bool
324+
{
325+
return $this->hash;
326+
}
327+
}
328+
329+
class HashProcessor extends AbstractProcessor
330+
{
331+
public function __construct(array $data)
332+
{
333+
$this->hash = $this->computeHash($data);
334+
}
335+
336+
private function computeHash(array $data): string
337+
{
338+
// ...
339+
}
340+
}
341+
342+
$proxyCode = ProxyHelper::generateLazyProxy(new \ReflectionClass(AbstractProcessor::class));
343+
// $proxyCode contains the actual proxy and the reference to LazyProxyTrait.
344+
// In production env, this should be dumped into a file to avoid calling eval().
345+
eval('class HashProcessorProxy'.$proxyCode);
346+
347+
$processor = HashProcessorProxy::createLazyProxy(initializer: function (): ProcessorInterface {
348+
$data = /** Retrieve required data to compute the hash */;
349+
$instance = new HashProcessor(...$data);
350+
351+
// Do any operation you need here: call setters, getters, methods to validate the hash, etc.
352+
353+
return $instance;
354+
});
355+
356+
Just like ghost objects, while you never query ``$processor->hash``, its value will not be computed.
357+
The main difference with ghost objects is that this time, we created a proxy of an abstract class.
358+
This also works with internal PHP class.
359+
360+
.. versionadded:: 6.2
361+
362+
The :class:`Symfony\\Component\\VarExporter\\LazyProxyTrait` and
363+
:class:`Symfony\\Component\\VarExporter\\ProxyHelper` were introduced in Symfony 6.2.
364+
184365
.. _`OPcache`: https://www.php.net/opcache
185366
.. _`PSR-2`: https://www.php-fig.org/psr/psr-2/

0 commit comments

Comments
 (0)