Skip to content

Commit 74df2d0

Browse files
[VarExporter] Add LazyGhostTrait and LazyProxyTrait documentation
1 parent dd90e0a commit 74df2d0

File tree

1 file changed

+181
-0
lines changed

1 file changed

+181
-0
lines changed

components/var_exporter.rst

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,5 +128,186 @@ created by using the special ``"\0"`` property name to define their internal val
128128
"\0" => [$inputArray],
129129
]);
130130

131+
Creating Lazy Objects
132+
---------------------
133+
134+
Lazy-objects are objects instantiated empty and populated on-demand. This is
135+
particularly useful when you have for example properties in your classes that
136+
requires some heavy computation to determine their value. In this case, you
137+
may want to trigger the property's value processing only when you actually need
138+
its value. Thanks to this, the heavy computation won't be done if you never use
139+
this property. The VarExporter component is bundled with two traits helping
140+
you implement such mechanism easily in your classes.
141+
142+
.. _var-exporter_ghost-objects:
143+
144+
LazyGhostTrait
145+
~~~~~~~~~~~~~~
146+
147+
Ghost objects are empty objects, which see their properties populated the first
148+
time any method is called. Thanks to :class:`Symfony\\Component\\VarExporter\\LazyGhostTrait`,
149+
the implementation of the lazy mechanism is eased. In the following example, we are
150+
defining the ``$hash`` property as lazy. We also declare that the ``MyLazyObject::computeHash()``
151+
method should be called only when ``$hash``'s value need to be known::
152+
153+
namespace App\Hash;
154+
155+
use Symfony\Component\VarExporter\LazyGhostTrait;
156+
157+
class HashProcessor
158+
{
159+
use LazyGhostTrait;
160+
// Because of how the LazyGhostTrait trait works internally, you
161+
// must add this private property in your class
162+
private int $lazyObjectId;
163+
164+
// This property may require a heavy computation to have its value
165+
public readonly string $hash;
166+
167+
public function __construct()
168+
{
169+
self::createLazyGhost(initializer: [
170+
'hash' => $this->computeHash(...),
171+
], instance: $this);
172+
}
173+
174+
private function computeHash(array $data): string
175+
{
176+
// Compute $this->hash value with the passed data
177+
}
178+
}
179+
180+
:class:`Symfony\\Component\\VarExporter\\LazyGhostTrait` also allows to
181+
convert non-lazy classes to lazy ones::
182+
183+
namespace App\Hash;
184+
185+
use Symfony\Component\VarExporter\LazyGhostTrait;
186+
187+
class HashProcessor
188+
{
189+
public readonly string $hash;
190+
191+
public function __construct(array $data)
192+
{
193+
$this->hash = $this->computeHash($data);
194+
}
195+
196+
private function computeHash(array $data): string
197+
{
198+
// ...
199+
}
200+
201+
public function validateHash(): bool
202+
{
203+
// ...
204+
}
205+
}
206+
207+
class LazyHashProcessor extends HashProcessor
208+
{
209+
use LazyGhostTrait;
210+
}
211+
212+
$processor = LazyHashProcessor::createLazyGhost(initializer: function (HashProcessor $instance): void {
213+
// Do any operation you need here: call setters, getters, methods to validate the hash, etc.
214+
$data = /** Retrieve required data to compute the hash */;
215+
$instance->__construct(...$data);
216+
$instance->validateHash();
217+
});
218+
219+
While you never query ``$processor->hash`` value, heavy methods will never be triggered.
220+
But still, the ``$processor`` object exists and can be used in your code, passed to
221+
methods, functions, etc.
222+
223+
Additionally and by adding two arguments to initializer function, it is possible to initialize
224+
properties one-by-one::
225+
226+
$processor = LazyHashProcessor::createLazyGhost(initializer: function (HashProcessor $instance, string $propertyName, ?string $propertyScope): mixed {
227+
if (HashProcessor::class === $propertyScope && 'hash' === $propertyName) {
228+
// Return $hash value
229+
}
230+
231+
// Then you can add more logic for the other properties
232+
});
233+
234+
Ghost objects unfortunately can't work with abstract classes but also internal PHP classes.
235+
Nevertheless, the VarExporter component covers this need with the help of to
236+
:ref:`Virtual Proxies <var-exporter_virtual-proxies>`.
237+
238+
.. versionadded:: 6.2
239+
240+
The :class:`Symfony\\Component\\VarExporter\\LazyGhostTrait` was introduced in Symfony 6.2.
241+
242+
.. _var-exporter_virtual-proxies:
243+
244+
LazyProxyTrait
245+
~~~~~~~~~~~~~~
246+
247+
The purpose of virtual proxies in the same one as
248+
:ref:`ghost objects <var-exporter_ghost-objects>`, but their internal behavior is
249+
totally different. Where ghost objects requires to extend a base class, virtual
250+
proxies take advantage of the **Liskov Substitution principle**. This principle
251+
describes that if two objects are implementing the same interface, you can swap between
252+
the different implementations without breaking your application. This is what virtual
253+
proxies take advantage of. To use virtual proxies, you may use
254+
:class:`Symfony\\Component\\VarExporter\\ProxyHelper` to generate proxy's class
255+
code::
256+
257+
namespace App\Hash;
258+
259+
use Symfony\Component\VarExporter\ProxyHelper;
260+
261+
interface ProcessorInterface
262+
{
263+
public function getHash(): bool;
264+
}
265+
266+
abstract class AbstractProcessor implements ProcessorInterface
267+
{
268+
protected string $hash;
269+
270+
public function getHash(): bool
271+
{
272+
return $this->hash;
273+
}
274+
}
275+
276+
class HashProcessor extends AbstractProcessor
277+
{
278+
public function __construct(array $data)
279+
{
280+
$this->hash = $this->computeHash($data);
281+
}
282+
283+
private function computeHash(array $data): string
284+
{
285+
// ...
286+
}
287+
}
288+
289+
$proxyCode = ProxyHelper::generateLazyProxy(new \ReflectionClass(AbstractProcessor::class));
290+
// $proxyCode contains the actual proxy and the reference to LazyProxyTrait.
291+
// In production env, this should be dumped into a file to avoid calling eval().
292+
eval('class HashProcessorProxy'.$proxyCode);
293+
294+
$processor = HashProcessorProxy::createLazyProxy(initializer: function (): ProcessorInterface {
295+
$data = /** Retrieve required data to compute the hash */;
296+
$instance = new HashProcessor(...$data);
297+
298+
// Do any operation you need here: call setters, getters, methods to validate the hash, etc.
299+
300+
return $instance;
301+
});
302+
303+
Just like ghost objects, while you never query ``$processor->hash``, its value will not be computed.
304+
The main difference with ghost objects is that this time, we created a proxy of an abstract class.
305+
This also works with internal PHP class.
306+
307+
.. versionadded:: 6.2
308+
309+
The :class:`Symfony\\Component\\VarExporter\\LazyProxyTrait` and
310+
:class:`Symfony\\Component\\VarExporter\\ProxyHelper` were introduced in Symfony 6.2.
311+
131312
.. _`OPcache`: https://www.php.net/opcache
132313
.. _`PSR-2`: https://www.php-fig.org/psr/psr-2/

0 commit comments

Comments
 (0)