Ленивые объекты (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