Um objeto lento é um objeto cuja inicialização é adiada até que seu estado seja observado ou modificado. Alguns exemplos de casos de uso incluem componentes de injeção de dependência que fornecem serviços lentos totalmente inicializados somente se necessário, ORMs fornecendo entidades lentas que se hidratam do banco de dados somente quando acessados, ou um analisador JSON que atrasa a análise até que os elementos sejam acessados.
Duas estratégias de objetos lentos são suportadas: Objetos Fantasmas e Proxies Virtuais, doravante denominados "fantasmas lentos" e "proxies lentos". Em ambas as estratégias, o objeto lento é anexado a um inicializador ou fábrica que é chamado automaticamente quando seu estado é observado ou modificado pela primeira vez. Do ponto de vista da abstração, os objetos fantasmas lentos são indistinguíveis dos não-lentos: eles podem ser usados sem que se saiba que são lentos, permitindo que sejam passados e usados por códigos que desconhecem a lentidão. Os proxies lentos são igualmente transparentes, mas deve-se ter cuidado ao usar sua identidade, pois o proxy e sua instância real possuem identidades diferentes.
É possível criar uma instância lenta de qualquer classe definida pelo usuário ou da classe stdClass (outras classes internas não são suportadas), ou redefinir uma instância dessas classes para torná-la lenta. Os pontos de entrada para criar um objeto lento são os métodos ReflectionClass::newLazyGhost() e ReflectionClass::newLazyProxy().
Ambos os métodos aceitam uma função que é chamada quando o objeto requer inicialização. O comportamento esperado da função varia dependendo da estratégia em uso, conforme descrito na documentação de referência de cada método.
Exemplo #1 Criando um Fantasma Lento
<?php
class Example
{
public function __construct(public int $prop)
{
echo __METHOD__, "\n";
}
}
$reflector = new ReflectionClass(Example::class);
$lazyObject = $reflector->newLazyGhost(function (Example $object) {
// Inicializa o objeto no local
$object->__construct(1);
});
var_dump($lazyObject);
var_dump(get_class($lazyObject));
// Dispara a inicialização
var_dump($lazyObject->prop);
?>
O exemplo acima produzirá:
lazy ghost object(Example)#3 (0) { ["prop"]=> uninitialized(int) } string(7) "Example" Example::__construct int(1)
Exemplo #2 Criando um Proxy Lento
<?php
class Example
{
public function __construct(public int $prop)
{
echo __METHOD__, "\n";
}
}
$reflector = new ReflectionClass(Example::class);
$lazyObject = $reflector->newLazyProxy(function (Example $object) {
// Cria e retorna a instância real
return new Example(1);
});
var_dump($lazyObject);
var_dump(get_class($lazyObject));
// Dispara a inicialização
var_dump($lazyObject->prop);
?>
O exemplo acima produzirá:
lazy proxy object(Example)#3 (0) { ["prop"]=> uninitialized(int) } string(7) "Example" Example::__construct int(1)
Qualquer acesso às propriedades de um objeto lento aciona sua inicialização (inclusive via ReflectionProperty). No entanto, certas propriedades podem ser conhecidas antecipadamente e não devem acionar a inicialização quando acessadas:
Exemplo #3 Inicializando Propriedades Ansiosamente
<?php
class BlogPost
{
public function __construct(
public int $id,
public string $title,
public string $content,
) { }
}
$reflector = new ReflectionClass(BlogPost::class);
$post = $reflector->newLazyGhost(function ($post) {
$data = fetch_from_store($post->id);
$post->__construct($data['id'], $data['title'], $data['content']);
});
// Sem esta linha, a seguinte chamada a ReflectionProperty::setValue()
// acionaria a inicialização.
$reflector->getProperty('id')->skipLazyInitialization($post);
$reflector->getProperty('id')->setValue($post, 123);
// Alternativamente, pode-se usar isso diretamente:
$reflector->getProperty('id')->setRawValueWithoutLazyInitialization($post, 123);
// A propriedade id pode ser acessada sem acionar a inicialização
var_dump($post->id);
?>
Os métodos ReflectionProperty::skipLazyInitialization() e ReflectionProperty::setRawValueWithoutLazyInitialization() oferecem maneiras de contornar a inicialização lenta ao acessar uma propriedade.
Fantasmas lentos são objetos que inicializam no local e, uma vez inicializados, são indistinguíveis de um objeto que nunca foi lento. Esta estratégia é adequada quando são controladas tanto a instanciação quanto a inicialização do objeto, tornando-a inadequada se qualquer um deles for gerenciado por outra parte.
Proxies lentos, uma vez inicializados, atuam como proxies para uma instância real: qualquer operação em um proxy lento inicializado é encaminhada para a instância real. A criação da instância real pode ser delegada a outra parte, tornando esta estratégia útil nos casos em que fantasmas lentos são inadequados. Embora os proxies lentos sejam quase tão transparentes quanto os fantasmas lentos, é necessário cautela quando sua identidade é usada, pois o proxy e sua instância real possuem identidades distintas.
Os objetos podem se tornar lentos no momento da instanciação usando ReflectionClass::newLazyGhost() ou ReflectionClass::newLazyProxy(), ou após a instanciação usando ReflectionClass::resetAsLazyGhost() ou ReflectionClass::resetAsLazyProxy(). Depois disso, um objeto lento pode ser inicializado por meio de uma das seguintes operações:
Como os objetos lentos são inicializados quando todas as suas propriedades são marcadas como não-lentas, os métodos acima não marcarão um objeto como lento se nenhuma propriedade puder ser marcada como lenta.
Objetos lentos são projetados para serem totalmente transparentes para seus consumidores, portanto, operações normais que observam ou modificam o estado do objeto acionarão automaticamente a inicialização antes que a operação seja executada. Isto inclui, mas não está limitado às seguintes operações:
Chamadas de método que não acessam o estado do objeto não acionarão a inicialização. Da mesma forma, as interações com o objeto que invocam métodos mágicos ou funções de gancho não acionarão a inicialização se esses métodos ou funções não acessarem o estado do objeto.
Os seguintes métodos específicos ou operações de baixo nível permitem acesso ou modificação de objetos lentos sem acionar a inicialização:
ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE
está definida, a menos que
__serialize() ou
__sleep() acionem a inicialização.
Esta seção descreve a sequência de operações executadas quando a inicialização é acionada, com base na estratégia em uso.
null
ou nenhum valor. O objeto não é
mais lento neste ponto, então a função pode acessar suas propriedades
diretamente.
Após a inicialização, o objeto é indistinguível de um objeto que nunca foi lento.
Após a inicialização, acessar qualquer propriedade no proxy produzirá o mesmo resultado que acessar a propriedade correspondente na instância real; todos os acessos de propriedade no proxy são encaminhados para a instância real, incluindo propriedades declaradas, dinâmicas, inexistentes ou marcadas com ReflectionProperty::skipLazyInitialization() ou ReflectionProperty::setRawValueWithoutLazyInitialization().
O objeto proxy em si não é substituído pela instância real.
Embora a fábrica receba o proxy como primeiro parâmetro, não se espera modificá-lo (modificações são permitidas, mas serão perdidas durante a etapa final de inicialização). Entretanto, o proxy pode ser usado para decisões baseadas nos valores das propriedades inicializadas, na classe, no próprio objeto ou em sua identidade. Por exemplo, o inicializador pode usar o valor de uma propriedade inicializada ao criar a instância real.
O escopo e o contexto $this do inicializador ou da função de fábrica permanecem inalterados e as restrições de visibilidade usuais se aplicam.
Após a inicialização bem-sucedida, o inicializador ou função de fábrica não é mais referenciado pelo objeto e pode ser liberado se não tiver outras referências.
Se o inicializador lançar uma exceção, o estado do objeto será revertido para seu estado de pré-inicialização e o objeto será marcado como lento novamente. Em outras palavras, todos os efeitos no próprio objeto são revertidos. Outros efeitos colaterais, como efeitos em outros objetos, não são revertidos. Isso evita a exposição de uma instância parcialmente inicializada em caso de falha.
Clonar um objeto lento aciona sua inicialização antes do clone ser criado, resultando em um objeto inicializado.
Para objetos proxy, tanto o proxy quanto sua instância real são clonados e
o clone do proxy é retornado.
O método __clone
é chamado na instância real, não no proxy.
O proxy clonado e a instância real são vinculados como estão durante
a inicialização, portanto, os acessos ao clone do proxy são encaminhados para o clone
da instância real.
Esse comportamento garante que o clone e o objeto original mantenham estados separados. As alterações no objeto original ou no estado de seu inicializador após a clonagem não afetam o clone. Clonar o proxy e sua instância real, em vez de retornar apenas um clone da instância real, garante que a operação de clonagem retorne consistentemente um objeto da mesma classe.
Para fantasmas lentos, o destrutor só é chamado se o objeto tiver sido inicializado. Para proxies, o destrutor só é chamado na instância real, se existir.
Os métodos ReflectionClass::resetAsLazyGhost() e ReflectionClass::resetAsLazyProxy() podem invocar o destrutor do objeto que está sendo redefinido.