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 dependencias. [Martin's article] (http://martinfowler.com/articles/injection.html) explica bem porque o container DI é útil. Aqui vamos explicar principalmente a utilização do container DI fornecido pelo Yii.

Injeção de Dependência

Yii fornece o recurso container 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.

Injecão de Construtor

O container 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

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\Object;

class Foo extends Object
{
    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 no 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\Object]]), 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()]] é 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 () {
    $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()
    {
        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 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 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 de 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 DI faz muito mais do que apenas a criação de um novo objeto. O container irá inspecionar o 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 hinting dos parâmetros do construtor da classe. Com o registro de dependência de propriedade, o container 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\Object;
use yii\db\Connection;
use yii\di\Container;

interface UserFinderInterface
{
    function findUser();
}

class UserFinder extends Object implements UserFinderInterface
{
    public $db;

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

    public function findUser()
    {
    }
}

class UserLister extends Object
{
    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

Yii cria um container DI quando você inclui o arquivo Yii.php no script de entrada da sua aplicação. O container 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 [[yii\di\Container::get()|get()]] do container para criar um novo objeto. Conforme já informado acima, o container DI resolverá automaticamente as dependências (se existir) e as injeta dentro do novo objeto criado. Como 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 DI:

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

Outro exemplo é se beneficiar da injecção automática de construtor do container 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 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

Ambos 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 Martin's article para obter uma compreensão mais profunda da injeção de dependência e service locator.

Yii implementa o service locator no topo da injecçã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 DI. Este último vai resolver as dependências automaticamente tal como descrito acima.