Лінивий об'єкт створюється одразу, але його справжня ініціалізація (дорогий конструктор, запит до бази, з'єднання) відкладається до моменту, коли об'єкт справді знадобиться. PHP 8.4 зробив це вбудованою можливістю мови через Reflection.
Два види:
- Ghost - сам об'єкт потрібного класу, який ініціалізується «на місці» при першому зверненні до його стану:
$reflector = new ReflectionClass(Connection::class);
$connection = $reflector->newLazyGhost(function (Connection $object): void {
$object->__construct(config('database.dsn')); // підключення лише тепер
});
// ... з'єднання ще не встановлено
$connection->query('SELECT 1'); // перше звернення до властивостей - ініціалізація
- Proxy - об'єкт-замісник, який при першому зверненні створює справжній екземпляр через фабрику й далі перенаправляє йому всі звернення:
$service = $reflector->newLazyProxy(fn () => $container->make(HeavyService::class));
Важливий нюанс: ініціалізацію запускає звернення до властивостей об'єкта. Виклик методу, який не чіпає стан, лінивий об'єкт не ініціалізує.
Де це корисно:
- Контейнери залежностей: сервіс, впроваджений у контролер, але не використаний у цьому запиті, не створюється взагалі.
- ORM: зв'язана сутність завантажується з бази лише при зверненні до неї (так Doctrine давно робила через згенеровані класи-проксі - тепер це вміє мова).
- Важкі клієнти: з'єднання з Redis, S3, сторонніми API, яке потрібне лише в частині запитів.
Чим краще за рукописні проксі: не треба генерувати код класів-проксі, об'єкт лишається того самого класу (працюють instanceof і типи), а final-класи теж можна зробити лінивими.
Обмеження: ініціалізація може відбутися в несподіваний момент (наприклад, при var_dump чи серіалізації), а помилка в ініціалізаторі виникне там, де об'єкт уперше використали, а не там, де його створили.