Container de Injeção de Dependência

Um container de injeção de dependência (DI) é um objeto que sabe como instanciar e configurar objetos e todas as suas dependências. O artigo do Martin explica bem porque o container de DI é útil. Aqui vamos explicar principalmente a utilização do container de DI fornecido pelo Yii.

Injeção de Dependência

O Yii fornece o recurso container de DI através da classe yii\di\Container. Ela suporta os seguintes tipos de injeção de dependência:

  • Injeção de Construtor;
  • Injeção de setter e propriedade;
  • Injeção de PHP callable.

Injeção de Construtor

O container de DI suporta injeção de construtor com o auxílio dos type hints identificados nos parâmetros dos construtores. Os type hints informam ao container quais classes ou interfaces são dependentes no momento da criação de um novo objeto. O container tentará pegar as instâncias das classes dependentes ou interfaces e depois injetá-las dentro do novo objeto através do construtor. Por exemplo:

class Foo
{
    public function __construct(Bar $bar)
    {
    }
}
$foo = $container->get('Foo');
// que equivale a:
$bar = new Bar;
$foo = new Foo($bar);

Injeção de Setter e Propriedade

A injeção de setter e propriedade é suportado através de configurações. Ao registrar uma dependência ou ao criar um novo objeto, você pode fornecer uma configuração que será utilizada pelo container para injetar as dependências através dos setters ou propriedades correspondentes. Por exemplo:

use yii\base\BaseObject;

class Foo extends BaseObject
{
    public $bar;

    private $_qux;

    public function getQux()
    {
        return $this->_qux;
    }

    public function setQux(Qux $qux)
    {
        $this->_qux = $qux;
    }
}

$container->get('Foo', [], [
    'bar' => $container->get('Bar'),
    'qux' => $container->get('Qux'),
]);

Informação: O método yii\di\Container::get() recebe em seu terceiro parâmetro um array de configuração que deve ser aplicado ao objecto a ser criado. Se a classe implementa a interface yii\base\Configurable (por exemplo, yii\base\BaseObject), o array de configuração será passado como o último parâmetro para o construtor da classe; caso contrário, a configuração será aplicada depois que o objeto for criado.

Injeção de PHP Callable

Neste caso, o container usará um PHP callable registrado para criar novas instâncias da classe. Cada vez que yii\di\Container::get() for chamado, o callable correspondente será invocado. O callable é responsável por resolver as dependências e injetá-las de forma adequada para os objetos recém-criados. Por exemplo:

$container->set('Foo', function ($container, $params, $config) {
    $foo = new Foo(new Bar);
    // ... Outras inicializações...
    return $foo;
});

$foo = $container->get('Foo');

Para ocultar a lógica complexa da construção de um novo objeto você pode usar um método estático de classe para retornar o PHP callable. Por exemplo:

class FooBuilder
{
    public static function build($container, $params, $config)
    {
        return function () {
            $foo = new Foo(new Bar);
            // ... Outras inicializações...
            return $foo;
       };        
    }
}

$container->set('Foo', FooBuilder::build());

$foo = $container->get('Foo');

Como você pode ver, o PHP callable é retornado pelo método FooBuilder::build(). Ao fazê-lo, quem precisar configurar a classe Foo não precisará saber como ele é construído.

Registrando Dependências

Você pode usar yii\di\Container::set() para registrar dependências. O registro requer um nome de dependência, bem como uma definição de dependência. Um nome de dependência pode ser um nome de classe, um nome de interface, ou um alias; e a definição de dependência pode ser um nome de classe, um array de configuração ou um PHP callable.

$container = new \yii\di\Container;

// registrar um nome de classe. Isso pode ser ignorado.
$container->set('yii\db\Connection');

// registrar uma interface
// Quando uma classe depende da interface, a classe correspondente
// será instanciada como o objeto dependente
$container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');

// registrar um alias. Você pode utilizar $container->get('foo')
// para criar uma instância de Connection
$container->set('foo', 'yii\db\Connection');

// registrar uma classe com configuração. A configuração
// será aplicada quando quando a classe for instanciada pelo get()
$container->set('yii\db\Connection', [
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// registrar um alias com a configuração de classe
// neste caso, um elemento "class" é requerido para especificar a classe
$container->set('db', [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// registrar um PHP callable
// O callable será executado sempre quando $container->get('db') for chamado
$container->set('db', function ($container, $params, $config) {
    return new \yii\db\Connection($config);
});

// registrar uma instância de componente
// $container->get('pageCache') retornará a mesma instância toda vez que for chamada
$container->set('pageCache', new FileCache);

Dica: Se um nome de dependência é o mesmo que a definição de dependência correspondente, você não precisa registrá-lo no container de DI.

Um registro de dependência através de set() irá gerar uma instância a cada vez que a dependência for necessária. Você pode usar yii\di\Container::setSingleton() para registrar a dependência de forma a gerar apenas uma única instância:

$container->setSingleton('yii\db\Connection', [
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

Resolvendo Dependências

Depois de registrar as dependências, você pode usar o container de DI para criar novos objetos e o container resolverá automaticamente as dependências instanciando e as injetando dentro do novo objeto criado. A resolução de dependência é recursiva, isso significa que se uma dependência tem outras dependências, essas dependências também serão resolvidas automaticamente.

Você pode usar yii\di\Container::get() para criar novos objetos. O método recebe um nome de dependência, que pode ser um nome de classe, um nome de interface ou um alias. O nome da dependência pode ou não ser registrado através de set() ou setSingleton(). Você pode, opcionalmente, fornecer uma lista de parâmetros de construtor de classe e uma configuração para configurara o novo objeto criado. Por exemplo:

// "db" é um alias registrado previamente
$db = $container->get('db');

// equivale a: $engine = new \app\components\SearchEngine($apiKey, ['type' => 1]);
$engine = $container->get('app\components\SearchEngine', [$apiKey], ['type' => 1]);

Nos bastidores, o container de DI faz muito mais do que apenas a criação de um novo objeto. O container irá inspecionar primeiramente o construtor da classe para descobrir classes ou interfaces dependentes e automaticamente resolver estas dependências recursivamente. O código abaixo mostra um exemplo mais sofisticado. A classe UserLister depende de um objeto que implementa a interface UserFinderInterface; A Classe UserFinder implementa esta interface e depende do objeto Connection. Todas estas dependências são declaradas através de type hint dos parâmetros do construtor da classe. Com o registro de dependência de propriedade, o container de DI é capaz de resolver estas dependências automaticamente e cria uma nova instância de UserLister simplesmente com get('userLister').

namespace app\models;

use yii\base\BaseObject;
use yii\db\Connection;
use yii\di\Container;

interface UserFinderInterface
{
    function findUser();
}

class UserFinder extends BaseObject implements UserFinderInterface
{
    public $db;

    public function __construct(Connection $db, $config = [])
    {
        $this->db = $db;
        parent::__construct($config);
    }

    public function findUser()
    {
    }
}

class UserLister extends BaseObject
{
    public $finder;

    public function __construct(UserFinderInterface $finder, $config = [])
    {
        $this->finder = $finder;
        parent::__construct($config);
    }
}

$container = new Container;
$container->set('yii\db\Connection', [
    'dsn' => '...',
]);
$container->set('app\models\UserFinderInterface', [
    'class' => 'app\models\UserFinder',
]);
$container->set('userLister', 'app\models\UserLister');

$lister = $container->get('userLister');

// que é equivalente a:

$db = new \yii\db\Connection(['dsn' => '...']);
$finder = new UserFinder($db);
$lister = new UserLister($finder);

Uso Prático

O Yii cria um container de DI quando você inclui o arquivo Yii.php no script de entrada de sua aplicação. O container de DI é acessível através do Yii::$container. Quando você executa o método Yii::createObject(), na verdade o que será realmente executado é o método get() do container para criar um novo objeto. Conforme já informado acima, o container de DI resolverá automaticamente as dependências (se existir) e as injeta dentro do novo objeto criado. Como o Yii utiliza Yii::createObject() na maior parte do seu código principal para criar novos objetos, isso significa que você pode personalizar os objetos globalmente lidando com Yii::$container.

Por exemplo, você pode customizar globalmente o número padrão de botões de paginação do yii\widgets\LinkPager:

\Yii::$container->set('yii\widgets\LinkPager', ['maxButtonCount' => 5]);

Agora, se você usar o widget na view (visão) com o seguinte código, a propriedade maxButtonCount será inicializado como 5 em lugar do valor padrão 10 como definido na class.

echo \yii\widgets\LinkPager::widget();

Todavia, você ainda pode substituir o valor definido através container de DI:

echo \yii\widgets\LinkPager::widget(['maxButtonCount' => 20]);

Outro exemplo é se beneficiar da injeção automática de construtor do container de DI. Assumindo que a sua classe controller (controlador) depende de alguns outros objetos, tais como um serviço de reserva de um hotel.

Você pode declarar a dependência através de um parâmetro de construtor e deixar o container DI resolver isto para você.

namespace app\controllers;

use yii\web\Controller;
use app\components\BookingInterface;

class HotelController extends Controller
{
    protected $bookingService;

    public function __construct($id, $module, BookingInterface $bookingService, $config = [])
    {
        $this->bookingService = $bookingService;
        parent::__construct($id, $module, $config);
    }
}

Se você acessar este controller (controlador) a partir de um navegador, você vai ver um erro informando que BookingInterface não pode ser instanciado. Isso ocorre porque você precisa dizer ao container de DI como lidar com esta dependência:

\Yii::$container->set('app\components\BookingInterface', 'app\components\BookingService');

Agora se você acessar o controller (controlador) novamente, uma instância de app\components\BookingService será criada e injetada como o terceiro parâmetro do construtor do controller (controlador).

Quando Registrar Dependência

Em função de existirem dependências na criação de novos objetos, o seu registo deve ser feito o mais cedo possível. Seguem abaixo algumas práticas recomendadas:

  • Se você é o desenvolvedor de uma aplicação, você pode registrar dependências no [script de entrada] (structure-entry-scripts.md) da sua aplicação ou em um script incluído no script de entrada.
  • Se você é um desenvolvedor de extensão, você pode registrar as dependências no bootstrapping (inicialização) da classe da sua extensão.

Resumo

Ambas as injeção de dependência e service locator são padrões de projetos conhecidos que permitem a construção de software com alta coesão e baixo acoplamento. É altamente recomendável que você leia o Artigo do Martin para obter uma compreensão mais profunda da injeção de dependência e service locator.

O Yii implementa o service locator no topo da injeção dependência container (DI). Quando um service locator tenta criar uma nova instância de objeto, ele irá encaminhar a chamada para o container de DI. Este último vai resolver as dependências automaticamente tal como descrito acima.