Autowiring em Container de Injeção de Dependência

No artigo Entendendo Injeção de Dependência vimos sobre o que é injeção de dependência, seu funcionamento e como se dá a sua aplicação. No artigo Container De Injeção De Dependência (DI Container) vimos como funciona um container para gerenciar o mapa de dependências.

Nesse artigo veremos uma funcionalidade relativamente comum nos containers de dependência de frameworks que é a habilidade de resolver as dependências de um construtor (principalmente) automaticamente, chamada de autowiring.

No artigo Container De Injeção De Dependência (DI Container) implementamos um protótipo (simples, porém funcional) de um container e, em determinado momento, chegamos nesse tipo de caso de uso:

$indexController = new IndexController(
    $container->get('user.repository')
);

$userController = new UserController(
    $container->get('user.repository')
);

$registerController = new RegisterController(
    $container->get('user.repository')
);

Alguns controladores precisavam receber a instância de UserRepository e tivemos que manualmente passá-la para cada um deles. Agora, imagine se cada um desses controladores tivesse que receber outras dependências? Nenhum problema, só que ficaria meio chato e improdutivo ter que ficar repetindo essas construções na instanciação deles, concorda?

E se desenvolvermos uma forma do nosso container automaticamente resolver essas dependências? Desse modo, tudo o que faríamos seria pedir para o Container uma instância desses controladores, sem a necessidade de nos preocuparmos em passar manualmente para o construtor de cada um deles as suas dependências. Teríamos como resultado um código assim:

$indexController = $container->get(IndexController::class);

$userController = $container->get(UserController::class);

$registerController = $container->get(RegisterController::class);

Estamos pedindo as instâncias desses controladores sem nos preocuparmos em alimentá-los de suas dependências, deixamos esse trabalho para o Container.

Symfony - Fundamentos
Curso de Symfony - Fundamentos
CONHEÇA O CURSO

Estrutura inicial do projeto

Vamos criar o protótipo de uma aplicação para que possamos testar a nossa implementação de Container. A nossa aplicação terá essa estrutura:

- [blog-artigo-di]
- - - [app]
- - - - [Http]
- - - - - - [Controller]
- - - - - - - UserController.php
- - - - [Repositories]
- - - - - LeadRepository.php
- - - - - TagRepository.php
- - - - - UserRepository.php
- - - Container.php
- index.php

O repositório dessa estrutura no GitHub você encontra clicando aqui. Se desejar, você pode fazer download direto dela clicando nesse link: sem-autowiring.zip

Depois de baixar e colocar no local onde normalmente você executa seus projetos, basta que você execute composer install pois o projeto faz uso da PSR-4 para autoloading:

$ ~/D/w/blog-artigo-di> composer install

O nosso Container atualmente possui uma implementação simples e que ainda não suporta a resolução automática de dependências. Abra app/Container.php:

<?php
declare(strict_types=1);

namespace App;

use Closure;

final class Container
{
    private $instances = [];

    public function set(string $id, Closure $closure) : void
    {
        $this->instances[$id] = $closure;
    }

    public function get(string $id) : object
    {
        return $this->instances[$id]($this);
    }

    public function singleton(string $id, Closure $closure) : void
    {
        $this->instances[$id] = function () use ($closure) {
            static $resolvedInstance;

            if (null !== $resolvedInstance) {
                $resolvedInstance = $closure($this);
            }

            return $resolvedInstance;
        };
    }
}

Esse container já foi explicado na indicação de leitura no início desse artigo. Portanto, não vamos entrar novamente em seus pormenores.

O arquivo index.php na raiz do projeto é o responsável pelo bootstrap da nossa aplicação protótipo:

<?php

// Carrega o autoload do Composer
require './vendor/autoload.php';

use App\Repositories\TagRepository;
use App\Repositories\LeadRepository;
use App\Repositories\UserRepository;
use App\Http\Controller\UserController;

// Instancia o container
$container = new App\Container();

// Adiciona referências ao container
$container->set(TagRepository::class, function() {
    return new TagRepository();
});

$container->set(LeadRepository::class, function() {
    return new LeadRepository();
});

$container->set(UserRepository::class, function() {
    return new UserRepository();
});

// Instancia o UserController passando para ele as dependências necessárias
$userController = new UserController(
    $container->get(TagRepository::class),
    $container->get(UserRepository::class),
    $container->get(LeadRepository::class)
);

$userController->index();

Para testar o exemplo, na raiz do projeto execute:

$ ~/D/w/blog-artigo-di> php -S localhost:8000

Isso executará o servidor embutido do PHP em cima da raiz do nosso projeto, permitindo que o acessemos pelo navegador através da URL: http://localhost:8000/

O resultado será:

/Users/kennedytedesco/Documents/www/blog-artigo-di/app/Http/Controller/UserController.php:31:
array (size=2)
  'id' => int 1
  'name' => string 'tag' (length=3)

/Users/kennedytedesco/Documents/www/blog-artigo-di/app/Http/Controller/UserController.php:31:
array (size=2)
  'id' => int 2
  'name' => string 'user' (length=4)

/Users/kennedytedesco/Documents/www/blog-artigo-di/app/Http/Controller/UserController.php:31:
array (size=2)
  'id' => int 3
  'name' => string 'lead' (length=4)

Indica que o método index() do UserController foi executado com sucesso. Ou seja, as dependências, os repositórios necessários para o funcionamento dessa classe foram injetados com sucesso. Até aqui tudo bem, ademais, injetamos tais dependências manualmente:

$userController = new UserController(
    $container->get(TagRepository::class),
    $container->get(UserRepository::class),
    $container->get(LeadRepository::class)
);

Implementando a resolução automática de dependências

Para que possamos implementar a resolução automática das dependências utilizaremos a API de reflexão do PHP. Essa API nos fornece meios para que façamos engenharia reversa nas classes, extraindo muitas de suas informações internas como quantos métodos possui, quais são públicos, protegidos ou privados, se implementa um construtor, quais são os parâmetros do construtor, se são opcionais ou exigidos, entre outras informações.

Dessa forma, se o usuário pedir para o Container a instanciação do UserController:

$userController = $container->get(UserController::class);

Que tal a gente usar reflexão para obter quais dependências (classes) ele necessita para ser instanciado e então resolver essas dependências automaticamente usando o próprio Container e retornar a instância dele?

É exatamente isso que a nossa implementação de autowiring fará. Para tanto, começaremos alterando o código do nosso container app\Container.php para:

<?php
declare(strict_types=1);

namespace App;

use Closure;
use ReflectionClass;
use ReflectionParameter;

final class Container
{
    private $instances = [];

    public function set(string $id, Closure $closure) : void
    {
        $this->instances[$id] = $closure;
    }

    public function get(string $id) : object
    {
        // Se essa referência existe no mapa do container, então a retorna diretamente.
        if ($this->has($id)) {
            return $this->instances[$id]($this);
        }

        // Se a referência não existe no container, então foi passado uma classe para ser instanciada
        // Façamos então a reflexão dela para obter os parâmetros do método construtor
        $reflector = new ReflectionClass($id);
        $constructor = $reflector->getConstructor();

        // Se a classe não implementa um método construtor, então vamos apenas retornar uma instância dela.
        if (null === $constructor) {
            return new $id();
        }

        // Itera sobre os parâmetros do construtor para realizar a resolução das dependências que ele exige.
        // O método "newInstanceArgs()" cria uma nova instância da classe usando os novos argumentos passados.
        // Usamos "array_map()" para iterar os parâmetros atuais, resolvê-los junto ao container e retornar um array das instâncias já resolvidas pelo container.
        return $reflector->newInstanceArgs(array_map(
            function (ReflectionParameter $dependency) {
                // Busca no container a referência da classe desse parâmetro
                return $this->get(
                    $dependency->getClass()->getName()
                );
            },
            $constructor->getParameters()
        ));
    }

    public function singleton(string $id, Closure $closure) : void
    {
        $this->instances[$id] = function () use ($closure) {
            static $resolvedInstance;

            if (null !== $resolvedInstance) {
                $resolvedInstance = $closure($this);
            }

            return $resolvedInstance;
        };
    }

    public function has(string $id) : bool
    {
        return isset($this->instances[$id]);
    }
}

(Através do GitHub você pode visualizar o que foi adicionado/removido. Basta visualizar esse commit aqui).

A partir do momento que o nosso Container consegue resolver as dependências automaticamente, podemos alterar no index.php a forma de instanciar, que era uma instanciação direta da classe usando new UserController:

$userController = new UserController(
    $container->get(TagRepository::class),
    $container->get(UserRepository::class),
    $container->get(LeadRepository::class)
);

Para uma instanciação que usa o Container (para que ele possa resolver as dependências para nós):

$userController = $container->get(UserController::class);
$userController->index();

(O código completo do exemplo com autowiring encontra-se no branch master do repositório desse exemplo).

Você pode testar o exemplo e terá o mesmo resultado obtido anteriormente quando não usávamos autowiring. O UserController continuará sendo instanciado com sucesso.

Laravel - Framework PHP (Parte 3/3)
Curso de Laravel - Framework PHP (Parte 3/3)
CONHEÇA O CURSO

Concluindo

Vimos nesse artigo a fundação sobre como a resolução automática de dependências é feita nos containers de injeção de dependência. É por esse caminho que Symfony, Laravel entre outros frameworks (inclusive de outras linguagens) fazem. Obviamente o nosso Container é simples, didático e bem direto ao ponto, não estando 100% pronto para uso em projetos reais. Algumas verificações de segurança (se a classe existe, senão lançar uma exceção etc) precisariam ser implementadas. Na realidade, existem boas implementações de containers por aí e, se você usa um Web Framework, não vai precisar criar a sua própria. No entanto, saber como funciona, é essencial. Essa foi a intenção desse artigo.

Até a próxima!

Deixe seu comentário

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