Many requests build services they never use, such as a report service that opens a database connection. Lazy objects (PHP 8.4) hand out the object at once and run the expensive initializer when code first touches its state. ReflectionClass::newLazyGhost() creates an empty instance of the real class and fills it in place; newLazyProxy() calls a factory and forwards to the object it returns.
<?php
final class ReportService
{
public function __construct(private string $dsn) { echo " connecting to $dsn\n"; }
public function totals(): string { return "totals from {$this->dsn}"; }
}
$class = new ReflectionClass(ReportService::class);
$ghost = $class->newLazyGhost(function (ReportService $object): void {
$object->__construct('mysql:dbname=shop');
});
echo "ghost created, lazy: ", json_encode($class->isUninitializedLazyObject($ghost)), "\n";
echo $ghost->totals(), "\n";ghost created, lazy: true connecting to mysql:dbname=shop totals from mysql:dbname=shop
Calling totals() did not initialize the ghost; reading $this->dsn inside it did. A proxy from newLazyProxy(fn() => new ReportService('mysql:host=replica')) behaved the same way when tried. Both pass instanceof ReportService, and the class needs no changes, even though it is final. You will rarely call these methods yourself: dependency injection containers (Dependency Injection) and ORMs use them in place of generated proxy classes. var_dump() and serialize() initialize the object, so debugging output can hide the saving you are measuring.