HardТеория8 min

Ленивые объекты

ReflectionClass::newLazyGhost, newLazyProxy, ghost vs proxy, ORM, DI Container

Ленивые объекты (Lazy Objects) — PHP 8.4

Что такое Lazy Objects

Lazy Objects (PHP 8.4+) -- встроенный механизм отложенной инициализации объектов. Объект создается "пустым" и инициализируется только при первом обращении к его свойствам или методам.

Два подхода:

  • Lazy Ghost -- объект самого целевого класса, инициализируется "на месте"
  • Lazy Proxy -- прокси-объект, делегирующий к реальному экземпляру
<?php
declare(strict_types=1);

class ExpensiveService
{
    public string $status;

    public function __construct()
    {
        // Expensive initialization: DB connection, API call, etc.
        sleep(2);  // Simulates heavy work
        $this->status = 'initialized';
        echo "ExpensiveService created!\n";
    }

    public function process(): string
    {
        return "Processing with status: {$this->status}";
    }
}

// WITHOUT lazy objects — always pays initialization cost
// $service = new ExpensiveService();  // 2 seconds even if never used

// WITH lazy ghost — initialization deferred
$reflector = new ReflectionClass(ExpensiveService::class);
$service = $reflector->newLazyGhost(function(ExpensiveService $object): void {
    // This runs ONLY when $service is first accessed
    $object->__construct();
});

echo "Service created (lazy)\n";  // Instant — no initialization yet

// First access triggers initialization
echo $service->process();  // NOW the constructor runs
// Output:
// Service created (lazy)
// ExpensiveService created!
// Processing with status: initialized

newLazyGhost()

Ghost -- сам объект инициализируется при первом доступе. Его класс -- это целевой класс напрямую.

<?php
declare(strict_types=1);

class User
{
    public int $id;
    public string $name;
    public string $email;
    private \DateTimeImmutable $createdAt;

    public function __construct(int $id, string $name, string $email)
    {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
        $this->createdAt = new \DateTimeImmutable();
        echo "User #{$id} loaded from database\n";
    }

    public function getCreatedAt(): \DateTimeImmutable
    {
        return $this->createdAt;
    }
}

// Create lazy ghost
$reflector = new ReflectionClass(User::class);

$lazyUser = $reflector->newLazyGhost(
    function(User $user): void {
        // Initializer — called on first property access
        // Simulates loading from database
        $user->__construct(42, 'Alice', '[email protected]');
    }
);

// Object exists but is NOT initialized yet
var_dump($lazyUser instanceof User);  // true — it IS a User
echo get_class($lazyUser);            // 'User'

// First access triggers initialization
echo $lazyUser->name;  // Triggers initializer → "User #42 loaded from database"
                        // Then returns: "Alice"

// Subsequent accesses do NOT trigger initializer again
echo $lazyUser->email;  // "[email protected]" — no re-initialization

Ghost с Closure инициализатором

<?php
declare(strict_types=1);

class Product
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly float $price,
        public readonly string $category,
    ) {
    }
}

class ProductRepository
{
    /**
     * Creates a lazy Product that loads from DB on first access.
     */
    public function findLazy(int $id): Product
    {
        $reflector = new ReflectionClass(Product::class);

        return $reflector->newLazyGhost(
            function(Product $product) use ($id): void {
                // Simulate database query
                $data = $this->fetchFromDatabase($id);
                $product->__construct(
                    id: $data['id'],
                    name: $data['name'],
                    price: $data['price'],
                    category: $data['category'],
                );
            }
        );
    }

    /**
     * @return array{id: int, name: string, price: float, category: string}
     */
    private function fetchFromDatabase(int $id): array
    {
        echo "SQL: SELECT * FROM products WHERE id = {$id}\n";
        return [
            'id' => $id,
            'name' => 'Widget',
            'price' => 19.99,
            'category' => 'Tools',
        ];
    }
}

$repo = new ProductRepository();

// No SQL query yet
$product = $repo->findLazy(42);
echo "Product object created\n";

// SQL query happens NOW
echo $product->name;
// Output:
// Product object created
// SQL: SELECT * FROM products WHERE id = 42
// Widget

newLazyProxy()

Proxy -- создается proxy-объект, который при инициализации получает ссылку на реальный объект и делегирует все вызовы ему.

<?php
declare(strict_types=1);

class HeavyService
{
    public string $name;

    public function __construct(string $name)
    {
        echo "HeavyService '{$name}' initialized\n";
        $this->name = $name;
    }

    public function execute(): string
    {
        return "Executing {$this->name}";
    }
}

// Create lazy proxy
$reflector = new ReflectionClass(HeavyService::class);

$proxy = $reflector->newLazyProxy(
    function(HeavyService $proxy): HeavyService {
        // Initializer RETURNS the real object
        return new HeavyService('RealService');
    }
);

// Proxy is not initialized yet
echo get_class($proxy);  // 'HeavyService'

// First access triggers initialization and delegates to real object
echo $proxy->execute();
// Output:
// HeavyService 'RealService' initialized
// Executing RealService

echo $proxy->name;  // 'RealService' — delegated to real object

Ключевое отличие Ghost vs Proxy

<?php
declare(strict_types=1);

class Service
{
    public function __construct(
        public string $type = 'default',
    ) {
    }
}

$reflector = new ReflectionClass(Service::class);

// GHOST: initializer populates the SAME object
$ghost = $reflector->newLazyGhost(function(Service $obj): void {
    $obj->__construct('ghost');
    // $obj IS the lazy object itself
});

// PROXY: initializer returns a DIFFERENT object
$proxy = $reflector->newLazyProxy(function(Service $obj): Service {
    return new Service('proxy');
    // Returns a new object; proxy delegates to it
});

// Both look the same from outside
echo $ghost->type;  // 'ghost'
echo $proxy->type;  // 'proxy'

// But internally:
// Ghost: the object itself was initialized
// Proxy: a separate real object exists, proxy delegates to it
Аспект Ghost Proxy
Инициализатор Заполняет сам объект Возвращает реальный объект
Идентичность $lazy === $lazy (тот же объект) $proxy !== $realObject
Применение Простая отложенная инициализация Замена объекта / decorator
spl_object_id Не меняется Proxy имеет свой ID

Когда происходит инициализация

<?php
declare(strict_types=1);

class Example
{
    public int $value = 0;
    private string $secret = '';

    public function __construct(int $value, string $secret)
    {
        $this->value = $value;
        $this->secret = $secret;
        echo "Initialized!\n";
    }

    public function getSecret(): string
    {
        return $this->secret;
    }
}

$reflector = new ReflectionClass(Example::class);
$lazy = $reflector->newLazyGhost(function(Example $obj): void {
    $obj->__construct(42, 'hidden');
});

// These DO trigger initialization:
$lazy->value;           // Reading a property → triggers
$lazy->value = 10;      // Writing a property → triggers
$lazy->getSecret();     // Calling a method → triggers
isset($lazy->value);    // isset on property → triggers
unset($lazy->value);    // unset on property → triggers
serialize($lazy);       // Serialization → triggers
clone $lazy;            // Cloning → triggers
(array) $lazy;          // Array cast → triggers

// These do NOT trigger initialization:
$lazy instanceof Example;  // Does NOT trigger
get_class($lazy);          // Does NOT trigger
var_dump($lazy);           // Special: shows lazy state info

isLazyObject() и маркировка

<?php
declare(strict_types=1);

class Config
{
    public function __construct(
        public string $dsn = '',
        public int $timeout = 30,
    ) {
    }
}

$reflector = new ReflectionClass(Config::class);
$lazy = $reflector->newLazyGhost(function(Config $obj): void {
    $obj->__construct('pgsql:host=localhost', 60);
});

// Check if object is lazy (not yet initialized)
$refLazy = new ReflectionClass($lazy);
var_dump($refLazy->isUninitializedLazyObject($lazy));  // true

// Access triggers initialization
$lazy->dsn;

// Now it's initialized
var_dump($refLazy->isUninitializedLazyObject($lazy));  // false

Принудительная инициализация

<?php
declare(strict_types=1);

class Service
{
    public string $status = 'ready';

    public function __construct()
    {
        echo "Service initialized\n";
    }
}

$reflector = new ReflectionClass(Service::class);
$lazy = $reflector->newLazyGhost(function(Service $obj): void {
    $obj->__construct();
});

// Force initialization without accessing properties
$reflector->initializeLazyObject($lazy);
// Output: Service initialized

// Object is now fully initialized
echo $lazy->status;  // 'ready' (no re-initialization)

Маркировка объекта как инициализированного

<?php
declare(strict_types=1);

class Entity
{
    public int $id;
    public string $name;
}

$reflector = new ReflectionClass(Entity::class);
$lazy = $reflector->newLazyGhost(function(Entity $obj): void {
    echo "This should NOT run\n";
    $obj->id = 1;
    $obj->name = 'Default';
});

// Manually set properties and mark as initialized
// using ReflectionProperty to skip the initializer
$idProp = $reflector->getProperty('id');
$nameProp = $reflector->getProperty('name');

$idProp->setRawValueWithoutLazyInitialization($lazy, 42);
$nameProp->setRawValueWithoutLazyInitialization($lazy, 'Manual');

$reflector->markLazyObjectAsInitialized($lazy);

// Initializer is NOT called
echo $lazy->id;    // 42
echo $lazy->name;  // 'Manual'

Use Cases: ORM Lazy Loading

<?php
declare(strict_types=1);

class Order
{
    public function __construct(
        public readonly int $id,
        public readonly string $status,
        public readonly float $total,
    ) {
    }
}

class Customer
{
    /** @var list<Order> */
    public array $orders = [];

    public function __construct(
        public readonly int $id,
        public readonly string $name,
    ) {
    }
}

class EntityManager
{
    /**
     * Creates a lazy Customer that loads data on first access.
     */
    public function getReference(int $id): Customer
    {
        $reflector = new ReflectionClass(Customer::class);

        return $reflector->newLazyGhost(
            function(Customer $customer) use ($id): void {
                $data = $this->loadCustomer($id);
                $customer->__construct($data['id'], $data['name']);
                $customer->orders = $this->loadOrders($id);
            }
        );
    }

    /**
     * @return array{id: int, name: string}
     */
    private function loadCustomer(int $id): array
    {
        echo "Loading customer #{$id} from DB\n";
        return ['id' => $id, 'name' => 'Alice'];
    }

    /**
     * @return list<Order>
     */
    private function loadOrders(int $id): array
    {
        echo "Loading orders for customer #{$id}\n";
        return [
            new Order(1, 'completed', 99.99),
            new Order(2, 'pending', 49.99),
        ];
    }
}

$em = new EntityManager();

// Get reference — no DB query yet
$customer = $em->getReference(1);
echo "Reference obtained\n";

// Type check works without loading
var_dump($customer instanceof Customer);  // true

// First property access loads everything
echo $customer->name;
// Output:
// Reference obtained
// Loading customer #1 from DB
// Loading orders for customer #1
// Alice

echo count($customer->orders);  // 2 (already loaded)

Use Case: DI Container

<?php
declare(strict_types=1);

class Logger
{
    public function __construct()
    {
        echo "Logger created\n";
    }

    public function log(string $message): void
    {
        echo "[LOG] {$message}\n";
    }
}

class Mailer
{
    public function __construct()
    {
        echo "Mailer created\n";
    }

    public function send(string $to, string $body): void
    {
        echo "Sending to {$to}: {$body}\n";
    }
}

class Container
{
    /** @var array<class-string, Closure(): object> */
    private array $factories = [];

    /**
     * @template T of object
     * @param class-string<T> $class
     * @param Closure(): T $factory
     */
    public function register(string $class, Closure $factory): void
    {
        $this->factories[$class] = $factory;
    }

    /**
     * Returns a lazy object that initializes on first use.
     *
     * @template T of object
     * @param class-string<T> $class
     * @return T
     */
    public function get(string $class): object
    {
        if (!isset($this->factories[$class])) {
            throw new RuntimeException("Service not registered: {$class}");
        }

        $factory = $this->factories[$class];
        $reflector = new ReflectionClass($class);

        // Return lazy proxy — real object created only on first use
        return $reflector->newLazyProxy(
            fn(object $proxy): object => $factory()
        );
    }
}

// Register services
$container = new Container();
$container->register(Logger::class, fn(): Logger => new Logger());
$container->register(Mailer::class, fn(): Mailer => new Mailer());

// Get lazy services — nothing created yet
$logger = $container->get(Logger::class);
$mailer = $container->get(Mailer::class);
echo "Services obtained from container\n";

// Only Logger is created when used
$logger->log('Hello');
// Output:
// Services obtained from container
// Logger created
// [LOG] Hello

// Mailer is STILL not created — never used!

Сравнение с __get/__set magic

<?php
declare(strict_types=1);

// OLD approach: magic methods (fragile, no type safety)
class OldLazyService
{
    private ?object $realService = null;
    private array $pendingProps = [];

    public function __get(string $name): mixed
    {
        $this->initialize();
        return $this->realService->$name;
    }

    public function __set(string $name, mixed $value): void
    {
        $this->pendingProps[$name] = $value;
    }

    public function __call(string $name, array $args): mixed
    {
        $this->initialize();
        return $this->realService->$name(...$args);
    }

    private function initialize(): void
    {
        if ($this->realService === null) {
            $this->realService = new ActualService();
        }
    }
}
// Problems: no instanceof, no type hints, no IDE support

// NEW approach: PHP 8.4 lazy objects
class ActualService
{
    public string $name = 'service';

    public function execute(): string
    {
        return 'done';
    }
}

$reflector = new ReflectionClass(ActualService::class);
$lazy = $reflector->newLazyGhost(fn(ActualService $s) => $s->__construct());

// ✓ instanceof works
var_dump($lazy instanceof ActualService);  // true

// ✓ Type hints work
function process(ActualService $service): string
{
    return $service->execute();
}
echo process($lazy);  // 'done'

// ✓ IDE autocompletion works (it's the real class)
// ✓ No magic methods needed
// ✓ Full type safety

Проверь себя

5 из 10

Как принудительно инициализировать lazy объект?

Вернет ли `instanceof` правильный результат для lazy ghost?

Что делает `setRawValueWithoutLazyInitialization()`?

Где Lazy Objects наиболее полезны?

С какой версии PHP доступны Lazy Objects?