Elefante sob o sol

Entendendo a PSR-11 e Containers de Injeção de Dependência

Injeção de dependências

Injeção de dependências é um conceito bem simples na realidade, embora o nome possa causar um certo estranhamento inicialmente.

O principio da injeção de dependência é algo mais fácil de mostrar do que descrever, visto que uma descrição poderia ser “Onde um objeto recebe suas dependências de outro objeto ou parte do programa”, mas uma definição mais clara pode ser encontrada na wikipedia, como descrito a seguir:

Onde um objeto fornece as dependências de outro objeto. Uma dependência é um objeto que pode ser usado, por exemplo, um serviço. Ao invés do cliente (classe que usa a dependência) especificar qual serviço irá usar, algo (a classe que fornece a dependência) fala ao cliente o que usar.

Fonte: https://en.wikipedia.org/wiki/Dependency_injection (Acessado em 15/10/2019, Tradução Própria)

De inicio se você não viu injeção de dependência esta ideia pode parecer um pouco difícil de explicar, mas vamos a um exemplo em código para ilustrar melhor a ideia.

<?php 
    // UsuarioDAO.php
    class UsuarioDAO
    {   
        public function __construct()
        {
        }

        public function auth(string $username, string $password) : bool
        {
            // Tenta autenticar usuário
        }
    }
<?php 
    // AuthController.php
    class AuthController
    {
        public function login($req)
        {
            $username = $req->username;
            $password = $req->password;
            $dao = new UsuarioDAO();
            $authorized = $dao->auth($username, $password);
            // Outras operações e retorna resultado.
        }
    }

Neste exemplo a classe AuthController depende da classe UsuarioDAO para fornecer autenticação, até ai este código pode parecer normal.

Disse normal, mas vamos a uma reflexão, leia as perguntas abaixo e reflita sobre como isso seria possível com o código atual?

  • Se quiser criar um teste de unidade para este controller, como você poderia mockar o DAO?
  • Seria no futuro o construtor do UsuarioDAO receber argumentos obrigatórios eu precisaria alterar o código do AuthController?
  • Se em algum momento no futuro eu mudar minha autenticação para um sistema externo, por exemplo LDAP, poderia fazer isso sem alterar o AuthController?

Caso tenha refletido sobre as perguntas você pode ter chegado a conclusão que o AuthController está fortemente acoplado ao UsuarioDAO, existe algumas formas bem simples de melhorar este código, vamos ver uma possível iteração do nosso AuthController

<?php 
    // AuthController.php
    class AuthController
    {
        public function login($req, UsuarioDAO $dao)
        {
            $username = $req->username;
            $password = $req->password;
            $authorized = $dao->auth($username, $password);
            // Outras operações e retorna resultado.
        }
    }

Ao olhar de relance pode não parecer que fizemos muito, caso ainda não tenha notado as principais diferenças entre este código e o anterior é que o UsuarioDAO não é mais instanciado dentro do método login, e agora é passado como argumento, vamos rever as nossas perguntas para ver se esta pequena mudança fez alguma diferença:

  • P: Se quiser criar um teste de unidade para este controller, como você poderia mockar o DAO?
    R: Sim poderiamos, frameworks como PHPUnit permitem mockar objetos para realização de testes.
  • P: Seria no futuro o construtor do UsuarioDAO receber argumentos obrigatórios eu precisaria alterar o código do AuthController?
    R: Não, o código do AuthController não precisa saber como o UsuarioDAO é instanciado, se preocupando apenas em chamar o método auth.
  • P: Se em algum momento no futuro eu mudar minha autenticação para um sistema externo, por exemplo LDAP, poderia fazer isso sem alterar o AuthController?
    R: Ainda não.

Bem, o que fizemos aqui em parte já pode ser considerado injeção de dependências, em nosso contexto podemos entender que um objeto, por exemplo nosso Roteador hipotético, constrói um UsuarioDAO e passa ele para o nosso AuthController, exatamente como previsto na citação.

Contudo ainda podemos melhorar, afinal eu não coloquei a pergunta sobre LDAP a toa, vamos ver abaixo como podemos melhorar nosso exemplo:

<?php 
    // AutenticavelInterface.php
    interface AutenticavelInterface 
    {
        public function auth(string $username, string $password) : bool;
    }
<?php 
    // UsuarioDAO.php
    class UsuarioDAO implements AutenticavelInterface
    {   
        public function __construct()
        {
        }

        public function auth(string $username, string $password) : bool
        {
            // Tenta autenticar usuário
        }
    }
<?php 
    // LDAPConnector.php
    class LDAPConnector implements AutenticavelInterface
    {   
        public function __construct()
        {
        }

        public function auth(string $username, string $password) : bool
        {
            // Tenta autenticar usuário
        }
    }
<?php 
    // AuthController.php
    class AuthController
    {
        public function login($req, AutenticavelInterface $autenticavel)
        {
            $username = $req->username;
            $password = $req->password;
            $authorized = $autenticavel->auth($username, $password);
            // Outras operações e retorna resultado.
        }
    }

Bem vamos entender as mudanças que fizemos.

Nós criamos uma nova interface chamada AutenticavelInterface, esta interface documenta apenas um método chamado auth, que possui a mesma assinatura da classe UsuarioDAO.

Após isso nós alteramos UsuarioDAO para que ele implementasse a interface AutenticavelInterface, e por fim nós alteramos o método login do AuthController para receber um AutenticavelInterface.

Como isso pode nos ajudar você pode estar se perguntando. Bem, ao fazer esta mudança nós desacoplamos ainda mais a classe AuthController da classe UsuarioDAO, neste novo cenário o nosso hipotético roteador pode passar ao método login tanto um objeto UsuarioDAO quanto um objeto LDAPConnector, visto que ambos implementam a interface AutenticavelInterface.

Caso ache que já viu isso antes você provavelmente está certo, esta técnica é chamada de Inversão de Dependência, se você já viu SOLID em orientação a Objeto este é o principio da letra D (Dependency Inversion), que pode ser resumido como:

Um deve “depender de abstrações, não de concreções (implementações)”

Fonte: https://en.wikipedia.org/wiki/SOLID (Tradução própria de One should “depend upon abstractions, [not] concretions.”)

Que por sua vez é baseada da interpretação dos dois princípios da inversão de dependência:

A. Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.

B. Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.

Fonte:  “Dependency Inversion Principle” Página.6 Tradução Própria.

Na prática ao fazer isso você dá menos oportunidades de mudança ao seu código, agora a nossa classe AuthController por exemplo não vai precisar mudar se a gente alterar o gateway de autenticação (desde que esse implemente o AutenticavelInterface).

Eu preciso transformar tudo que minhas classes ou meus métodos usam em dependências?

Não, absolutamente não, e isso pode ser uma questão meio difícil de entender após ler a seção anterior, mas há uma forma de ajuda-lo a entender onde a injeção de dependência é útil e onde ela atrapalha.

A injeção de dependência serve para auxiliar você diminuir o nível de acoplamento e a manter o principio da responsabilidade única de uma classe, módulo ou unidade de um programa, uma outra forma de pensar é se perguntar “Quais são os motivos pelo qual esse pedaço de programa precisaria ser atualizado?”, se você tiver mais de um motivo, essa unidade do seu programa está violando o princípio único de responsabilidade.

E os containers de injeção de dependência?

Certo espero ter conseguido explicar bem o que é injeção de dependência, mas agora se você ainda lembra do titulo do artigo o que bugalhos são containers de injeção de dependências?

Bem, você se esforçou para aplicar injeção de dependência em seu código, mas um outro problema surgiu. Se você está injetando as dependências, onde você está instanciando os objetos que serão passados como dependências?

Nota importante: Você não precisa de containers de injeção de dependências para praticar injeção de dependência.

Respondendo a esta pergunta, não há como fugir, em algum momento seu programa vai precisar instanciar alguns objetos, o que você pode fazer no entanto é entender onde é ok instanciar objetos e o que você pode fazer para diminuir o acoplamento e a necessidade de mudança de seu código.

Muitos sistemas e frameworks web que se dizem MVC trabalham em geral com um padrão chamado de Front Controller, que de forma resumida significa que todas as requisições para um determinado programa ou sistema passam por um único lugar. Uma descrição breve pode ser encontrada no catálogo de padrões de arquitetura de aplicações corporativas de Martin Fowler, assim como uma rápida análise do padrão pode ser encontrada no site GeeksForGeeks.

Isso é importante, porque nos dá um plano de execução no qual sabemos que todas as requisições vão executar um determinado código, neste momento podemos incluir uma ferramenta que irá instanciar os objetos que precisamos e irá passar para as classes que preparamos para receber estes objetos.

Normalmente o front controller também é acompanhado de um roteador (ou router em inglês), responsável por definir para onde cada requisição deve ser enviada para que possa ser atendida e devolver uma resposta.

Normalmente um container é registrado junto a aplicação e passado como uma dependência para o roteador, o roteador por sua vez vai identificar para onde deve enviar a requisição que foi recebida pelo programa, determinar quais dependências o código que atende a requisição tem e providenciar a este código instâncias das dependências que ele precisa.

Se você conseguiu entender tudo isso só lendo o texto parabéns, mas eu entendo que este conceito pode ser um pouco difícil de pegar, por este motivo eu desenhei um diagrama de sequência para auxiliar no entendimento desse fluxo.

O diagrama acima representa como a maioria das aplicações e frameworks se comportam no que tange responder a uma requisição, as implementações podem ser diferentes, mas em geral a ideia é a mesma.

Mas onde as instâncias são criadas? E como a PSR-11 entra nisso?

Se você prestou atenção até aqui deve ter notado que eu ainda não mencionei como o container obtém as instâncias que ele vai fornecer para as demais classes.

É importante notar que não existe apenas um meio, e que não existe um meio certo para um container criar os objetos que serão fornecidos, vou listar mais a frente algumas das possibilidades nas quais você poderia criar e instanciar um container de dependência.

Boa parte do foco deste artigo foi no fornecimento dos objetos, e isso é proposital, a PSR-11 (PSR significa Recomendação de Padrão PHP) só define como deve ser a interface para obter objetos de um container, ela não dita como o container ontem ou cria estes objetos.

  • Caso você não conheça as PSRs, é importante notar que elas são recomendações de padrões de código para PHP, o principal objetivo delas é prover uma interface simples para soluções de problemas comuns para que implementações destas soluções possam ser intercambiáveis entre sistemas, o grupo que cria as PSRs literalmente se chama Framework Interoperability Group, ou em português: Grupo de Interoperabilidade de Frameworks.

Uma das motivações para isso é que os containers normalmente só possuem dois tipos de operação, sendo elas:

  • Configuração de objetos, valores ou dependências.
  • Fornecimento de objetos, valores ou dependências.

E que estas operações são feitas por atores/lados diferentes, no sentido de que normalmente o desenvolvedor é responsável por configurar os valores/objetos e o framework onde o container está sendo usado é responsável por obter os objetos, desta forma a interface proposta pela PSR-11 serve para padronizar a interação entre o framework e o container, e não entre o programador e o container.

É interessante notar o outro lado da PSR-11, que se você construir a sua aplicação para usar um container compatível com a PSR-11, no futuro você pode trocar por qualquer outro que também seja compatível.

Isso não quer dizer que você precise seguir as recomendações deles para seu container de injeção, só quer dizer que se você seguir, você pode usar a implementação do seu container em um framework que adere a esta interface.

A PSR-11 pode ser encontrada em https://www.php-fig.org/psr/psr-11/, e define que um container deve implementar a interface Psr\Container\ContainerInterface e ao implementar esta interface deve expor dois métodos, sendo eles:

  • get(string $identificador)
  • has(string $identificador)

Esta PSR também define que ambos os métodos devem receber um argumento obrigatório, do tipo string que identifique o objeto que deve ser retornado ou consultado.

Método GET

O método get deve receber um identificador e retornar um valor (obj, array, etc) se ele possuir este valor, ou lançar uma exceção caso não encontre o valor.

Método HAS

O método has deve receber um identificador e retornar um booleano, com valor verdadeiro caso possa retornar um valor para o identificador ou falso caso não possa.

É importante notar que se o método has retornar falso, o método get deve lançar exceção de não encontrado.

A PSR também define que a exceção que for lançada deve implementar a interface Psr\Container\NotFoundExceptionInterface.

E de forma bem resumida esta PSR se resume a isso, caso tenha interesse em implementar recomendo a leitura da PSR em sua página, e se quiser implementar o download do pacote com as interfaces do packagist (pode ser adicionado ao seu composer.json).

Exemplos de implementação

Para finalizar com chave de ouro esta nossa jornada por containers de injeção de dependência vamos explorar algumas ideias de implementação, é importante notar que o objetivo não é listar todos os containers ou ideias já usadas, mas sim explorar algumas ideias de como a parte de configuração e uso dos containers é implementada em alguns frameworks.

Laravel Service Container

Se você já se aventurou na documentação do Laravel provavelmente já ouviu falar do Laravel Service Container, a ideia de ter um container de “serviços” não é nova no entanto, frameworks como dotnet core da microsoft para C# e VB.NET, assim como o Symfony apresentam este conceito.

O Laravel permite o registro do serviço a partir de um Service provider, para registrar um você pode usar o comando:

php artisan make:provider MeuServiceProvider

Isso deve criar uma classe que extende a classe ServiceProvider que declara um método chamado register.

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class MeuServiceProvider extends ServiceProvider
{
    /**
     * Registra os serviços no container.
     *
     * @return void
     */
    public function register()
    {
        // Registrar o serviço aqui.

        // Bind vincula uma chave (o nome da classe qualificado) 
        // a algo que resolve uma instância da classe, ou no caso de
        // interfaces um nome da classe que implementa a interface.
        $this->app->bind(App\MinhaClasse::class, function ($app) {
            return new App\MinhaClasse($app->make('OutraDependencia'));
        });

        // Singleton registra um serviço que deve ser resolvido
        // apenas uma vez, ou seja, depois de resolvido a mesma 
        // instância será devolvida.
        $this->app->singleton(App\MinhaClasseSingleton::class, function ($app) {
            return new App\MinhaClasseSingleton($app->make('OutraDependencia'));
        });

    }

    // O laravel ainda conta com outros recursos para registrar
    // dependências, se em um service provider você colocar a 
    // propriedade $defer como verdadeira ele só vai resolver
    // a dependência quando for necessário.
    protected $defer = true;

    // Além disso você também pode declarar bindings simples
    // em duas arrays de propriedades de um service provider
    // como abaixo:
    public $bindings = [App\MinhaClasseInterface::class => App\MinhaClasseImplementacao::class, ...];
    public $singletons = [App\MinhaClasseSingletonInterface::class => App\MinhaClasseSingletonImplementacao::class, ...];
}

Uma vez que a classe existe ela deve ser registrada junto a aplicação no arquivo config/app.php, dentro da array que fica na chave da array ‘providers’, parece meio confuso, mas o arquivo tem a seguinte estrutura:

<?php
// O arquivo inicia retornando um array;
return [
    // várias chaves nas demais linhas ...
    'providers' => [
        // Serviços definidos pelo Laravel.
        App\Providers\MeuServiceProvider::class,
    ]
]

Se você chegou a ver a injeção do dotnet core isso pode parecer com o ConfigureServices do Startup.cs.

O Laravel realiza a injeção automática por meio de typehints, para isso basta utilizar o nome da classe como typehint de um parametro no método de um controller, queue job ou middleware, ou qualquer outra classe que o Laravel resolva dependências.

Fonte: https://laravel.com/docs/5.7/container#automatic-injection

Caso esteja se perguntando sobre a PSR-11, você pode obter uma instância do container ao usar um typehint para classe Psr\Container\ContainerInterface, na qual você receberá uma instância que está de acordo com a PSR-11, mais detalhes podem ser vistos aqui.

O Laravel possui ainda outros esquemas para injeção de dependência, mas não vou me aprofundar neles, mais sobre isso pode ser lido aqui e aqui.

Slim Framework com PHP-DI

Outro framework PHP não tão conhecido é o Slim Framwork, ele é um framework leve para pequenas aplicações e provas de conceito.

Até a versão 3 o Slim fornecia um simples container, além de oferecer suporte para qualquer container que atendesse a PSR-11, a partir da versão 4 o Slim não é mais distribuído com um Container de Injeção de dependências, por este motivo a própria documentação sugere usar um container que atenda a PSR-11, como por exemplo o PHP-DI que no momento de escrita deste artigo é um dos containers mais famosos no Packagist.

<?php
// Importa o PHP-DI container
use DI\Container;
use Psr\Container\ContainerInterface;
use Slim\Factory\AppFactory;

// Chama o autoloader do Composer, assumindo que este
// arquivo está na mesma pasta do vendor.
require_once __DIR__ . '/../vendor/autoload.php';

// Cria um novo container
$container = new Container();

// Adiciona uma resolução para quando for necessário
// injetar uma dependência da MinhaClasseInterface.
$container->set(MinhaClasseInterface::class, function (ContainerInterface $container) {
    return new MinhaClasse(...usa o container para resolver algo...);
});


// registra outras dependências ...

// Configure the application via container
AppFactory::setContainer($container);
$app = AppFactory::create();


// Inicia a aplicação SLIM.
$app->run();

Vale notar que você pode criar algumas classes que recebem um container, registram as dependências e retornar um objeto container que pode ser então passado para o método setContainer do AppFactory.

Se você tiver uma API Web estruturada por exemplo com um diretório de DTOs você pode instruir uma função ou classe do PHP a ler o diretório e injetar um builder que obtem os dados de um RequestInterface da PSR-7 e retornar uma instância deste DTO.

Um conceito que o PHP-DI tem que é interessante é o AutoWiring, neste modo quando ativado o PHP-DI usa a API de reflections do PHP para identificar, instanciar e injetar as instâncias nos métodos que dependem delas, você pode ler mais sobre isso nesta página: http://php-di.org/doc/autowiring.html

O AutoWiring contudo pode não conseguir resolver interfaces, por este motivo é importante nestes casos registrar as interfaces com suas implementações no container.

Considerações

Espero que este artigo tenha lhe auxiliado a entender um pouco mais do funcionamento da injeção de dependência e de seus usos.

Deixe uma resposta