PHP

API de Reflexão do PHP

Veja como usar a API de reflexão do PHP para extrair e até mesmo alterar características internas de classes, interfaces, métodos etc, em tempo de execução.

há 4 anos 1 mês

Formação Desenvolvedor PHP
Conheça a formação em detalhes

O PHP implementa uma API de reflexão que permite com que façamos “engenharia reversa” para extrair e até mesmo alterar características internas de classes, interfaces, métodos e até mesmo de extensões, tudo isso em tempo de execução.

Os casos de uso para reflexão não costumam ser tão comuns no dia a dia de desenvolvimento, mas podemos destacar alguns:

  • Core de frameworks (para alguns comportamentos especiais como, por exemplo, no container de injeção de dependência para saber se determinada classe pode ser instanciada ou não);
  • Para testes (frameworks de teste usam principalmente para mocks);
  • Extrair informações de classes e gerar documentação;
  • Criar hydrators que extraiam/alimentem objetos com dados de forma dinâmica;
  • Extrair comentários de classes e criar meta-dados estendendo o comportamento delas (anotações);

Tudo sobre a API de reflexão você pode consultar direto na documentação oficial do PHP. Nesse artigo vou passar os aspectos que considero principais.

PHP - Fundamentos
Curso PHP - Fundamentos
Conhecer o curso

Um exemplo elementar pra dar a ideia do que é possível extrair:

<?php

/** @psalm-immutable */
final class Email
{
    public string $email;

    public function __construct(string $email)
    {
        $this->email = $email;
    }
}

$reflectionClass = new ReflectionClass('Email');

echo $reflectionClass->getName() . PHP_EOL;
echo ($reflectionClass->isFinal() ? 'Final' : 'Not final') . PHP_EOL;
echo $reflectionClass->getDocComment() . PHP_EOL;
echo $reflectionClass->getConstructor()->getNumberOfParameters() . PHP_EOL;

echo PHP_EOL . 'Property:' . PHP_EOL . PHP_EOL;

/** @var ReflectionProperty $property */
$property = $reflectionClass->getProperties()[0];
echo $property->getName() . PHP_EOL;
echo ($property->isPrivate() ? 'public' : 'not public') . PHP_EOL;
echo $property->getType() . PHP_EOL;

O resultado da execução desse exemplo:

Email
Final
/** @psalm-immutable */
1

Property:

email
not public
string

A ReflectionClass é responsável por extrair os dados de uma classe. Extraímos o nome da classe, se ela é final, o comentário associado a ela e o número de argumentos do seu construtor. O método getConstructor() retorna uma instância de ReflectionMethod. Depois, usando getProperties() , obtemos um array com os atributos da classe, mas na forma de instâncias de ReflectionProperty, que provê outros métodos de acesso.

Se quisermos dar uma espécie de var_dump() em uma classe:

<?php

/** @psalm-immutable */
final class Email
{
    public string $email;

    public function __construct(string $email)
    {
        $this->email = $email;
    }
}

Reflection::export(new ReflectionClass('Email'));

O resultado será:

/** @psalm-immutable */
Class [ <user> final class Email ] {
  @@ /home/kennedy/Documents/www/php-reflection/index.php 4-12

  - Constants [0] {
  }

  - Static properties [0] {
  }

  - Static methods [0] {
  }

  - Properties [1] {
    Property [ <default> public $email ]
  }

  - Methods [1] {
    Method [ <user, ctor> public method __construct ] {
      @@ /home/kennedy/Documents/www/php-reflection/index.php 8 - 11

      - Parameters [1] {
        Parameter #0 [ <required> string $email ]
      }
    }
  }
}

Podemos extrair diretamente as informações de um método sem precisar usar a ReflectionClass:

<?php

final class Node
{
    private Node $next;

    private function next(): Node
    {
        return $this->next;
    }
}

$reflectionMethod = new ReflectionMethod('Node', 'next');
echo $reflectionMethod->getName() . PHP_EOL; // next
echo ($reflectionMethod->isPrivate() ? 'private' : 'not private') . PHP_EOL; // private
echo $reflectionMethod->getReturnType() . PHP_EOL; // Node

Da mesma forma que ReflectionClass, também temos a ReflectionFunction:

<?php

function foo(int $bar) : int {
    return $bar + 1;
}

$reflectionFunction = new ReflectionFunction('foo');

echo $reflectionFunction->getName() . PHP_EOL; // foo
echo $reflectionFunction->getReturnType() . PHP_EOL; // int

$parameter = $reflectionFunction->getParameters()[0];
echo $parameter->getName() . PHP_EOL; // bar
echo $parameter->getType() . PHP_EOL; // int

var_dump($reflectionFunction->getClosure()); // class Closure#2 (1) {

Outra classe muito importante do “combo” é a ReflectionObject, que trabalha diretamente em uma instância de objeto:

<?php

final class Node
{
    private int $value;
    private ?Node $next;

    public function __construct(int $value, ?Node $next = null)
    {
        $this->value = $value;
        $this->next = $next;
    }

    private function next(): Node
    {
        return $this->next;
    }

    public function value(): int
    {
        return $this->value;
    }
}

$node = new Node(1, new Node(2));

$reflectionObject = new ReflectionObject($node);
echo $reflectionObject->getName() . PHP_EOL; // Node
echo $reflectionObject->getConstructor()->getNumberOfParameters() . PHP_EOL; // 2
echo $reflectionObject->getMethod('next')->getReturnType() . PHP_EOL; // Node
echo $reflectionObject->getMethod('value')->getReturnType() . PHP_EOL; // int

Existem outras classes e elas possuem outros vários métodos a serem explorados. Mas o que vimos até aqui é o essencial para que avancemos um pouco mais em exemplos palpáveis, do mundo real.

PHP Avançado
Curso PHP Avançado
Conhecer o curso

Um caso prático pra uso de reflexão

Criaremos uma classe transporter base. Um transporter é um conceito para transacionar dados (do input do usuário, por exemplo) entre objetos. Se você já ouviu falar de DTO (Data Transfer Objects), é basicamente isso, mas com outro nome, só pra causar confusão mesmo. :P

Mas deixando a ladainha de lado, show me the code:

<?php

declare(strict_types=1);

abstract class Transporter
{
    public function __construct(array $properties = [])
    {
        $reflection = new ReflectionObject($this);
        foreach ($properties as $name => $value) {
            $property = $reflection->getProperty($name);
            if ($property->isPublic() || !$property->isStatic()) {
                $this->$name = $value;
            }
        }
    }

    public function toArray(): array
    {
        $reflection = new ReflectionObject($this);
        return \array_map(
            function (ReflectionProperty $property) {
                return [
                    $property->getName() => $property->getValue($this)
                ];
            },
            $reflection->getProperties(ReflectionProperty::IS_PUBLIC)
        );
    }
}

final class UserTransporter extends Transporter
{
    public int $age;
    public string $firstName;
    public string $lastName;
}

$transporter = new UserTransporter([
    'age' => 29,
    'firstName' => 'Kennedy',
    'lastName' => 'Parreira',
]);

echo $transporter->age . PHP_EOL;
echo $transporter->firstName . PHP_EOL;
echo $transporter->lastName . PHP_EOL;

var_dump($transporter->toArray());

O resultado:

29
Kennedy
Parreira
/home/kennedy/Documents/www/php-reflection/index.php:50:
array(3) {
  [0] =>
  array(1) {
    'age' =>
    int(29)
  }
  [1] =>
  array(1) {
    'firstName' =>
    string(7) "Kennedy"
  }
  [2] =>
  array(1) {
    'lastName' =>
    string(8) "Parreira"
  }
}

Esse exemplo usou basicamente o que já tínhamos visto anteriormente. Ele permite que recebamos os dados via array (no construtor) e então alimenta os atributos do objeto dinamicamente. O método toArray() itera nos atributos públicos e não estáticos da classes e os retorna em um array.

Queue jobs do Laravel

Outro caso de uso interessante de se passar aqui, pra mostrar como reflexão pode ser útil em código de framework, refere-se às Queue Jobs do Laravel:

<?php

namespace App\Jobs;

use App\Podcast;
use App\AudioProcessor;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

class ProcessPodcast implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $podcast;

    public function __construct(Podcast $podcast)
    {
        $this->podcast = $podcast;
    }

    public function handle(AudioProcessor $processor)
    {
        // Process uploaded podcast...
    }
}

Veja que essa classe implementa a interface ShouldQueue. No Dispacher de jobs tem um método que recebe o nome da classe pra decidir se ela deve entrar na fila ou não, e esse método usa reflexão para verificar se a classe implementa a interface ShouldQueue:

protected function handlerShouldBeQueued($class)
{
    try {
        return (new ReflectionClass($class))->implementsInterface(
            ShouldQueue::class
        );
    } catch (Exception $e) {
        return false;
    }
}

Nesse contexto específico é melhor usar reflexão que futilmente gerar uma instância da classe para depois verificar se o objeto gerado implementa a requerida interface. Se fôssemos implementar instanciando a classe, ficaria mais ou menos assim:

protected function handlerShouldBeQueued($class)
{
    $object = new $class();

    return $object instanceof ShouldQueue;
}

(E note que nesse protótipo nem nos preocupamos com as possíveis dependências que o construtor de $class poderia precisar receber).

Anotações

Você já deve ter visto libraries/componentes que estendem seus comportamentos a partir de anotações. Uma delas é a Annotations, para Laravel, que permite definir comportamentos diretamente por anotações nas classes, por exemplo:


/**
 * @Middleware("guest", except={"logout"}, prefix="/your/prefix")
 */
class AuthController extends Controller
{
    /**
     * @Get("logout", as="logout")
     * @Middleware("auth")
     */
    public function logout()
    {
        $this->auth->logout();

        return redirect(route('login'));
    }
}

E usa-se reflexão para obter essas anotações.

Palavras finais

É bem provável que no dia a dia de desenvolvimento no código de aplicação não usemos reflexão, mas são uma importante ferramenta, principalmente para libraries e frameworks.

Até a próxima!

Desenvolvedor PHP
Formação Desenvolvedor PHP
Conhecer a formação

Autor(a) do artigo

Kennedy Tedesco
Kennedy Tedesco

Head de desenvolvimento. Vasta experiência em desenvolvimento Web com foco em PHP. Graduado em Sistemas de Informação. Pós-graduando em Arquitetura de Software Distribuído pela PUC Minas. Zend Certified Engineer (ZCE) e Coffee Addicted Person (CAP). @KennedyTedesco

Todos os artigos

Artigos relacionados Ver todos