Container de injeção de dependência (DI Container)

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.

Injetar dependências pode se tornar uma tarefa tediosa quando se têm muitas classes envolvidas. Antes de injetar uma dependência ela precisa ser instanciada. Portanto, não cuidamos apenas da “injeção”, precisamos também ter o conhecimento de quais objetos ela precisa para funcionar.

Um container de injeção de dependência (DI Container) gerencia e automatiza as instanciações. Dizemos pra ele como um objeto deve ser criado (essa é a parte que nos toca, o nosso conhecimento sobre ele) e então sempre que o precisarmos, basta que usemos o container para obtê-lo.

Esse artigo utilizará PHP como linguagem base para os exemplos, no entanto, há de se destacar, o conceito é agnóstico à linguagem. Se PHP não é a sua “praia”, não tem problema, você pode pesquisar por “dependency injection container C#” ou por qualquer outra linguagem que encontrará importantes referências e implementações.

PHP Básico
Curso de PHP Básico
CONHEÇA O CURSO

No cenário dos frameworks PHP, os mais utilizados pelo mercado (Symfony, Laravel etc) implementam, cada um, o seu próprio container e, assim o fazem, pois seria impraticável manter os objetos “conversando” pelo detrimento da enorme quantidade de instanciações repetidas que precisariam ser feitas no ciclo de uma simples requisição. Esses frameworks possuem centenas de classes e não ter por onde resolver as dependências e reutilizá-las sob demanda, é impensável.

Um container nada mais é do que um “mapa” das dependências que o projeto usa, em termos práticos, é uma classe que armazena o conhecimento sobre seus objetos e suas dependências.

Uma implementação genérica de um container (para que possamos assimilar melhor):

<?php
declare(strict_types=1);

use Closure;

final class Container
{
    private $instances = [];

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

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

O método set() armazena no array $instances a identificação/nome de uma dependência e a lógica por trás da sua instanciação.

Por exemplo:

$container = new Container();

$container->set('db', function() {
    return new DatabaseAdapter('mysql:dbname=test;host=127.0.0.1', 'root', '');
});

O método get() é utilizado para resolver e retornar a instância do objeto. Observe que a instanciação não se dá no set() e sim sob demanda, na hora que precisamos daquele objeto, ou seja, na hora que usamos get().

Observe essa linha:

return $this->instances[$id]($this);

Está executando a função anônima que definimos (a que resolve a dependência) e está passando para ela como único parâmetro a instância da classe Container ($this dentro daquele contexto refere-se à instância da classe em operação). Quando a função anônima é executada temos como retorno um novo objeto.

Lembra o que a nossa função anônima retorna?

$container->set('db', function() {
    return new DatabaseAdapter('mysql:dbname=test;host=127.0.0.1', 'root', '');
});

Pois bem, saindo um pouco dessas nuances relacionadas à implementação, na prática temos:

<?php

$container = new Container();

$container->set('db', function() {
    return new DatabaseAdapter('mysql:dbname=test;host=127.0.0.1', 'root', '');
});

// Imprime a instância de um objeto do tipo 'DatabaseAdapter'
var_dump($container->get('db'));

Vamos aumentar o nosso leque de objetos e suas dependências?

<?php

// Container
$container = new Container();

// Objeto que recupera configurações salvas em algum tipo de arquivo.
$container->set('config', function() {
    return new Config();
});

// Veja que agora *db* têm como dependência um objeto da classe Config
// e o utiliza para obter os dados de acesso ao BD.
$container->set('db', function($container) {
    $config = $container->get('config')->getConfig('db');

    return new DatabaseAdapter($config['dsn'], $config['user'], $config['password']);
});

// A classe UserRepository precisa de uma instância de *db*
// para fazer consultas ao banco de dados.
$container->set('user.repository', function($container) {
    return new UserRepository($container->get('db'));
});

Veja que temos uma cadeia de objetos interdependentes. Imagine agora a situação de termos três diferentes controladores, sendo instanciados em momentos diferentes no ciclo de execução da aplicação e todos eles necessitando da instância de UserRepository para recuperar informações sobre um usuário?

Com o container definido tudo o que teríamos que fazer:

// ... 

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

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

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

Observe que estamos passando para o construtor dos controladores uma instância de user.repository. O container lidará de nos retornar o objeto que queremos injetar a partir dessa identificação.

Um container pode implementar ainda mais comportamentos. Por exemplo, você deve ter percebido que a execução de $container->get('user.repository') vai sempre instanciar um novo objeto. Se executarmos 100 vezes, serão 100 novos objetos criados.

No entanto, algumas dependências são definitivas o suficiente para que não haja a necessidade de sempre instanciarmos um novo objeto delas. Nesses casos podemos ter um novo método no container para definir uma dependência compartilhada (singleton), onde uma única instância é gerada e retornada durante todo o ciclo de execução da aplicação.

O objetivo primário desse artigo não é se preocupar tanto com a implementação, mas com o conceito. No entanto, é importante que desenvolvamos alguns “protótipos” para uma melhor assimilação.

Vejamos então a implementação do nosso container com o novo método singleton():

<?php
declare(strict_types=1);

use Closure;

final class Container
{
    private $instances = [];

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

    public function get($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;
        };
    }
}

Nesse método a lógica de resolução tem uma camada a mais, nela verificamos:

  1. Essa dependência já foi resolvida (devidamente instanciada) anteriormente?
    1.1. Não? Então assim o faremos.
    1.2. Já foi? Então vamos retorná-la do “cache” da variável estática (tipo de variável que não perde o valor mesmo quando o nível de execução do programa deixa o escopo).

Voltando ao contexto do nosso exemplo das classes controladoras que recebem a injeção do objeto UserRepository, podemos agora otimizar a resolução dessa dependência usando o método singleton() ao invés do set():

$container->singleton('user.repository', function($container) {
    return new UserRepository($container->get('db'));
});

Agora, na injeção dessa dependência nas classes dos controladores teremos sempre o mesmo objeto sendo compartilhado entre as diferentes instâncias:

// ... 

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

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

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

É comum referências a DI Container (Dependency Injection Container) ou a IoC Container (Inversion of Control Container). São a mesma coisa: containers de injeção de dependência. A diferença é que um IoC Container precisa conseguir (inclusive) resolver as dependências a partir do mapeamento de interfaces (ele as resolve a partir de abstrações em detrimento às implementações concretas). É possível programar um IoC Container para resolver determinado objeto se uma determinada interface for requerida.

O container do Laravel Framework é um bom caso de uso. Ele é bastante encorpado e consegue resolver de diferentes formas. Ele é considerado um IoC Container, mas nada de errado se o referirmos como sendo um DI Container.

Veja esse trecho do core do Laravel Framework:

// The database manager is used to resolve various connections, since multiple
// connections might be managed. It also implements the connection resolver
// interface which may be used by other components requiring connections.
$this->app->singleton('db', function ($app) {
    return new DatabaseManager($app, $app['db.factory']);
});

Ele possui diversos services providers que configuram dezenas de dependências. E o container é compartilhado e utilizado por quase todas as classes do Framework.

(Para visualizar o Código-fonte do IoC Container do Laravel, clique aqui).

Laravel 5.1 - Framework PHP
Curso de Laravel 5.1 - Framework PHP
CONHEÇA O CURSO

Outras importantes implementações de containers para PHP:

Recomendação de leitura:

Novo artigo da série, sobre resolução automática de dependências. Nele , vamos incrementar o container criado aqui nesse artigo. Portanto, recomendo a leitura:

Até a próxima!

PHP Avançado
Curso de PHP Avançado
CONHEÇA O CURSO
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