Bem-vindo à aula sobre Reflexão no PHP. A reflexão é uma capacidade poderosa que permite ao programa examinar a si mesmo, ou seja, inspecionar classes, interfaces, métodos, propriedades e até mesmo parâmetros de funções em tempo de execução. Isso abre um leque de possibilidades para criar código flexível e dinâmico, como frameworks de injeção de dependência, geradores de documentação, testadores automáticos e muito mais.

Nesta aula, vamos nos aprofundar na classe ReflectionClass, que é a porta de entrada para a reflexão de classes. Veremos como inspecionar classes, obter informações sobre métodos e propriedades, e como aplicar isso em cenários reais. Também discutiremos os custos de desempenho envolvidos, pois a reflexão não é gratuita e deve ser usada com sabedoria.

ReflectionClass

A classe ReflectionClass é uma das classes centrais da API de Reflexão do PHP. Ela fornece métodos para obter informações sobre uma classe, como seu nome, modificadores (final, abstract, etc.), métodos, propriedades, constantes, e até mesmo a classe pai e interfaces implementadas. Para criar uma instância de ReflectionClass, você passa o nome da classe (ou um objeto) como argumento.

Por exemplo, se você tem uma classe MinhaClasse, pode criar um objeto ReflectionClass assim:

$reflection = new ReflectionClass('MinhaClasse');

Ou, se já tem uma instância da classe, pode fazer:

$objeto = new MinhaClasse();
$reflection = new ReflectionClass($objeto);

Uma vez que você tem o objeto de reflexão, pode chamar métodos como getName() para obter o nome completo da classe (incluindo namespace), isFinal() para verificar se a classe é final, isAbstract() para verificar se é abstrata, e assim por diante. Vamos ver um exemplo completo:

namespace App;

class Exemplo {
    private $atributo;
    public function metodo() {}
}

$reflection = new \ReflectionClass(Exemplo::class);
echo $reflection->getName(); // App\Exemplo
echo $reflection->isFinal() ? 'final' : 'não final'; // não final
echo $reflection->isAbstract() ? 'abstract' : 'não abstract'; // não abstract

Além disso, ReflectionClass pode ser usado para instanciar uma classe sem conhecer seu nome em tempo de compilação, usando o método newInstance(). Isso é útil em padrões como fábricas dinâmicas.

Inspecionando classes

Inspecionar classes envolve obter informações sobre seus membros: métodos, propriedades, constantes e também a estrutura da própria classe (herança, interfaces, traits). A API de reflexão oferece métodos específicos para cada tipo de membro.

Para obter todos os métodos de uma classe, use getMethods() que retorna um array de objetos ReflectionMethod. Cada objeto ReflectionMethod pode fornecer detalhes como nome, visibilidade (public, protected, private), se é estático, se é abstrato, os parâmetros, e até mesmo o código-fonte (se a extensão tokenizer estiver habilitada). Exemplo:

class MinhaClasse {
    public function metodoPublico() {}
    protected function metodoProtegido() {}
    private function metodoPrivado() {}
}

$reflection = new ReflectionClass('MinhaClasse');
$metodos = $reflection->getMethods();
foreach ($metodos as $metodo) {
    echo $metodo->getName() . ' - ' . $metodo->isPublic() ? 'public' : 'não public';
}

Da mesma forma, getProperties() retorna um array de ReflectionProperty, e getConstants() retorna um array associativo de constantes definidas na classe. Você também pode obter a classe pai com getParentClass(), as interfaces com getInterfaces() e os traits com getTraits().

Um recurso interessante é a capacidade de inspecionar parâmetros de métodos. Usando ReflectionMethod::getParameters(), você obtém um array de objetos ReflectionParameter, que contém informações como nome, tipo, se é opcional, se tem valor padrão, etc. Isso é muito usado em frameworks para fazer injeção de dependência automática.

Vamos ver um exemplo mais completo que inspeciona uma classe e imprime todas as informações relevantes:

class Exemplo {
    const CONSTANTE = 'valor';
    public $prop1;
    private $prop2;
    public function __construct($arg1, $arg2 = 'padrao') {}
    public function metodo() {}
}

$reflection = new ReflectionClass('Exemplo');
echo 'Constantes: ';
print_r($reflection->getConstants());
echo 'Propriedades:\n';
foreach ($reflection->getProperties() as $prop) {
    echo $prop->getName() . ' - ' . $prop->isPublic() ? 'public' : 'private' . "\n";
}
echo 'Métodos:\n';
foreach ($reflection->getMethods() as $metodo) {
    echo $metodo->getName() . "\n";
    foreach ($metodo->getParameters() as $param) {
        echo '  Parâmetro: $' . $param->getName() . ' - tipo: ' . $param->getType() . "\n";
    }
}

Essas informações são extremamente valiosas para criar ferramentas que precisam entender a estrutura de classes sem conhecimento prévio.

Casos de uso

A reflexão é usada em muitos cenários práticos no desenvolvimento PHP. Aqui estão alguns dos mais comuns:

  • Injeção de dependência automática: Frameworks como Laravel e Symfony usam reflexão para resolver dependências de classes. Ao instanciar um objeto, o contêiner de injeção de dependência inspeciona o construtor da classe, identifica os tipos dos parâmetros e instancia automaticamente as dependências necessárias.
  • Proxies e proxies dinâmicos: Em ORMs (Object-Relational Mappers) como Doctrine, a reflexão é usada para criar proxies de entidades, permitindo carregamento preguiçoso (lazy loading) de propriedades.
  • Serialização e deserialização: Ferramentas que convertem objetos em JSON ou XML podem usar reflexão para percorrer todas as propriedades, independentemente da visibilidade, e serializá-las.
  • Geradores de documentação: Ferramentas como phpDocumentor usam reflexão para extrair informações de classes e gerar documentação automaticamente.
  • Testes unitários: Frameworks de teste como PHPUnit usam reflexão para acessar métodos privados e testá-los, ou para criar mocks de classes.

Vamos ver um exemplo prático de um contêiner de injeção de dependência simples usando reflexão:

class Container {
    private $instancias = [];

    public function get($classe) {
        if (isset($this->instancias[$classe])) {
            return $this->instancias[$classe];
        }

        $reflection = new ReflectionClass($classe);
        $construtor = $reflection->getConstructor();

        if ($construtor === null) {
            $objeto = new $classe();
        } else {
            $parametros = $construtor->getParameters();
            $dependencias = [];
            foreach ($parametros as $parametro) {
                $tipo = $parametro->getType();
                if ($tipo instanceof ReflectionNamedType && !$tipo->isBuiltin()) {
                    $dependencias[] = $this->get($tipo->getName());
                } else {
                    $dependencias[] = $parametro->getDefaultValue();
                }
            }
            $objeto = $reflection->newInstanceArgs($dependencias);
        }

        $this->instancias[$classe] = $objeto;
        return $objeto;
    }
}

class Servico {
    public function executar() {
        echo "Serviço executado!\n";
    }
}

class Cliente {
    private $servico;
    public function __construct(Servico $servico) {
        $this->servico = $servico;
    }
    public function acao() {
        $this->servico->executar();
    }
}

$container = new Container();
$cliente = $container->get(Cliente::class);
$cliente->acao(); // Serviço executado!

Nesse exemplo, o contêiner usa ReflectionClass para descobrir as dependências do construtor e instanciá-las automaticamente. Isso demonstra como a reflexão pode ser usada para criar código altamente flexível.

Custos

Embora a reflexão seja extremamente útil, ela tem um custo de desempenho significativo. O processo de reflexão em PHP é mais lento do que acessar diretamente métodos e propriedades, porque envolve muita instrospecção e criação de objetos temporários. Em aplicações de alto tráfego, o uso excessivo de reflexão pode degradar o desempenho.

Por exemplo, chamar ReflectionClass::getMethods() em uma classe com muitos métodos pode ser relativamente caro. Além disso, o cache de opcode (como o OPcache) não otimiza bem o código de reflexão, porque ele é dinâmico.

Portanto, é importante usar reflexão com moderação e, quando possível, armazenar em cache os resultados da reflexão. Muitos frameworks fazem cache de metadados de classes após a primeira análise. Por exemplo, o Symfony usa cache de metadados para evitar repetir a reflexão a cada requisição.

Outra consideração é que a reflexão pode quebrar o encapsulamento, permitindo acessar métodos e propriedades privadas. Isso pode ser útil em testes, mas também pode levar a código frágil se usado indevidamente. Portanto, é recomendável limitar o uso de reflexão a bibliotecas e frameworks, e não em código de aplicação comum.

Para medição de desempenho, você pode usar o microtime para comparar o tempo de acesso direto versus reflexão:

class Teste {
    public function metodo() {}
}

$obj = new Teste();
$inicio = microtime(true);
for ($i = 0; $i < 10000; $i++) {
    $obj->metodo();
}
$fim = microtime(true);
echo "Acesso direto: " . ($fim - $inicio) . " s\n";

$reflection = new ReflectionClass(Teste::class);
$metodo = $reflection->getMethod('metodo');
$inicio = microtime(true);
for ($i = 0; $i < 10000; $i++) {
    $metodo->invoke($obj);
}
$fim = microtime(true);
echo "Reflexão: " . ($fim - $inicio) . " s\n";

Na prática, a diferença pode ser de várias vezes, dependendo da complexidade. Portanto, use reflexão apenas quando necessário e sempre que possível, cacheie os resultados.

Boas práticas e observações finais

Ao trabalhar com reflexão, considere estas boas práticas:

  • Cacheie os resultados: Se você precisa refletir sobre a mesma classe repetidamente, armazene os resultados em uma variável estática ou em um cache (como APCu).
  • Prefira APIs específicas: Em vez de usar reflexão para acessar propriedades privadas, veja se não há uma alternativa melhor, como métodos públicos ou interfaces.
  • Use somente quando necessário: Para código de aplicação comum, evite reflexão; ela é mais adequada para bibliotecas e frameworks.
  • Esteja ciente da segurança: A reflexão pode permitir acesso a membros privados, o que pode violar a encapsulação. Use com cuidado e somente em contextos confiáveis.

Em resumo, a reflexão é uma ferramenta poderosa que todo desenvolvedor PHP deve conhecer, mas que deve ser usada com responsabilidade. Com a prática, você saberá quando e como aplicá-la de forma eficiente.

Referências

Exercícios

  1. Exercício 1: Crie uma classe chamada Carro com propriedades privadas marca e modelo, e um construtor que recebe esses valores. Use ReflectionClass para obter o nome da classe, as propriedades e os métodos.
  2. ✓ Resposta: Você deve criar a classe e depois usar ReflectionClass para inspecioná-la. Por exemplo:
    class Carro {
        private $marca;
        private $modelo;
        public function __construct($marca, $modelo) {
            $this->marca = $marca;
            $this->modelo = $modelo;
        }
    }
    
    $reflection = new ReflectionClass('Carro');
    echo $reflection->getName(); // Carro
    print_r($reflection->getProperties());
    print_r($reflection->getMethods());
    
  3. Exercício 2: Escreva uma função que, dado o nome de uma classe, retorne um array associativo com os nomes dos métodos públicos e seus parâmetros (nome e tipo).
  4. ✓ Resposta:
    function getPublicMethodsWithParams($classe) {
        $reflection = new ReflectionClass($classe);
        $metodosPublicos = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
        $resultado = [];
        foreach ($metodosPublicos as $metodo) {
            $params = [];
            foreach ($metodo->getParameters() as $param) {
                $tipo = $param->getType();
                $params[] = [
                    'nome' => $param->getName(),
                    'tipo' => $tipo ? $tipo->getName() : null
                ];
            }
            $resultado[$metodo->getName()] = $params;
        }
        return $resultado;
    }
    
  5. Exercício 3: Implemente um método invokePrivate que usa reflexão para chamar um método privado de um objeto, dado o nome do método e um array de argumentos.
  6. ✓ Resposta:
    function invokePrivate($objeto, $nomeMetodo, $args = []) {
        $reflection = new ReflectionMethod($objeto, $nomeMetodo);
        $reflection->setAccessible(true);
        return $reflection->invokeArgs($objeto, $args);
    }
    
    // Exemplo de uso:
    class Exemplo {
        private function privado($a, $b) {
            return $a + $b;
        }
    }
    
    $obj = new Exemplo();
    echo invokePrivate($obj, 'privado', [2, 3]); // 5
    
  7. Exercício 4: Crie um contêiner de injeção de dependência simples que use reflexão para instanciar classes com dependências automáticas (como no exemplo da aula). Teste com duas classes: Repositorio e Servico, onde Servico depende de Repositorio.
  8. ✓ Resposta:
    class Container {
        private $instancias = [];
        public function get($classe) {
            if (isset($this->instancias[$classe])) return $this->instancias[$classe];
            $reflection = new ReflectionClass($classe);
            $construtor = $reflection->getConstructor();
            if (!$construtor) {
                $obj = new $classe();
            } else {
                $params = [];
                foreach ($construtor->getParameters() as $param) {
                    $tipo = $param->getType();
                    if ($tipo && !$tipo->isBuiltin()) {
                        $params[] = $this->get($tipo->getName());
                    } else {
                        $params[] = $param->getDefaultValue();
                    }
                }
                $obj = $reflection->newInstanceArgs($params);
            }
            $this->instancias[$classe] = $obj;
            return $obj;
        }
    }
    
    class Repositorio {
        public function salvar() { echo "Salvo!\n"; }
    }
    class Servico {
        private $repositorio;
        public function __construct(Repositorio $repositorio) {
            $this->repositorio = $repositorio;
        }
        public function executar() { $this->repositorio->salvar(); }
    }
    
    $container = new Container();
    $servico = $container->get(Servico::class);
    $servico->executar(); // Salvo!
    
  9. Exercício 5: Escreva um script que use reflexão para listar todos os métodos de uma classe que tenham anotações (docblocks) que contenham a palavra "@deprecated". Dica: use getDocComment().
  10. ✓ Resposta:
    function listarDeprecated($classe) {
        $reflection = new ReflectionClass($classe);
        $deprecated = [];
        foreach ($reflection->getMethods() as $metodo) {
            $doc = $metodo->getDocComment();
            if ($doc && strpos($doc, '@deprecated') !== false) {
                $deprecated[] = $metodo->getName();
            }
        }
        return $deprecated;
    }
    
    // Exemplo:
    class Teste {
        /**
         * @deprecated use novoMetodo()
         */
        public function velho() {}
        public function novo() {}
    }
    print_r(listarDeprecated(Teste::class)); // Array ( [0] => velho )