Serviços não precisam de nomes
Gosto da solução do Nette Framework para injeção de dependência. Eu realmente a amo. Este artigo está aqui para compartilhar essa paixão e explicar por que acho que é a melhor solução de DI no ecossistema PHP atual.

(Este post foi originalmente publicado no blog do autor.)
O desenvolvimento de software é um processo iterativo infinito de abstração. Encontramos abstrações adequadas do mundo real na modelagem de domínio. Na programação orientada a objetos, usamos abstrações para descrever e impor contratos entre diferentes atores no sistema. Introduzimos novas classes no sistema para encapsular responsabilidades e definir seus limites, e então usamos composição para criar todo o sistema.
Estou falando sobre o impulso de extrair a lógica de autenticação do seguinte controller:
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);
}
}
Você provavelmente reconhecerá que a verificação de credenciais não pertence ali. O controller não tem a responsabilidade de determinar quais credenciais são válidas – de acordo com o princípio da responsabilidade única, o controller deve ter apenas um único motivo para mudar, e esse motivo deve estar dentro da interface do usuário da aplicação, não no processo de autenticação.
Vamos seguir o caminho óbvio e extrair a condição para a classe
Authenticator
:
final class Authenticator
{
public function authenticate(string $username, string $password): bool
{
return $username === 'admin' && $password === 'p4ssw0rd!';
}
}
Agora basta delegarmos do controller para este autenticador. Criamos o autenticador dependência do controller e o controller de repente precisa obtê-lo de algum lugar:
final class SignInController extends Best\Framework\Controller
{
public function action(string $username, string $password): Best\Framework\Response
{
$authenticator = new Authenticator(); // <== fácil, apenas crio 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 apenas até que uma autenticação
mais robusta seja implementada, o que exigirá que o autenticador consulte uma
tabela de banco de dados de usuários. De repente, o Authenticator
tem sua própria dependência, digamos UserRepository
, que por sua
vez depende de uma instância de Connection
, que depende de
parâmetros do ambiente específico. Isso escalou rapidamente!
Criar instâncias manualmente em todos os lugares não é uma maneira
sustentável de gerenciar dependências. É por isso que temos o padrão de
injeção de dependência, que permite ao controller apenas declarar a
dependência do Authenticator
e deixar para outra pessoa fornecer a
instância real. E essa outra pessoa é chamada de contêiner de
injeção de dependência.
O contêiner de injeção de dependência é o arquiteto principal da aplicação – ele sabe como resolver as dependências de qualquer serviço no sistema e é responsável por criá-los. Os contêineres de DI são tão comuns hoje que quase todo framework web maior tem sua própria implementação de contêiner, e existem até pacotes separados dedicados à injeção de dependência, por exemplo, PHP-DI.
Caindo na armadilha
A quantidade de opções acabou motivando um grupo de desenvolvedores a buscar uma abstração que as tornasse interoperáveis. Uma interface comum foi aprimorada com o tempo e finalmente proposta para o PHP-FIG na seguinte forma:
interface ContainerInterface
{
public function get(string $id): mixed;
public function has(string $id): bool;
}
Esta interface ilustra uma propriedade muito importante dos contêineres de DI: **Eles são um bom servo, mas podem facilmente se tornar um mau mestre. São imensamente úteis se você souber como usá-los, mas se os usar incorretamente, eles vão te queimar. Considere o seguinte contêiner:
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é agora, tudo bem. A implementação parece boa pelos padrões que estabelecemos: ela realmente sabe como criar cada serviço na aplicação e resolve recursivamente suas dependências. Tudo é gerenciado em um só lugar e o contêiner até aceita parâmetros, então a conexão ao banco de dados é facilmente configurável. Legal!
Mas agora que você vê apenas os dois métodos de
ContainerInterface
, talvez fique tentado a usar o contêiner
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 cair na armadilha. Em outras palavras, o contêiner se tornou um mestre mau. Por que isso acontece?
Primeiro, você está confiando em um identificador de serviço arbitrário:
'authenticator'
. A injeção de dependência trata de ser
transparente sobre suas dependências, e usar um identificador artificial vai
diretamente contra essa noção: faz com que o código dependa silenciosamente
da definição do contêiner. Se um serviço for renomeado no contêiner, você
terá que encontrar essa referência e atualizá-la.
E o que é pior, essa dependência está oculta: à primeira vista, de fora,
o controller depende apenas da abstração do contêiner. Mas como
desenvolvedor, você precisa ter conhecimento de como os serviços são
nomeados no contêiner e que o serviço chamado authenticator
é,
na verdade, uma instância de Authenticator
. Tudo isso seu novo
colega terá que aprender. Desnecessariamente.
Felizmente, podemos recorrer a um identificador muito mais natural: o tipo do serviço. Afinal, isso é a única coisa que interessa a você como desenvolvedor. Você não precisa saber qual string aleatória está atribuída ao serviço no contêiner. Acredito que este código é muito mais simples 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 apagamos as chamas. Nem um pouco. O problema maior é que você está humildemente colocando o contêiner no papel de localizador de serviços, o que é um enorme antipadrão. É como trazer a geladeira inteira para alguém pegar um lanche – muito mais sensato é trazer apenas o lanche.
Novamente, a injeção de dependência trata de transparência, e este controller ainda não é transparente sobre suas dependências. A dependência do autenticador está completamente oculta do mundo exterior por trás da dependência do contêiner. Isso torna o código mais difícil de ler. Ou usar. Ou testar! Mockar o autenticador em um teste unitário agora exige que você crie um contêiner inteiro ao redor dele.
E, a propósito, o controller ainda depende da definição do contêiner, e
de uma maneira bastante ruim. Se o serviço autenticador não existir no
contêiner, o código falhará apenas no método action()
, o que
é um feedback bastante tardio.
Cozinhando algo saboroso
Para ser justo, ninguém pode culpá-lo por ter chegado a este beco sem saída. Afinal, você estava apenas seguindo a interface projetada e comprovada por desenvolvedores inteligentes. A questão é que todos os contêineres de injeção de dependência são, por definição, também 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, a própria especificação PSR avisa contra isso.
É assim que você pode usar o contêiner de DI como um bom serviço:
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);
//...
}
}
No construtor, a dependência é declarada explicitamente, de forma clara e
transparente. As dependências não estão mais ocultas espalhadas pela classe.
Elas também são impostas: o contêiner não é capaz de criar uma instância
de SignInController
sem fornecer o Authenticator
necessário. Se não houver autenticador no contêiner, a execução falhará
prematuramente, não no método action()
. Testar esta classe
também se tornou muito mais fácil, pois você só precisa mockar o serviço
autenticador sem qualquer boiler plate do contêiner.
E mais um detalhe pequeno, mas muito importante: introduzimos a informação
sobre o tipo do serviço. O fato de ser uma instância de
Authenticator
– anteriormente implícito e desconhecido para
o IDE, ferramentas de análise estática ou mesmo para um desenvolvedor
desconhecedor da definição do contêiner – agora está estaticamente
gravado no type hint do parâmetro promovido.
O único passo que resta é ensinar o contêiner a criar também o controller:
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 ter notado que o contêiner ainda usa internamente a abordagem de
localizador de serviços. No entanto, isso não importa, desde que esteja
contido (trocadilho intencional). O único lugar fora do contêiner onde a
chamada do método get
é permitida é em index.php
,
no ponto de entrada da aplicação, onde é necessário criar o próprio
contêiner e, em seguida, carregar e executar a aplicação:
$container = bootstrap();
$application = $container->get(Best\Framework\Application::class);
$application->run();
A joia escondida
Mas não vamos parar por aí, permita-me levar esta afirmação adiante:
o único lugar onde a chamada do método get
é permitida
é o ponto de entrada.
O código do contêiner é apenas fiação, são instruções de montagem. Não é código executável. De certa forma, não é importante. Embora sim, seja crucial para a aplicação, mas apenas do ponto de vista do desenvolvedor. Na verdade, não traz nenhum valor direto para o usuário e deve ser tratado com essa consideração.
Olhe novamente para o contêiner:
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 { /* . . . */ }
}
Isso se refere apenas a um segmento muito pequeno e simples da aplicação. À medida que a aplicação cresce, escrever manualmente o contêiner se torna incrivelmente tedioso. Como eu disse, o contêiner é apenas um manual de montagem – mas é muito complexo, tem muitas páginas, inúmeras referências cruzadas e muitos avisos escritos em letras pequenas. Queremos transformá-lo em um manual estilo IKEA, gráfico, conciso e com ilustrações de pessoas sorrindo enquanto colocam o ÅUTHENTICATÖR no tapete durante a montagem para não quebrá-lo.
É aqui que entra o Nette Framework.
A solução de DI do Nette Framework utiliza Neon, um formato de arquivo de configuração semelhante ao YAML, mas com esteroides. É assim que você definiria o mesmo contêiner usando a configuração Neon:
services:
- SignInController
- Authenticator
- UserRepository
- Connection(%database%)
Permita-me apontar duas coisas notáveis: primeiro, a lista de serviços é
realmente uma lista, não um mapa hash – não há chaves, nenhum
identificador de serviço artificial. Não existe authenticator
,
nem Authenticator::class
. Segundo, você não precisa declarar
explicitamente nenhuma dependência em lugar nenhum, exceto os parâmetros de
conexão ao banco de dados.
Isso porque o Nette Framework depende do autowiring. Lembra como, graças à
injeção de dependência, pudemos expressar o tipo da dependência com um
typehint nativo? O contêiner de DI utiliza essa informação, então quando
você solicita uma instância de Authenticator
, ele ignora
completamente quaisquer nomes e encontra o serviço correto exclusivamente pelo
seu tipo.
Você pode argumentar que o autowiring não é um recurso exclusivo. E você estaria certo. O que torna o contêiner do Nette Framework único é o uso do sistema de tipos do PHP, enquanto em muitos outros frameworks, o autowiring ainda é internamente construído sobre nomes de serviços. Existem cenários em que outros contêineres ficam para trás. É assim que você definiria o serviço autenticador no contêiner Symfony DI usando a linguagem YAML:
services:
Authenticator: ~
Na seção services
, há um mapa hash e o bit
Authenticator
é o identificador do serviço. O til significa
null
em YAML, que o Symfony interpreta como “use
o identificador do serviço como seu tipo”.
Logo, porém, os requisitos de negócios mudarão e você precisará suportar
a autenticação via LDAP além da busca local no banco de dados. No primeiro
passo, você altera a classe Authenticator
para uma interface e
extrai a implementação original para a classe
LocalAuthenticator
:
services:
LocalAuthenticator: ~
De repente, o Symfony fica perdido. Isso porque o Symfony trabalha com
nomes de serviços em vez de tipos. O controller ainda depende corretamente da
abstração e lista a interface Authenticator
como sua
dependência, mas não há nenhum serviço com o nome
Authenticator
no contêiner. Você precisa dar uma dica ao Symfony,
por exemplo, usando um alias de nome de serviço:
services:
LocalAuthenticator: ~
Authenticator: '@LocalAuthenticator'
O Nette Framework, por outro lado, não precisa de nomes de serviços nem de
dicas. Ele não o força a duplicar na configuração informações que já
estão expressas no código (através da cláusula implements
). Ele
está posicionado diretamente sobre o sistema de tipos do PHP. Ele sabe que
LocalAuthenticator
é do tipo Authenticator
, e
se for o único serviço que implementa essa interface, ele o conectará
automaticamente com prazer onde essa interface for necessária, com base apenas
nesta linha de configuração:
services:
- LocalAuthenticator
Admito que, se você não está familiarizado com o autowiring, pode parecer um pouco mágico e talvez precise de algum tempo para aprender a confiar nele. Felizmente, funciona de forma transparente e determinística: quando o contêiner não consegue resolver inequivocamente as dependências, ele lança uma exceção durante a compilação, o que o ajuda a corrigir a situação. Desta forma, você pode ter duas implementações diferentes e ainda ter um bom controle sobre onde cada uma delas é usada.
No geral, o autowiring impõe uma carga cognitiva menor a você como desenvolvedor. Afinal, você só se preocupa com tipos e abstrações, então por que o contêiner de DI o forçaria a se preocupar também com implementações e identificadores de serviços? E, mais importante, por que você deveria se preocupar com qualquer contêiner? No espírito da injeção de dependência, você quer ter a capacidade de simplesmente declarar dependências e deixar que seja problema de outra pessoa fornecê-las. Você quer se concentrar totalmente no código da aplicação e esquecer a fiação. E é isso que o DI do Nette Framework permite que você faça.
Aos meus olhos, isso torna a solução de DI do Nette Framework a melhor que existe no mundo PHP. Ele fornece um contêiner que é confiável e impõe bons padrões arquiteturais, mas ao mesmo tempo é tão fácil de configurar e manter que você não precisa pensar nele.
Espero que este post tenha conseguido despertar sua curiosidade. Não se esqueça de conferir o repositório no Github e a documentação – talvez você descubra que mostrei apenas a ponta do iceberg e que todo o pacote é muito mais poderoso.
Faça o login para enviar um comentário