PHP - Um pouco de Reflection

Reflection(ou reflexão) é uma técnica onde se pode manipular o próprio programa como sendo seus dados, permitindo assim modificar sua estrutura. Ela permite realizar a engenharia reversa de um classe em tempo de execução, recuperando nomes de métodos para verificar sua existência, descobrir seus parâmetros e tipos, mudar visibilidade de propriedades, invocar métodos dinamicamente entre outras funcionalidades. Ela é muito utilizada em frameworks, por exemplo de testes onde declaramos vários métodos com o nome iniciando com a palavra ‘test‘ e eles são invocados automaticamente.

Para começar precisamos criar uma objeto da ReflectionClass a partir da classe que queremos manipular. Para nosso exemplo vamos utilizar uma classe simples de exemplo: Cachorro.

class Cachorro{
    private string $nome;
    private int $idade;

    public function __construct(string $nome, int $idade)
    {
        $this->nome = $nome;
        $this->idade = $idade;
    }

    public function getNome(): string
    {
        return $this->nome;
    }

    public function  setNome(string $nome): void
    {
        $this->nome = $nome;
    }

    public function latir(): void
    {
        echo 'au au au!';
    }

}

Agora criamos a classe RefletionClass a partir dela.

include './Cachorro.php';

$refClass = new ReflectionClass( Cachorro::class );

//$refClass = new ReflectionClass( 'Cachorro' );

//$cachorro = new Cachorro('Fifi',3);
//$refClass = new ReflectionClass( $cachorro );

echo  $refClass->getName() . PHP_EOL;

Como podemos ver podemos instanciar a ReflectionClass passando para seu construtor tanto o nome da classe em string(usando a palavra chave class) ou passando um objeto já instanciado.

A partir do objeto $refClass podemos pegar as informações da classe, no caso utilizamos o método getName para pegar o nome dela.

Manipulando Propriedades

Para manipular as propriedades precisamos primeiro recuperá-las. Podemos pegar todas utilizando o método getProperties que retorna um array de objetos ReflectionProperty

$attrs = $refClass->getProperties();
foreach($attrs as $attr){
    echo $attr->getName() . PHP_EOL;
}

Também podemos pegar uma propriedade individual pelo seu nome, através do método getProperty que retorna um objeto ReflectionProperty.

if($refClass->hasProperty('nome')){
    $attr = $reflectionClass->getProperty('nome');
    echo $attr->getName() . PHP_EOL;
}

Se tentarmos pegar uma propriedade que não existe um erro será gerado, então é interessante verificar se a classe realmente possui a propriedade que estamos tentando acessar. Isto pode ser feito utilizando o método hasProperty passando o nome da propriedade.

Com um objeto ReflectionProperty podemos acessar os dados da propriedade como nome(getName), tipo(getType) e seu valor(getValue), lembrando que para utilizar o método getValue é necessário passar um objeto instanciado para ele obter o valor do atributo.

$cachorro = new Cachorro('Fifi',3);

$attr = $refClass->getProperty('nome');
echo $attr->getName() . PHP_EOL;
echo $attr->getType() . PHP_EOL; 

echo $attr->getValue($cachorro) . PHP_EOL; 

Se você executar o código acima receberá um erro na linha 7, isso se deve a que a propriedade nome é privada e não pode ser acessada , podemos contornar isso já que estamos utilizando Reflection, podemos deixar o atributo visível chamando o método setAccessible, mas antes disso podemos verificar a visibilidade dele através do método isPrivate, que, como o próprio nome sugere, retorna true se o método é privado ou false se não é. Também temos os métodos isProtected e isPublic para verificar as outras visibilidades.

if($attr->isPrivate()){
    $attr->setAccessible(true);
}
        
echo $attr->getValue($cachorro) . PHP_EOL; 

Para modificar o valor de uma propriedade utilizamos o método setValue passando o objeto que queremos modificar e o novo valor da propriedade

$cachorro = new Cachorro('Fifi',3);

$attr = $refClass->getProperty('nome');
if($attr->isPrivate()){
    $attr->setAccessible(true);
}
        
$attr->setValue($cachorro, 'Sãosão'); 

echo $cachorro->getNome();

Manipulando Métodos

De forma semelhante podemos recuperar todos os métodos de uma classe utilizando o método getMethods que retorna uma array de ReflectionMethod.

$methods = $refClass->getMethods();
foreach($methods as $method){
    echo $method->getName(). PHP_EOL;
}

Também podemos pegar um método pelo seu nome através de getMethod, passando o nome do método, mas antes é interessante verificar se o método existe na classe, isto pode ser feito utilizando hasMethod.

if( $refClass->hasMethod('setNome')){
    $method = $refClass->getMethod('setNome');
    echo $method->getName(). PHP_EOL;
}

Com um objeto ReflectionMethod podemos manipular as informações do método, como pegar seu nome com getName, ou pegar seu tipo de retorno com getReturnType, ou verificar sua visibilidade através dos métodos isPrivate, isProtected ou isPublic. Também podemos verificar se o método é o construtor da classe, o que pode ser interessante quando estamos percorrendo todos os métodos através do getMethods().

echo $method->getReturnType(). PHP_EOL;

echo ($method->isPrivate() ? 'Privado' : 'Não Privado') . PHP_EOL;
echo ($method->isProtected() ? 'Protegido' : 'Não Protegido') . PHP_EOL;
echo ($method->isPublic() ? 'Publico' : 'Não Publico') . PHP_EOL;
echo ($method->isConstructor() ? 'É o construtor' : 'Não é o construtor') . PHP_EOL;

Podemos pegar os parâmetros de um método através do getParameters, que retorna um array de objetos ReflectionParameter que fornece informações sobre o parâmetro como nome getName, tipo getType, se o parâmetro tem algum valor padrão com isDefaultValueAvailable ou o próprio valor padrão com getDefaultValue.

$params = $method->getParameters();
foreach($params as $param){
    echo "Nome: {$param->getName()}" . PHP_EOL;
    echo "Tipo: {$param->getType()}" . PHP_EOL;
    if($param->isDefaultValueAvailable()){
        echo "Valor padrão: {$param->getDefaultValue()}". PHP_EOL;
    }
    echo '------------' . PHP_EOL;
}

Invocando um método via Reflection

Podemos invocar um método através de um objeto ReflectionMethod, para isso chamamos invoke passando o objeto instanciado do qual queremos executar o método.

$cachorro = new Cachorro('Fifi',3);

if( $refClass->hasMethod('latir')){
    $method = $refClass->getMethod('latir');
    $method->invoke($cachorro);
}
// irá chamar latir()  exibindo 'au au au!'

Caso o método possua parâmetros podemos passar eles após o objeto, como no método setNome que aceita uma string de nome.

$cachorro = new Cachorro('Fifi',3);

if( $refClass->hasMethod('setNome')){
    $method = $refClass->getMethod('setNome');
    $method->invoke($cachorro, 'Sãosão');
}

echo $cachorro->getNome();

Também podemos chamar um método passando seus parâmetros como um array através do método invokeArgs onde o objeto é passado como primeiro parâmetro e o segundo o array com os parâmetros.

$cachorro = new Cachorro('Fifi',3);
 
if( $refClass->hasMethod('setNome')){
    $method = $refClass->getMethod('setNome');
    $method->invokeArgs($cachorro, ['Sãosão']);
}
 
echo $cachorro->getNome();

Instanciando um objeto através de ReflectionClass

Podemos criar um objeto a partir de um objeto ReflectionClass, para isso chamamos o método newInstance passando os parâmetros do construtor para o método.

$cachorro = $refClass->newInstance('fifi', 3);

echo $cachorro->getNome() . PHP_EOL;
$cachorro->latir();

Também podemos passar os argumentos como um array de forma semelhante feita com um método.

$cachorro = $refClass->newInstanceArgs([ 'fifi' , 3 ]);

echo $cachorro->getNome() . PHP_EOL;
echo $cachorro->latir();

Também é possível construir um objeto ignorando seu construtor, fazemos isso através do método newInstanceWithoutConstructor. Mas devemos ter cuidado com as inicializações, por exemplo, se alguma propriedade estava sendo inicializada no construtor podemos ter erros de propriedade não inicializada ao utilizar algum método que a use. Por exemplo:

$cachorro = $refClass->newInstanceWithoutConstructor();

echo $cachorro->getNome() . PHP_EOL;
echo $cachorro->latir();

Bom era isso o uso básico de Reflection, o uso mais comum é em frameworks e ferramentas, mas é muito interessante entendermos pelo menos um pouco de como isso funciona. Mais métodos podem ser encontrados na documentação do PHP.

T++