Container de Injeção de Depedências no PHP

Escrito por Lucas Scemist em 03/01/2025
PHP Programação
Imagem Ilustrativa de Código.

Este artigo descreve como criar um container DI básico em PHP para resolver automaticamente dependências.

A injeção de dependências (Dependency Injection, DI) é uma técnica amplamente utilizada para promover baixo acoplamento e aumentar a flexibilidade e testabilidade do código. Um container de injeção de dependências ajuda a gerenciar e resolver dependências automaticamente, simplificando a construção de objetos complexos.

O que é Reflection no PHP?

Reflection é uma funcionalidade nativa do PHP que permite inspecionar classes, métodos, propriedades e parâmetros em tempo de execução. Este conceito não faz parte da Injeção de Dependências, mas é a ferramenta do PHP que nos possíbilita essa implantação. Com ela, podemos inspecionar classes, objetos e parâmetros e determinar quais objetos precisam ser instanciados com base nos tipos definidos nos parâmetros dos métodos ou construtores.

Por exemplo, o ReflectionClass pode ser usado para analisar uma classe, enquanto ReflectionMethod e ReflectionParameter permitem inspecionar métodos e seus argumentos.

Criando um Container DI

Criamos uma interface que especificará que qualquer resolver precisará ter um método chamado resolve.

<?php

class Container
{
    public function resolve(string $class)
    {
        $reflectionClass = new ReflectionClass($class);
        $this->ensureClassIsInstantiable($reflectionClass);
        $constructor = $reflectionClass->getConstructor();

        if (!$constructor)
            return $this->instantiateClassWithoutConstructor($reflectionClass);

        return $this->instantiateClassWithConstructor($reflectionClass, $constructor);
    }

    private function ensureClassIsInstantiable(ReflectionClass $reflectionClass)
    {
        if (!$reflectionClass->isInstantiable())
            throw new Exception("Class {$reflectionClass->getName()} is not instantiable.");
    }

    private function instantiateClassWithoutConstructor(ReflectionClass $reflectionClass)
    {
        return $reflectionClass->newInstance();
    }

    private function instantiateClassWithConstructor(
        ReflectionClass $reflectionClass,
        ReflectionMethod $constructor
    ) {
        $parameters = $constructor->getParameters();
        $dependencies = $this->resolveDependencies($parameters);

        return $reflectionClass->newInstanceArgs($dependencies);
    }

    private function resolveDependencies(array $parameters): array
    {
        return array_map(
            fn($parameter) => $this->resolveDependency($parameter),
            $parameters
        );
    }

    private function resolveDependency(ReflectionParameter $parameter)
    {
        $this->ensureParameterIsInstantiable($parameter);

        return $this->resolve($parameter->getType()->getName());
    }

    private function ensureParameterIsInstantiable(ReflectionParameter $parameter)
    {
        $type = $parameter->getType();

        if (!$type || $type->isBuiltin())
            throw new Exception("Cannot resolve param {$parameter->getName()} of type {$type}.");
    }
}

Utilização Prática

Aqui nós queremos instanciar um usuário, que tem como dependencia o banco de dados, que por sua vez, tem uma dependencia de um driver.

class MySQLDriver {}

class Database
{
    public function __construct(private MySQLDriver $mySqlDriver) {}
}

class Usuario
{
    public function __construct(private Database $database)
    {
        echo 'Usuário instanciado com dependência de Database.';
    }
}

Que com o container, podemos utilizar o container para resolver a dependência do banco de dados sem nunca ter que ter instanciado manualmente

$container = new Container();
$usuario = $container->resolve(Usuario::class);
var_dump($usuario);

# Usuário instanciado com dependência de Database.
#
# object(Usuario)#5 (1) {
#     ["database":"Usuario":private]=>
#         object(Database)#9 (1) {
#             ["mySqlDriver":"Database":private]=>
#                 object(MySQLDriver)#11 (0) {
#                 }
#         }
# }

Conclusão

Este é um exemplo básico que serve para ilustrar o conceito e funcionamento de um container de injeção de dependências. Antes de ser utilizado em produção, ele precisará de muitas melhorias, como suporte a interfaces, ciclos de dependência e resolução de tipos nativos. É importante que os parâmetros dos construtores estejam estritamente tipados com classes ou interfaces para que o container consiga encontrar as dependências automaticamente. Vale lembrar que tipos nativos, como int ou string, não são resolvidos automaticamente e exigiriam mais algumas implantações.