Os serviços não precisam de nomes

há 3 anos De Jiří Pudil  

Eu adoro a solução de injeção de dependência da Nette Framework. Eu realmente amo. Este post está aqui para compartilhar esta paixão, explicando porque eu acho que é a melhor solução de DI no ecossistema PHP de hoje.

(Este post foi publicado originalmente no blog do autor).

O desenvolvimento de software é um processo interminável e iterativo de abstração. Nós encontramos abstrações apropriadas do mundo real na modelagem de domínios. Na programação orientada a objetos, utilizamos abstrações para descrever e executar contratos entre vários atores dentro do sistema. Introduzimos novas classes no sistema para encapsular responsabilidades e definir seus limites, e depois usamos composição para construir o sistema inteiro.

Estou falando da necessidade de extrair a lógica de autenticação do seguinte controlador:

final class SignInController extends Best\Framework\Controller
{
    public function action(string $username, string $password): Best\Framework\Response
    {
        if ($username !== 'admin' || $password !== 'p4ssw0rd!') {
            return $this->render(__DIR__ . '/error.latte');
        }

        $this->signIn(new Identity($username));
        return $this->redirect(HomepageController::class);
    }
}

Provavelmente você pode dizer que a verificação das credenciais não pertence lá. Não é responsabilidade do controlador dizer quais credenciais são válidas – seguindo o princípio de responsabilidade única, o controlador deve ter apenas um único motivo para mudar, e esse motivo deve estar dentro da interface do usuário da aplicação, e não no processo de autenticação.

Vamos pegar a saída óbvia disto e extrair a condição para uma classe Authenticator:

final class Authenticator
{
    public function authenticate(string $username, string $password): bool
    {
        return $username === 'admin' && $password === 'p4ssw0rd!';
    }
}

Agora tudo o que precisamos fazer é delegar do controlador a este autenticador. Nós fizemos o autenticador uma dependência do controlador, e, de repente, o controlador precisa levá-la a algum lugar:

final class SignInController extends Best\Framework\Controller
{
    public function action(string $username, string $password): Best\Framework\Response
    {
        $authenticator = new Authenticator(); // <== fácil, vou apenas criar um novo!
        if ( ! $authenticator->authenticate($username, $password)) {
            return $this->render(__DIR__ . '/error.latte');
        }

        $this->signIn(new Identity($username));
        return $this->redirect(HomepageController::class);
    }
}

Esta maneira ingênua funcionará. Mas somente até que uma autenticação mais robusta seja implementada, exigindo que o autenticador consulte uma tabela de banco de dados de usuários. O Authenticator de repente tem uma dependência própria, digamos um UserRepository, que por sua vez depende de uma instância Connection, que é dependente dos parâmetros do ambiente específico. Isso se intensificou rapidamente!

Criar instâncias à mão em todos os lugares não é uma forma sustentável de gerenciar dependências. É por isso que temos o padrão de injeção de dependência, que permite ao controlador simplesmente declarar sua dependência em um Authenticator, e deixar que seja um problema de outra pessoa realmente fornecer uma instância. E que outra pessoa seja chamada de injeção de dependência container.

O recipiente de injeção de dependência é o arquiteto supremo da aplicação – ele sabe como resolver as dependências de qualquer serviço dentro do sistema e é responsável por criá-las. Os recipientes DI são tão comuns hoje em dia que praticamente toda grande estrutura web tem sua própria implementação de recipientes, e há até mesmo pacotes independentes dedicados à injeção de dependência, como PHP-DI.

Queimando as pimentas

A abundância de opções acabou motivando um grupo de desenvolvedores a buscar uma abstração para torná-las interoperáveis. A interface comum tem sido polida ao longo do tempo e eventualmente proposta ao PHP-FIG na seguinte forma:

interface ContainerInterface
{
    public function get(string $id): mixed;
    public function has(string $id): bool;
}

Esta interface ilustra um atributo muito importante dos recipientes DI: Eles são como fogo. Eles são um bom servo, mas podem facilmente se tornar um mau mestre. Eles são tremendamente úteis desde que você saiba usá-los, mas se você os usar incorretamente, eles o queimarão. Considere o seguinte recipiente:

final class Container implements ContainerInterface
{
    private array $factories = [];
    public function __construct(array $parameters)
    {
        $this->factories['authenticator'] = fn() => new Authenticator($this->get('userRepository'));
        $this->factories['userRepository'] = fn() => new UserRepository($this->get('connection'));
        $this->factories['connection'] = fn() => new Connection($parameters['database']);
    }

    public function get(string $id): mixed { /* . . . */ }
    public function has(string $id): bool { /* . . . */ }
}

Até o momento, tudo bem. A implementação parece boa pelo padrão que estabelecemos: ela de fato sabe como criar cada serviço na aplicação, resolvendo recursivamente suas dependências. Tudo é gerenciado em um único lugar, e o container até aceita parâmetros, de modo que a conexão do banco de dados é facilmente configurável. Legal!

Mas agora, vendo os dois únicos métodos do ContainerInterface, você pode ser tentado a usar o recipiente desta forma:

final class SignInController extends Best\Framework\Controller
{
    public function __construct(
        private ContainerInterface $container,
    ) {}

    public function action(string $username, string $password): Best\Framework\Response
    {
        $authenticator = $this->container->get('authenticator');
        //...
    }
}

Parabéns, você acabou de queimar seus pimentões. Em outras palavras, o recipiente se tornou o mau mestre. Por que isso acontece?

Primeiro, você está confiando em um identificador de serviço arbitrário: 'authenticator'. A injeção de dependência tem tudo a ver com ser transparente sobre as dependências, e o uso de um identificador artificial vai diretamente contra essa noção: faz o código depender silenciosamente da definição do recipiente. Se acontecer de renomear o serviço no contêiner, é preciso encontrar essa referência e atualizá-la.

E o que é pior, essa dependência é escondida: à primeira vista do exterior, o controlador depende apenas de uma abstração de um recipiente. Mas como um desenvolvedor, você deve ter o conhecimento de como os serviços são nomeados no contêiner, e que um serviço chamado authenticator é, de fato, um exemplo de Authenticator. Seu novo colega tem que aprender tudo isso. Desnecessáriamente.

Por sorte, podemos recorrer a um identificador muito mais natural: o tipo de serviço. Afinal de contas, é tudo o que lhe interessa como desenvolvedor. Você não precisa saber que cadeia aleatória está associada ao serviço no contêiner. Acredito que este código é muito mais fácil de escrever e ler:

final class SignInController extends Best\Framework\Controller
{
    public function __construct(
        private ContainerInterface $container,
    ) {}

    public function action(string $username, string $password): Best\Framework\Response
    {
        $authenticator = $this->container->get(Authenticator::class);
        //...
    }
}

Infelizmente, ainda não domamos as chamas. Nem um pouquinho. A questão maior é que você está rebaixando o recipiente para o papel de localizador de serviços, que é um enorme anti-padrão. É como trazer alguém a geladeira inteira para que ele possa ir buscar um único lanche – é muito mais razoável conseguir apenas o lanche.

Mais uma vez, a injeção de dependência é sobre transparência, e este controlador ainda não é transparente sobre suas dependências. A dependência de um autenticador é totalmente escondida do mundo exterior, por trás da dependência do contêiner. Isto torna o código mais difícil de ser lido. Ou uso. Ou testar! Zombar do autenticador em um teste de unidade agora exige que você crie um recipiente inteiro ao redor dele.

E, a propósito, o controlador ainda depende da definição do contêiner, e o faz de uma forma bastante ruim. Se o serviço de autenticação não existir no contêiner, o código não falha até o método action(), que é um feedback bastante tardio.

Cozinhando algo delicioso

Para ser justo, ninguém pode realmente culpá-lo por chegar a este beco sem saída. Afinal de contas, você acabou de seguir a interface projetada e comprovada por desenvolvedores inteligentes. O problema é que todos os recipientes de injeção de dependência também são, por definição, localizadores de serviços, e acontece que o padrão é realmente a única interface comum entre eles. Mas isso não significa que você deveria usá-los como localizadores de serviços. Na verdade, o próprio PSR adverte sobre isso.

É assim que você pode usar um recipiente DI como um bom servo:

final class SignInController extends Best\Framework\Controller
{
    public function __construct(
        private Authenticator $authenticator,
    ) {}

    public function action(string $username, string $password): Best\Framework\Response
    {
        $areCredentialsValid = $this->authenticator->authenticate($username, $password);
        //...
    }
}

O controlador declara a dependência explicitamente, de forma clara e transparente no construtor. As dependências não estão mais escondidas espalhadas em torno da classe. Elas também são aplicadas: o recipiente não é capaz de criar uma instância de SignInController sem fornecer o necessário Authenticator. Se não houver um autenticador no contêiner, a execução falha cedo, não no método action(). Testar esta classe também se tornou muito mais fácil, pois basta zombar do serviço de autenticador sem nenhuma placa de caldeira do contêiner.

E há um último detalhe minúsculo, mas muito importante: nós nos infiltramos nas informações sobre o tipo de serviço. O fato de ser uma instância do Authenticator – anteriormente implícita e desconhecida pela IDE, ferramentas de análise estática, ou mesmo um desenvolvedor sem conhecimento da definição do recipiente – está agora estaticamente esculpida na dica de tipo do parâmetro promovido.

O único passo que resta é ensinar ao recipiente como criar também o controlador:

final class Container implements ContainerInterface
{
    private array $factories = [];
    public function __construct(array $parameters)
    {
        $this->factories[SignInController::class] = fn() => new SignInController($this->get(Authenticator::class));
        $this->factories[Authenticator::class] = fn() => new Authenticator($this->get(UserRepository::class));
        $this->factories[UserRepository::class] = fn() => new UserRepository($this->get(Connection::class));
        $this->factories[Connection::class] = fn() => new Connection($parameters['database']);
    }

    public function get(string $id): mixed { /* . . . */ }
    public function has(string $id): bool { /* . . . */ }
}

Você pode notar que o contêiner ainda usa internamente a abordagem de localização de serviços. Mas tudo bem, desde que esteja contido (trocadilho pretendido). O único lugar fora do contêiner onde é aceitável chamar o método get, é no index.php, no ponto de entrada da aplicação onde você precisa criar o próprio contêiner e depois buscar e executar a aplicação:

$container = bootstrap();

$application = $container->get(Best\Framework\Application::class);
$application->run();

A jóia escondida

Mas não vamos parar por aí, permitam-me levar a declaração mais longe: o lugar somente onde é aceitável chamar o método get, está no ponto de entrada.

O código do recipiente é apenas fiação, são instruções de montagem. Não é um código executivo. Não é importante, de certa forma. Embora sim, é crucial para a aplicação, isso é apenas do ponto de vista do desenvolvedor. Não traz realmente nenhum valor direto ao usuário, e deve ser tratado com isso em mente.

Dê uma olhada novamente no container:

final class Container implements ContainerInterface
{
    private array $factories = [];
    public function __construct(array $parameters)
    {
        $this->factories[SignInController::class] = fn() => new SignInController($this->get(Authenticator::class));
        $this->factories[Authenticator::class] = fn() => new Authenticator($this->get(UserRepository::class));
        $this->factories[UserRepository::class] = fn() => new UserRepository($this->get(Connection::class));
        $this->factories[Connection::class] = fn() => new Connection($parameters['database']);
    }

    public function get(string $id): mixed { /* . . . */ }
    public function has(string $id): bool { /* . . . */ }
}

Isto cobre apenas um segmento muito pequeno e simples da aplicação. À medida que a aplicação cresce, a escrita à mão do recipiente fica incrivelmente entediante. Como eu disse antes, o recipiente é apenas um manual de montagem – mas é complicado demais, com muitas páginas, incontáveis referências cruzadas e muitas advertências de pequenas impressões. Queremos transformá-lo em um manual estilo IKEA, gráfico, conciso e com ilustrações de pessoas sorrindo quando colocam o ÅUTHENTICATÖR no tapete durante a montagem, para que ele não se quebre.

É aqui que a Nette Framework entra em jogo.

A Nette Framework solução DI utiliza Neon, um formato de arquivo de configuração similar ao YAML, mas em esteróides. É assim que você definiria o mesmo recipiente, usando a configuração do Neon:

services:
    - SignInController
    - Authenticator
    - UserRepository
    - Connection(%database%)

Deixe-me ressaltar duas coisas dignas de nota: primeiro, a lista de serviços é verdadeiramente uma lista, não um mapa de hash – não há chaves, não há identificadores artificiais de serviços. Não há authenticator, e também não há Authenticator::class. Segundo, não é necessário listar explicitamente nenhuma dependência em nenhum lugar, exceto os parâmetros de conexão ao banco de dados.

Isso porque a Nette Framework se baseia na fiação automática. Lembra-se como, graças à injeção de dependência, fomos capazes de expressar o tipo de dependência em uma dica nativa? O recipiente DI usa essa informação, de modo que quando você precisa de uma instância de Authenticator, ele ignora completamente qualquer nome e encontra o serviço correto apenas por seu tipo.

Você pode argumentar que o cabeamento automático não é uma característica única. E você estaria certo. O que torna o container da Nette Framework única é a utilização do sistema do tipo PHP, enquanto em muitas outras frameworks, a auto-cablagem ainda é construída sobre nomes de serviços internamente. Há cenários em que outros contêineres ficam aquém das expectativas. É assim que se define o serviço de autenticação no contêiner DI da Symfony usando YAML:

services:
  Authenticator: ~

A seção services é um mapa hash e o bit Authenticator é um identificador de serviço. O til significa null em YAML, que Symfony interpreta como “use o identificador de serviço como seu tipo”.

Mas logo, os requisitos comerciais mudam e você precisa apoiar a autenticação através do LDAP, além de uma busca em banco de dados local. Como primeiro passo, você transforma a classe Authenticator em uma interface e extrai a implementação original em uma classe LocalAuthenticator:

services:
  LocalAuthenticator: ~

De repente, a Symfony está sem pistas. Isso porque a Symfony trabalha com nomes de serviços em vez de tipos. O controlador ainda confia corretamente na abstração e lista a interface Authenticator como sua dependência, mas não há nenhum serviço nome Authenticator no contêiner. Você precisa dar uma dica à Symfony, por exemplo, usando um nome do serviço:

services:
  LocalAuthenticator: ~
  Authenticator: '@LocalAuthenticator'

A Nette Framework, por outro lado, não precisa de nomes de serviço ou dicas. Não obriga você a duplicar na configuração as informações já expressas em código (através da cláusula implements ). Ele fica bem em cima do sistema do tipo PHP. Ele sabe que LocalAuthenticator é do tipo Authenticator, e desde que seja o único serviço que implementa a interface, ele felizmente faz a auto-conexão onde a interface é solicitada, dada apenas esta linha de configuração:

services:
    - LocalAuthenticator

Admito que se você não estiver familiarizado com a fiação automática, pode parecer um pouco mágico e você pode precisar de algum tempo para aprender a confiar nela. Felizmente, ele funciona de forma transparente e determinística: quando o contêiner não consegue resolver as dependências sem ambigüidade, ele lança uma exceção de tempo de compilação que o ajuda a resolver a situação. Dessa forma, você pode ter duas implementações diferentes e ainda estar em bom controle de onde cada uma delas é utilizada.

Como um todo, o cabeamento automático coloca menos carga cognitiva sobre você como desenvolvedor. Afinal, você só se preocupa com tipos e abstrações, então por que um contêiner DI deve forçá-lo a se preocupar também com implementações e identificadores de serviço? Mais importante ainda, por que você deve se preocupar com algum container em primeiro lugar? No espírito da injeção de dependência, você quer ser capaz de apenas declarar as dependências e ser problema de outra pessoa fornecê-las. Você quer se concentrar totalmente no código de aplicação e esquecer a fiação. E o DI da Nette Framework permite que você faça isso.

A meu ver, isto faz da Nette Framework a melhor solução de DI no mundo PHP. Ela lhe dá um recipiente que é confiável e impõe bons padrões arquitetônicos, mas ao mesmo tempo é tão fácil de configurar e manter que você não precisa pensar nisso de forma alguma.

Espero que este posto tenha conseguido aguçar sua curiosidade. Não deixe de conferir o Github repositório e o docs – espero que você saiba que só lhe mostrei a ponta do iceberg e que o pacote inteiro é muito mais poderoso.