1. Home
  2. /Blog
  3. /PHP 8.4: Lazy Objects—Ghost vs Proxy (When to Use Which)
2026-05-043 min readLoading views...Backend

PHP 8.4: Lazy Objects—Ghost vs Proxy (When to Use Which)

PHPPHP 8.4BackendLazy ObjectsReflectionOOP

PHP 8.4: Lazy Objects—Ghost vs Proxy (When to Use Which)

2026-05-043 min readBackend
Table of contents
Two strategies: ghost vs proxyEntry points: `newLazyGhost` and `newLazyProxy`Lazy ghost (in-place initializer)Lazy proxy (factory returns the real instance)Skipping initialization for known propertiesPractical gotchas (from the manual)SummaryFurther reading (official)

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.


Two strategies: ghost vs proxy

PHP supports two lazy patterns (manual):

StrategyWhat happens on initIdentity
Lazy ghostThe same object is filled in place (initializer mutates the lazy instance).After init, it is indistinguishable from a normal instance of that class.
Lazy proxyA 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.


Entry points: newLazyGhost and newLazyProxy

Creation 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).

Lazy ghost (in-place initializer)

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):

php
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).

Lazy proxy (factory returns the real instance)

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:

php
$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).


Skipping initialization for known properties

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.


Practical gotchas (from the manual)

  1. Cloning a lazy object triggers initialization before the clone is produced. For proxies, both proxy and real instance are cloned; __clone runs on the real instance, not the proxy (cloning).
  2. Destructors: For ghosts, the destructor runs only if the object was initialized. For proxies, the destructor runs on the real instance when it exists (destructors).
  3. Failed init: If the initializer throws, the object is reverted to its pre-init lazy state so you never leak a half-initialized instance (common behavior).

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.


Summary

  • Lazy objects in PHP 8.4 defer work until state is observed or changed—see the 8.4 release notes and migration: new features.
  • Use a ghost for transparent, in-place lazy setup; use a proxy when a factory must build the real object, and treat identity carefully.
  • Treat the Lazy Objects manual as the source of truth for triggers, cloning, and edge cases.

Further reading (official)

  • PHP 8.4 Release Announcement
  • Migration: New features in PHP 8.4
  • Lazy Objects
  • ReflectionClass::newLazyGhost
  • ReflectionClass::newLazyProxy
  • ReflectionProperty::skipLazyInitialization
  • ReflectionClass::initializeLazyObject

Comments

No comments yet

Loading comments...

Table of contents
Two strategies: ghost vs proxyEntry points: `newLazyGhost` and `newLazyProxy`Lazy ghost (in-place initializer)Lazy proxy (factory returns the real instance)Skipping initialization for known propertiesPractical gotchas (from the manual)SummaryFurther reading (official)
or search for other articles
Previous

PHP 8.5: 3 Features That Make Your Code Cleaner

2026-02-23Backend
No next post

Let's Talk.

LinkedInGitHubTwitter

© 2024 idnasirasira.

Designed & Engineered with ♥ in Jakarta.