PHP 8.4 introduced lazy objects: instances whose full initialization runs only when something observes or modifies their state. That is useful when you build dependency-injection graphs (services you might never touch), ORM-style entities that should hydrate from storage only on access, or parsers that defer work until a field is used. The feature is documented in depth in the Lazy Objects chapter of the PHP manual; this post distills the two strategies and when each fits.
PHP supports two lazy patterns (manual):
| Strategy | What happens on init | Identity |
|---|---|---|
| Lazy ghost | The same object is filled in place (initializer mutates the lazy instance). | After init, it is indistinguishable from a normal instance of that class. |
| Lazy proxy | A factory returns a separate “real” instance; the proxy forwards property access to it. | The proxy and the real object are not the same object—=== and object id can surprise you. |
Rule of thumb: Prefer a ghost when you control both construction and initialization. Prefer a proxy when another layer must construct the real instance (the factory can return new RealThing(...)), at the cost of caring about identity.
newLazyGhost and newLazyProxyCreation goes through ReflectionClass::newLazyGhost() and ReflectionClass::newLazyProxy(). Both accept a callback that runs when initialization is required. You can also reset existing instances with resetAsLazyGhost / resetAsLazyProxy (lifecycle).
The initializer receives the lazy object and is expected to set it up on that same instance (for example by calling __construct or assigning properties):
class Example{ public int $prop;
public function __construct(int $x) { $this->prop = $x; }}
$reflector = new ReflectionClass(Example::class);
$lazyObject = $reflector->newLazyGhost(function (Example $object): void { $object->__construct(1);});
// Initialization runs when state is observed, e.g. reading $prop:var_dump($lazyObject->prop); // int(1)Until something triggers initialization, properties behave as uninitialized from the outside; var_dump on the object itself is one of the operations listed in the manual that does not automatically initialize it (non-triggering operations).
The factory receives the proxy but should return a non-lazy instance of a compatible class. After init, property reads/writes on the proxy are forwarded to that real instance:
$lazyProxy = $reflector->newLazyProxy(function (Example $proxy): Example { return new Example(42);});
var_dump($lazyProxy->prop); // int(42)The manual stresses that identity differs between proxy and real instance—code that compares object identity or relies on a single canonical instance must account for that (about lazy object strategies).
Sometimes a few fields are known up front and should be readable without forcing full initialization. The manual documents ReflectionProperty::skipLazyInitialization() and ReflectionProperty::setRawValueWithoutLazyInitialization() for that workflow (example in manual).
You can also force or finalize initialization explicitly with ReflectionClass::initializeLazyObject() and related APIs when you need deterministic timing.
__clone runs on the real instance, not the proxy (cloning).Method calls that do not touch object state may not trigger initialization; conversely, normal property access, many reflection operations, serialization (unless special flags apply), and other operations do trigger it—see Initialization triggers for the full list.
No comments yet
Loading comments...