@@ -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, the
203+ ``$hash `` property is defined as lazy. Also, 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
273+ triggered. But still, the ``$processor `` object exists and can be used in your
274+ code, passed to methods, functions, etc.
275+
276+ Additionally and by adding two arguments to the initializer function, it is
277+ possible to initialize 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 or internal PHP
288+ classes. Nevertheless, the VarExporter component covers this need with the help
289+ of :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
305+ between the different implementations without breaking your application. This is
306+ what virtual 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
357+ will not be computed. The main difference with ghost objects is that this time,
358+ a proxy of an abstract class was created. 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