omegaalfa / lazy-object
Small typed facade for PHP 8.4 native lazy proxies and lazy ghosts
Requires
- php: ^8.4
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.5
This package is not auto-updated.
Last update: 2026-07-26 18:34:28 UTC
README
⚡ Lazy Object
Uma fachada pequena, tipada e fiel aos lazy objects nativos do PHP 8.4
Tip
Use a API moderna LazyObject::proxy() e LazyObject::ghost() sem criar uma implementação paralela das regras do PHP.
O pacote não implementa proxies próprios, não substitui regras do engine, não oferece compatibilidade artificial e não converte indiscriminadamente erros nativos.
📋 Requisitos
- PHP 8.4 ou superior;
- Composer.
📦 Instalação
composer require omegaalfa/lazy-object
🆚 Proxy ou ghost?
| 🔍 Característica | 🔀 Proxy | 👻 Ghost |
|---|---|---|
| 📞 Callback | Closure(T): object |
Closure(T): void |
| ⚙️ Inicialização | A factory fornece outra instância | O initializer configura o próprio objeto |
| 🪪 Identidade | O proxy mantém identidade própria e delega para uma instância real | O próprio objeto é inicializado in-place |
| 🎯 Uso típico | Factory, container ou criação delegada | Hidratação ou inicialização in-place |
🔀 Quando usar um proxy
O proxy é um objeto lazy que, no primeiro acesso que exige inicialização, chama uma factory. Essa factory cria e retorna outra instância, chamada de instância real. Depois disso, as operações feitas no proxy são delegadas para ela.
Proxy criado → primeiro acesso → factory executada → instância real criada → operações delegadas
✅ Cenários recomendados:
- criação controlada por um container de dependências;
- serviços cuja construção precisa ser totalmente delegada;
- conexões, clientes HTTP, mailers e gateways caros;
- factories que podem escolher dinamicamente a implementação real;
- integração com componentes que já sabem construir o serviço completo;
- quando é importante separar o objeto exposto da instância que executa o trabalho.
⚠️ Considere as seguintes características:
- o proxy e a instância real têm identidades diferentes;
- a factory precisa retornar um objeto aceito pelas regras nativas do PHP;
- retornar o próprio proxy ou um objeto incompatível é rejeitado pelo engine;
- o proxy normalmente possui mais overhead que um ghost;
- regras de herança entre proxy e instância real são verificadas pelo PHP.
👻 Quando usar um ghost
O ghost é criado sem executar o construtor. No primeiro acesso que exige inicialização, o initializer configura o próprio objeto in-place, atribuindo propriedades ou chamando seu construtor.
Ghost criado → primeiro acesso → initializer executado → mesmo objeto inicializado
✅ Cenários recomendados:
- hidratação tardia de entidades e modelos;
- carregamento sob demanda a partir de banco de dados ou armazenamento;
- objetos cuja identidade precisa permanecer a mesma;
- inicialização tardia por atribuição de propriedades;
- classes cujo construtor pode ser chamado pelo initializer;
- objetos de domínio que devem ser preenchidos in-place.
⚠️ Considere as seguintes características:
- o initializer não cria nem retorna uma instância substituta;
- o retorno esperado é
void; - o próprio ghost recebe o estado inicializado;
- o initializer precisa deixar o objeto em um estado válido;
- classes sem propriedades de instância podem não permanecer lazy.
🧭 Guia rápido de decisão
| Se você precisa de... | Escolha |
|---|---|
| 🏭 Delegar a criação completa para uma factory | Proxy |
| 📦 Resolver o serviço por um container | Proxy |
| 🔀 Escolher a instância real dinamicamente | Proxy |
| 🪪 Preservar a identidade do objeto | Ghost |
| 💧 Hidratar propriedades posteriormente | Ghost |
| 🧱 Executar o construtor no próprio objeto lazy | Ghost |
| 🗄️ Carregar uma entidade sob demanda | Ghost |
Tip
Se a callback deve retornar outro objeto, use um proxy. Se a callback deve configurar o objeto recebido, use um ghost.
🔬 Diferenças de identidade
$proxy = LazyObject::proxy( Service::class, static fn (Service $_proxy): object => new Service('real'), ); // O proxy mantém sua identidade e delega para outra instância. $ghost = LazyObject::ghost( Service::class, static fn (Service $ghost): void => $ghost->__construct('in-place'), ); // O próprio $ghost é inicializado; sua identidade é preservada.
⏱️ O que ambos têm em comum
Proxy e ghost compartilham o ciclo de vida básico oferecido pelo PHP:
- são criados sem executar imediatamente a callback;
- inicializam quando uma operação observa ou modifica seu estado;
- executam a callback somente uma vez após uma inicialização bem-sucedida;
- restauram o estado lazy quando a callback lança uma exceção;
- normalmente inicializam antes de clonagem ou serialização;
- continuam sendo
instanceofda classe solicitada; - dependem das regras nativas de lazy objects do PHP 8.4.
🚀 Exemplos
1. 🔀 Lazy proxy completo
<?php declare(strict_types=1); require __DIR__ . '/vendor/autoload.php'; use Omegaalfa\LazyObject\LazyObject; final class DatabaseConnection { public function __construct(public string $dsn) { echo "Conexão aberta\n"; } } $connection = LazyObject::proxy( DatabaseConnection::class, static fn (DatabaseConnection $_proxy): object => new DatabaseConnection('mysql:host=localhost;dbname=app'), ); echo "Proxy criado\n"; echo $connection->dsn; // Executa a factory.
Note
A factory recebe obrigatoriamente o proxy, mesmo quando não precisa utilizá-lo, e retorna a instância real. Closure(T): object é intencional: o engine aceita a mesma classe e certos casos de superclasse compatível. A biblioteca não duplica essas regras complexas. No PHP 8.4.16, retornar uma subclasse para um proxy da classe pai é rejeitado pelo engine.
2. 📦 Proxy criado por container
Considerando um container PSR-11 disponível em $container:
/** @var Psr\Container\ContainerInterface $container */ $mailer = LazyObject::proxy( Mailer::class, static fn (Mailer $_proxy): object => $container->get(Mailer::class), );
3. ⏳ Serviço caro usado condicionalmente
$reports = LazyObject::proxy( ReportGenerator::class, static fn (ReportGenerator $_proxy): object => new ReportGenerator(loadTemplates()), ); if ($request->wantsReport()) { echo $reports->generate(); }
4. 👻 Lazy ghost completo
<?php declare(strict_types=1); require __DIR__ . '/vendor/autoload.php'; use Omegaalfa\LazyObject\LazyObject; final class UserProfile { public function __construct(public int $id, public string $name) {} } $profile = LazyObject::ghost( UserProfile::class, static function (UserProfile $profile): void { $profile->__construct(42, 'Ada'); }, ); echo $profile->name; // Executa o initializer.
5. 💧 Ghost hidratando propriedades
$invoice = LazyObject::ghost( Invoice::class, static function (Invoice $invoice): void { $row = fetchInvoice(100); $invoice->id = $row['id']; $invoice->total = $row['total']; }, );
6. 🏷️ Nome da classe ou objeto
$fromClassName = LazyObject::ghost( Service::class, static fn (Service $service): void => $service->__construct('class-string'), ); $typeExample = new Service('apenas informa o tipo'); $fromObject = LazyObject::ghost( $typeExample, static fn (Service $service): void => $service->__construct('novo objeto'), ); var_dump($fromObject !== $typeExample); // true
A instância passada em $class serve para resolver o tipo. Ela não é reaproveitada como ghost nem como instância real do proxy.
7. 🔁 Exceção e nova tentativa
$attempts = 0; $service = LazyObject::ghost( Service::class, static function (Service $service) use (&$attempts): void { if (++$attempts === 1) { throw new RuntimeException('Falha temporária'); } $service->__construct('recuperado'); }, ); try { echo $service->value; } catch (RuntimeException) { echo $service->value; // O estado lazy foi restaurado; tenta novamente. }
8. 💾 Evitar inicialização na serialização
$service = LazyObject::ghost( Service::class, static fn (Service $service): void => $service->__construct('carregado'), \ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE, ); $payload = serialize($service); // O initializer não é executado.
Important
SKIP_INITIALIZATION_ON_SERIALIZE é a opção disponível para newLazyProxy() e newLazyGhost(). Não use SKIP_DESTRUCTOR: essa flag pertence aos métodos resetAsLazyProxy() e resetAsLazyGhost() e é rejeitada aqui.
9. 🧬 Clonagem
$service = LazyObject::ghost( Service::class, static fn (Service $service): void => $service->__construct('original'), ); $clone = clone $service; // Inicializa antes de clonar. $clone->value = 'clone';
10. 🔎 Inspeção explícita
$reflection = new \ReflectionClass($service); if ($reflection->isUninitializedLazyObject($service)) { $reflection->initializeLazyObject($service); }
🪶 Classes sem propriedades
Classes sem propriedades de instância — ou somente com propriedades estáticas ou virtuais — podem resultar em um objeto normal. Quando o engine retorna uma instância normal nesses casos, a callback não é executada. Consumidores não devem presumir que todo retorno esteja necessariamente lazy e não inicializado.
$reflection = new \ReflectionClass($object); var_dump($reflection->isUninitializedLazyObject($object));
Note
No PHP 8.4.16 testado, stdClass e uma classe de usuário sem propriedades retornaram objetos normais.
⚠️ Exceções
A biblioteca lança InvalidArgumentException para:
- classe inexistente (preservando
ReflectionExceptioncomo exceção anterior); - interface;
- trait;
- enum;
- classe abstrata.
As validações restantes são delegadas ao PHP. A biblioteca não converte erros do engine relacionados a classes internas incompatíveis, descendentes de classes internas, factory incompatível, retorno do próprio proxy ou outras regras nativas.
Warning
No PHP 8.4.16, classes/factories incompatíveis resultaram em Error/TypeError, enquanto uma opção inválida resultou em ReflectionException. Esses tipos são propagados sem conversão.
⚡ Cache de ReflectionClass
O pacote mantém um cache interno de ReflectionClass por nome de classe. A presença de uma reflexão no cache significa apenas que a classe foi refletida e passou pelas verificações estruturais locais. A compatibilidade completa com lazy objects, incluindo herança de classes internas, opções e compatibilidade entre proxy e instância real, continua sendo validada pelo engine do PHP em cada chamada.
O cache não possui API pública de controle. Consulte benchmarks/reflection-cache.php para medir seu impacto no ambiente de destino.
🎯 Gatilhos de inicialização
Operações sobre propriedades, como leitura, escrita, isset(), unset() e determinadas operações via Reflection, podem disparar a inicialização conforme as regras nativas do PHP. Clonagem e serialização inicializam por padrão, salvo quando uma opção nativa aplicável altera esse comportamento. Uma chamada de método pode não inicializar quando o método não observa nem modifica o estado.
🛠️ Desenvolvimento
composer install composer check XDEBUG_MODE=coverage composer coverage composer benchmark -- 20000 7 php -d opcache.enable_cli=1 benchmarks/reflection-cache.php 20000 7
composer check executa sintaxe/estilo básico, PHPUnit e PHPStan. O benchmark usa hrtime(true), warm-up, múltiplas rodadas e apresenta média, mediana, mínimo, máximo, desvio padrão, operações por segundo e memória do cache.
O @phpstan-ignore-next-line em LazyObject::proxy() é intencional: o stub atual do PHPStan exige callable(T): T, enquanto a API nativa declara uma factory que retorna object e deixa a compatibilidade concreta da instância retornada ser validada pelo engine. A anotação pode ser removida quando o stub refletir esse contrato.
Metodologia, ambiente e instruções detalhadas estão em benchmarks/README.md.
🔗 Compatibilidade
A série v1 exige PHP ^8.4 e usa diretamente ReflectionClass::newLazyProxy() e ReflectionClass::newLazyGhost().
Documentação oficial: Lazy Objects, newLazyProxy() e newLazyGhost().
📄 Licença
🟢 Distribuído sob a licença MIT. Consulte o arquivo LICENSE.