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.
Curso PHP - Fundamentos
Conhecer o cursoUm 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.
Curso PHP Avançado
Conhecer o cursoUm 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!