Injeção de Dependência com PHP-DI

Introdução

Injeção de dependência é um design pattern que visa diminuir o acoplamento entre classes, facilitando e permitindo a troca das dependências em tempo de execução ou em tempo de compilação.

Usar a injeção de dependência nos ajuda a escrever códigos mais fáceis de manter e testar, além de torna-los mais modular. Todos projetos tem dependências, principalmente grandes projetos. Dependendo de como você gerencia as dependências nele, pode acabar tornando seu projeto difícil de manter.

Injeção de dependência já é um conceito antigo, bastante difundido principalmente na linguagem Java, porém tem ganhado popularidade também em projetos PHP principalmente devido aos frameworks como Zend, Symphony e Laravel e/ou e também devido a evolução da linguagem PHP onde os profissionais tem buscado trabalhar com as melhores práticas de desenvolvimento.

Para exemplificar os conceitos, vamos ver algo prático e simples. Vamos criar uma par de classes sem usar qualquer tipo de injeção de dependência. Depois vamos reescrevê-las usando o conceito de injeção de dependência. Como exemplo, temos uma classe chamada SmtpEmail, responsável por enviar e-mails conectando em um servidor de SMTP, e uma outra classe User, responsável por gerenciar funções diversas de um Usuário, que tem um método que envia um e-mail para o usuário. A classe User utiliza a classe SmtpEmail como auxiliar no processo de envio de e-mail. Podemos dizer então que a classe User depende da classe SmtpEmail.

SmtpEmail:

<?php

class SmtpEmail
{
        private $smtpServer;

        public function __construct($smtpServer)
        {
                $this->smtpServer = $smtpServer;
        }


        public function send($to, $subject, $body)
        {
                // conecta no servidor de smtp e envia o email
        }
}

User:

<?php

class User
{
        private $nome;
        private $email;
        private $smtpEmail;

        function __construct($smtpServer, $nome, $email)
        {
                $this->smtpEmail = new SmtpEmail($smtpServer);
                $this->nome = $nome;
                $this->email = $email;
        }


        public function sendMail($subject, $body)
        {
                $this->smtpEmail->send($this->email, $subject, $body);
        }
}

De acordo com o modelo acima, podemos destacar alguns pontos que infringem princípios de boas práticas de desenvolvimento:

  • A classe SmtpEmail é instanciada dentro da classe User (Linha 11). Ela esta acoplada a classe User. A principio parece não haver problema com esse modelo. Porém imagine se for necessário inserir mais parâmetros na classe de SmtpEmail. Será necessário alterar todas as classes que utilizam a SmtpEmail em seu código.
  • Ficou difícil usar testes unitários na classe acima, não sendo possível testar somente a classe User sem ter que testar também a classe SmtpEmail.
  • Devido ao acoplamento, ficou difícil a manutenção caso precise de uma implementação diferente para o serviço de envio de e-mail. Quanto menos a classe User souber sobre a classe SmtpEmail melhor.

Injeção de dependência através do construtor:

Vamos reescrever a classe User usando o conceito de injeção de dependência.

Utilizando a Injeção de dependência temos um código mais fácil de manter, principalmente se estamos trabalhando em projetos grandes. Com um pequeno ajuste no código anterior, aumentamos a qualidade do código, diminuindo sua complexidade, além de deixá-lo mais fácil de se testar.

A injeção de dependência mais comum normalmente é feita através de construtor de classe. Veja abaixo a classe User modificada para receber a dependência (SmtpEmail) através do construtor:

<?php

class User
{
        private $nome;
        private $email;
        private $smtpEmail;

        function __construct(SmtpEmail $smtpEmail, $nome, $email)
        {
                $this->smtpEmail = $smtpEmail;
                $this->nome = $nome;
                $this->email = $email;
        }


        public function sendMail($subject, $body)
        {
                $this->smtpEmail->send($this->email, $subject, $body);
        }
}

Considerações:

  • Na maioria das vezes a dependência de objeto é obrigatória. Usando injeção através do construtor garante que teremos sempre disponível uma instância daquela classe.
  • Também estamos garantindo que a dependência não poderá ser alterada durante o tempo de vida do objeto. O construtor é chamado somente uma vez e não temos um método setter que altere a referência interna da dependência.

Injeção de dependência através de método Setter:

Outra forma de tratar as dependências, é injetar através de métodos setters. Veja abaixo a classe User modificada para receber a dependência via método setter ao invés de construtor:

<?php

class User
{
        private $nome;
        private $email;
        private $smtpEmail;

        function __construct($nome, $email)
        {
                $this->nome = $nome;
                $this->email = $email;
        }

        public function setSmtpEmail(SmtpEmail $smtpEmail)
        {
                $this->smtpEmail = $smtpEmail;
        }

        public function sendMail($subject, $body)
        {
                $this->smtpEmail->send($this->email, $subject, $body);
        }
}

Considerações:

  • Permite que a dependência seja opcional. (O usuário da classe pode ou não chamar o método setSmtpEmail)
  • Adicionar novas dependências a classe por setter é mais simples do que modificar o construtor. A possibilidade de quebra de código é menor.
  • Indicado para situações onde é necessário maior flexibilidade no uso da Classe.

Containers de Injeção de Dependência

Quanto temos um sistema onde existe a necessidade de gerenciar uma série de objetos e uma série de dependências entre eles, um Container de Injeção de Dependência se torna extremamente útil. Um container de injeção de dependência fica responsável por instanciar os objetos e gerenciar as dependências entre eles. Podemos orientar o container como realizar os injections através de arquivo de configuração ou através do uso de Annotations, porém na maioria das vezes não é necessário.

Em PHP temos algumas opções de containers de DI. O próprio Symphony e ZF fornecem boas opções. Porém eu gostaria de destacar neste artigo o PHP-DI: The dependency injection container for humans! Uma container que se destaca pela qualidade e simplicidade e que pode ser facilmente integrado em qualquer Framework.

No restante deste artigo vamos aprender de forma prática como iniciar com o php-di.

Instalando o PHP-DI

O PHP-DI pode ser facilmente instalado em seu projeto através do composer. Crie o arquivo composer.json com o conteúdo abaixo:

{
    "require": {
        "mnapoli/php-di": "~4.0"
    }
}

Para instalar o composer e o php-di:

curl -s http://getcomposer.org/installer | php
 php composer.phar install

Autowiring

Nesse modo, ativado por padrão, o container criará a instância da classe desejada injetando automaticamente as dependências dela. Não há necessidade de nenhuma configuração extra a ser feita. Para que funcione corretamente é necessário que a classe receba as dependências através do construtor e que estejam corretamente “tipadas”.

Para entender melhor, vamos criar abaixo a classe \Application\Foo que recebe a classe \Application\Bar como injeção de dependência através do construtor. Em seguida veremos como obter uma instância dela através do container.

namespace Application;

class Foo
{
    private $bar;

    public function __construct(\Application\Bar $bar)
    {
        return $this->bar = $bar;
    }
}

Para obter a instância da classe Application\Foo, não vamos instancia-la diretamente, mas sim solicitá-la ao container do php-di. Ele se encarregará de fazer as injeções necessárias, no caso \Application\Bar. Veja exemplo abaixo:

use DI\ContainerBuilder;

// autoload gerado pelo composer
require "vendor/autoload.php";

// cria container 
$builder = new ContainerBuilder();
$container = $builder->build();

// instancia 'Application\Foo'
$foo = $container->get('Application\Foo');

A maioria dos casos de injeção de dependência é resolvido pelo modo Autowire.

Arquivo de configuração PHP

Caso o Autowire não seja suficiente para você, pois precise de instruções para inicializar um objeto, ou dependência como seja, o php-di permite que você tenha mais controle das instâncias que deseja criar usando um arquivo de configuração de apoio, usando formato de array do PHP.

É possível definir uma série de tipos de entradas no arquivo de configuração:

  • valores: string, inteiros, array, etc.
  • object: DI\object($classname = null): define uma entrada de objeto.
  • factory: DI\factory($factory): define uma função callback (factory) que retorna uma instância.
  • alias: DI\link($entryName): permite referenciar outras entradas no arquivo de configuração.

Veja abaixo um arquivo de exemplo onde usamos alguns dos componentes que o php-di fornece através de configuração. Neste exemplo, mostramos como instruir o php-di quando for necessário obter uma instância de ‘Monolog\Logger’. Estamos usando um factory neste caso. Também criamos uma entrada que retorna um objeto da classe ‘My\Database\Class’. Veja que usamos o componente DI\link() que permite referenciar outras entradas dentro do mesmo arquivo de configuração.

<?php
// config-di.php

// imports
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\LineFormatter;
use DI\Container;
use DI\Scope;

return array(

    // caminho do arquivo de log para o monolog
    'log.path' => '/var/log/my-app-log',

    // valores
    'db.name' => 'nome do database',
    'db.user' => 'usuario',
    'db.pass' => 'senha',
    'db.host' => '192.168.0.1',

    // exemplo, classe de conexao com banco de dados
    // usando DI\object() e DI\link() para referenciar valores definidos acima
    'My\Database\Class' => DI\object()
        ->constructor(DI\link('db.host'), 
                       DI\link('db.user'),  
                       DI\link('db.pass'),  
                       DI\link('db.name')),

    // exemplo fazendo factory do monolog
    // usando DI\factory() e DI\link()
    'Monolog\Logger' => DI\factory(function (Container $c) {
        $logger = new Logger($c->get('app.name'));

        $fileHandler = new StreamHandler($c->get('log.path'), Logger::DEBUG);
        $fileHandler->setFormatter(new LineFormatter(null, null, false, true));
        $logger->pushHandler($fileHandler);

        return $logger;
    }),

);

Vamos supor que você tenha uma outra classe que receba tanto uma instância do \My\Database\Class quanto uma instância do \Monolog\Logger como injeção por construtor:

<?php
namespace Application;

class Foo
{
    private $db;
    private $logger;

    public function __construct(\My\Database\Class $db, 
                                \Monolog\Logger $logger)
    {
        ...
    }
}

Vamos agora usar o container para obter a instância da classe Application\Foo, informando agora sobre o arquivo de configuração criado anteriormente:

$builder = new ContainerBuilder();
$builder->addDefinitions(__DIR__ . '/config.php');
$container = $builder->build();

// obtem Application\Foo ja injetando \My\Database\Class e \Monolog\Logger atraves do autowire
$foo = $container->get('Application\Foo');

O container irá injetar automaticamente \My\Database\Class e \Monolog\Logger na classe Application\Foo se baseando nas instruções do arquivo de configuração que criamos. Veja que não foi necessário criar uma entrada da classe Application\Foo no arquivo de configuração. Portanto a injeção foi feita automaticamente pelo container através de Autowire.

Interessante não ? Outro ponto importante a observar aqui, é que o container trabalha com as instâncias usando o Designer Pattern Singleton, ou seja, haverá sempre uma única instância da classe independente de quantas vezes ela for solicitada. Você pode instruir o container para que ele construa uma nova instância da classe sempre que for requisitada. Isso pode ser feito definindo o escopo para PROTOTYPE. Veja no exemplo abaixo:

<?php

return [

	// constroi sempre uma nova instancia quando requisitado
	'My\OtherClass' => DI\object()->scope(Scope::PROTOTYPE()),

	// factory: constroi sempre uma nova instancia quando requisitado
	'SomeOtherClass' => DI\factory(function () {
		return new SomeOtherClass();
	})->scope(Scope::PROTOTYPE()),

];

Annotations

Outro método de injeção de dependência no php-di pode ser feito através de Annotations. Annotations são inseridas em comentários do PHP (docblock). As possibilidades de Injeções através de Annotations podem ser via:

  • construtor
  • metodos setters
  • properties

Assim como o Autowiring a injeção de dependência por Annotations já vem habitada por padrão. Veja o exemplo abaixo um caso bem simples do uso de injeção de dependência por Annotations: (via properties, construtor e método setter)

class Example {

    /**
     * @Inject
     * @var Foo
     */
    protected $property1;

    /**
     * @Inject
     * @param Foo $param1
     * @param Bar $param2
     */
    public function __construct($param1, $param2) {
    }

    /**
     * @Inject
     */
    public function method1(Foo $param) {
    }
}

Basta instanciar a classe acima através do container que as injeções declaradas via annotation serão inseridas automaticamente.

Finalizando

O meu objetivo com este artigo foi inserir o conceito de Injeção de Dependência e exemplos de como dar um pontapé inicial no uso do php-di. Caso queira se aprofundar mais e conhecer melhor o php-di, aconselho a consultar a documentação oficial através do link: http://php-di.org/doc/definition.html.

Espero que tenha aproveitado.

Referências

http://coderoncode.com/2014/01/06/dependency-injection-php.html
http://php-di.org/

Please follow and like us:

Comments

  1. By Jhonata Menezes

Follow

Get every new post on this blog delivered to your Inbox.

Join other followers: