Ao construir aplicativos PHP complexos, podemos contar com injeção de dependência e contêineres de serviço para gerenciar a instanciação dos objetos, ou “serviços”, no aplicativo.

Existem várias bibliotecas de injeção de dependência que atendem ao PSR-11 , a recomendação padrão do PHP que descreve o contrato para uma “interface de contêiner”:

Com 3.4K estrelas no GitHub, Symfony’s DependencyInjection é um passo acima de bibliotecas semelhantes. É extremamente poderoso, mas simples de usar. Uma vez que a lógica de como todos os serviços devem ser inicializados pode ser gerada e despejada como um arquivo PHP, é rápido para ser executado na produção. Ele pode ser configurado para atender a PHP e YAML. E é de fácil compreensão porque é apoiado por extensa documentação .

Usar contêineres de serviço já é útil para gerenciar aplicativos complexos. Tão importante quanto, os contêineres de serviço diminuem a necessidade de desenvolvedores externos produzirem código para nossos aplicativos.

Por exemplo, nosso aplicativo PHP pode ser extensível por meio de módulos, e desenvolvedores terceirizados podem codificar suas próprias extensões. Ao usar um contêiner de serviço, tornamos mais fácil para eles injetar seus serviços em nosso aplicativo, mesmo se eles não tiverem um conhecimento profundo de como nosso aplicativo funciona. Isso porque podemos programar regras para definir como o contêiner de serviço inicializa os serviços e automatiza esse processo.

Essa automação se traduz em trabalho que os desenvolvedores não precisam mais fazer. Como consequência, eles não precisarão entender os detalhes internos essenciais de como o serviço é inicializado; que é cuidado pelo contêiner de serviço.

Embora os desenvolvedores ainda precisem entender os conceitos por trás de injeção de dependência e serviços de contêiner, usando a biblioteca DependencyInjection, podemos simplesmente direcioná-los para Documentação do Symfony sobre o assunto . Reduzir a quantidade de documentação que precisamos manter nos deixa mais felizes e libera tempo e recursos para trabalhar em nosso código.

Neste artigo, veremos alguns exemplos de como usar a biblioteca DependencyInjection para tornar um aplicativo PHP mais extensível.

Trabalhando com passes de compilador

Passes do compilador são o mecanismo da biblioteca para modificar como os serviços no contêiner são inicializados e chamados logo antes do contêiner de serviço é compilado .

Um objeto de passagem do compilador deve implementar CompilerPassInterface :

> use Symfony \ Component \ DependencyInjection \ Compiler \ CompilerPassInterface;
use Symfony \ Component \ DependencyInjection \ ContainerBuilder; classe OurCustomPass implementa CompilerPassInterface
{ processo de função pública (ContainerBuilder $ container) { //... fazer algo durante a compilação }
}

Para registrá-lo em nosso aplicativo, fazemos o seguinte:

 use Symfony \ Component \ DependencyInjection \ ContainerBuilder; $ containerBuilder=new ContainerBuilder ();
$ containerBuilder-> addCompilerPass (new OurCustomPass ());

Podemos injetar como muitas passagens do compilador conforme precisamos :

//Injetar todas as passagens do compilador
foreach ($ compilerPasses as $ compilerPass) { $ containerBuilder-> addCompilerPass ($ compilerPass);
}
//Compila o contêiner
$ containerBuilder-> compile ();

Inicializando serviços automaticamente

Por meio de uma passagem de compilador, podemos inicializar automaticamente os serviços de um certo tipo-por exemplo, qualquer classe que se estende de uma certa classe, implementa certas interfaces, tem um certo tag de serviço atribuída à sua definição ou algum outro comportamento personalizado.

Vejamos um exemplo. Faremos nosso aplicativo PHP inicializar automaticamente qualquer objeto que implemente AutomaticallyInstantiatedServiceInterface invocando seu método initialize :

interface

 AutomaticallyInstantiatedServiceInterface
{ public function initialize (): void;
}

Podemos então criará um passo do compilador que irá iterar a lista de todos os serviços definidos no contêiner e identificar os serviços que implementam AutomaticallyInstantiatedServiceInterface :

 classe AutomaticallyInstantiateServiceCustomPass implementa CompilerPassInterface
{ processo de função pública (ContainerBuilder $ container) { $ settings=$ container-> getDefinitions (); foreach ($ settings as $ definitionID=> $ definition) { $ definitionClass=$ definition-> getClass (); if ($ definitionClass===null ||! is_a ($ definitionClass, AutomaticallyInstantiatedServiceInterface:: class, true)) { Prosseguir; } //$ definition é um AutomaticallyInstantiatedServiceInterface //Faça algo com isso //... } }
}

A seguir, criaremos um serviço chamado ServiceInstantiatorInterface , que será encarregado de inicializar os serviços identificados . Com o método addService , ele coletará todos os serviços a serem inicializados e seu método initializeServices será eventualmente invocado pelo aplicativo PHP:

 interface ServiceInstantiatorInterface
{ public function addService (AutomaticallyInstantiatedServiceInterface $ service): void; função pública initializeServices (): void;
}

A implementação para este serviço está disponível em GitHub :

 classe ServiceInstantiator implementa ServiceInstantiatorInterface
{ /** * @var AutomaticallyInstantiatedServiceInterface [] */ array protegido $ services=[]; public function addService (AutomaticallyInstantiatedServiceInterface $ service): void { $ this-> serviços []=$ serviço; } public function initializeServices (): void { foreach ($ this-> services as $ service) { $ service-> initialize (); } }
}

Agora podemos completar o código para a passagem do compilador acima, injetando todos os serviços identificados no serviço ServiceInstantiatorInterface :

 classe AutomaticallyInstantiateServiceCustomPass implementa CompilerPassInterface
{ processo de função pública (ContainerBuilder $ container) { $ serviceInstantiatorDefinition=$ container-> getDefinition (ServiceInstantiatorInterface:: class); $ settings=$ container-> getDefinitions (); foreach ($ settings as $ definitionID=> $ definition) { $ definitionClass=$ definition-> getClass (); if ($ definitionClass===null) { Prosseguir; } if (! is_a ($ definitionClass, AutomaticallyInstantiatedServiceInterface:: class, true)) { Prosseguir; } //$ definition é um AutomaticallyInstantiatedServiceInterface //Faça algo com isso $ serviceInstantiatorDefinition-> addMethodCall ( 'addService', [nova referência ($ definitionID)] ); } }
}

Sendo um serviço em si, a definição de ServiceInstantiatorInterface também é encontrada no contêiner de serviço. Por isso, para obter uma referência a este serviço, devemos fazer:

 $ serviceInstantiatorDefinition=$ container-> getDefinition (ServiceInstantiatorInterface:: class);

Não estamos trabalhando com os objetos/serviços instanciados porque ainda não os temos. Em vez disso, estamos lidando com as definições dos serviços no contêiner. É também por isso que, para injetar um serviço em outro serviço, não podemos fazer isso:

 $ serviceInstantiator-> addService (new $ definitionClass ());

Mas deve fazer isso em vez disso:

 $ serviceInstantiatorDefinition-> addMethodCall ( 'addService', [nova referência ($ definitionID)]
);

O aplicativo PHP deve acionar a inicialização dos serviços ao inicializar:

 $ serviceInstantiator-> initializeServices ();

Finalmente, implementamos os serviços que precisam ser inicializados automaticamente:

 AutomaticallyInstantiatedServiceInterface 

Neste exemplo, nosso aplicativo usa os serviços SchemaConfiguratorExecuter . A lógica de inicialização já é satisfeita por sua classe ancestral, AbstractSchemaConfiguratorExecuter , assim:

 classe abstrata AbstractSchemaConfiguratorExecuter implementa AutomaticallyInstantiatedServiceInterface
{ public function initialize (): void { if ($ customPostID=$ this-> getCustomPostID ()) { $ schemaConfigurator=$ this-> getSchemaConfigurator (); $ schemaConfigurator-> executeSchemaConfiguration ($ customPostID); } } /** * Forneça o ID da postagem personalizada que contém o bloco de configuração do esquema */ função protegida abstrata getCustomPostID ():? int; /** * Inicialize a configuração dos serviços antes da execução da consulta GraphQL */ função protegida abstrata getSchemaConfigurator (): SchemaConfiguratorInterface;
}

Agora, qualquer desenvolvedor terceirizado que deseja criar seu próprio serviço SchemaConfiguratorExecuter precisa apenas criar uma classe herdada de AbstractSchemaConfiguratorExecuter , satisfazer os métodos abstratos e definir a classe na configuração do contêiner de serviço.

O contêiner de serviço se encarregará de instanciar e inicializar a classe, conforme necessário no ciclo de vida do aplicativo.

Registrando, mas não inicializando serviços

Em algumas situações, podemos desejar desativar um serviço. Em nosso aplicativo PHP de exemplo, um servidor GraphQL para WordPress permite que os usuários removam tipos do esquema GraphQL. Se as postagens do blog no site não mostrarem comentários, podemos pular a adição do tipo Comentário ao esquema.

CommentTypeResolver é o serviço que adiciona o tipo Comentário ao esquema. Para pular a adição desse tipo ao esquema, tudo o que precisamos fazer é não registrar este serviço no contêiner.

Mas, ao fazer isso, encontramos um problema: se qualquer outro serviço injetou CommentTypeResolver nele (como este ) falharia porque DependencyInjection não sabe como resolver esse serviço e gerará um erro:

 Erro fatal: Symfony \ Component \ DependencyInjection \ Exception \ RuntimeException: não é possível autowire serviço"GraphQLAPI \ GraphQLAPI \ ModuleResolvers \ SchemaTypeModuleResolver": argumento"$ commentTypeResolver"do método"__construct ()"faz referência à classe PoPSReschema \ \ CommentTypeResolver", mas esse serviço não existe. em/app/wordpress/wp-content/plugins/graphql-api/vendor/symfony/dependency-injection/Compiler/DefinitionErrorExceptionPass.php:54

Isso significa que CommentTypeResolver e todos os outros serviços devem sempre ser registrados no serviço de contêiner-isto é, a menos que tenhamos certeza absoluta de que não será referenciado por algum outro serviço. Conforme explicado abaixo, alguns serviços em nosso aplicativo de exemplo estão disponíveis apenas no lado do administrador, portanto, podemos pular o registro para o lado voltado para o usuário.

A solução para remover o tipo Comentário do esquema deve ser instanciar o serviço, que deve ser livre de efeitos colaterais, mas não inicializá-lo, onde os efeitos colaterais acontecem.

Para conseguir isso, podemos usar o autoconfigure property ao registrar o serviço para indicar que o serviço deve ser inicializado:

 serviços: PoPSchema \ Comments \ TypeResolvers \ CommentTypeResolver: classe: ~ autoconfigure: true

E nós podemos atualize a senha do compilador para injetar apenas esses serviços com autoconfigure: true em ServiceInstantiatorInterface :

 classe AutomaticallyInstantiateServiceCustomPass implementa CompilerPassInterface
{ processo de função pública (ContainerBuilder $ container) { //... foreach ($ settings as $ definitionID=> $ definition) { //... if ($ definition-> isAutoconfigured ()) { //$ definition é um AutomaticallyInstantiatedServiceInterface //Faça algo com isso $ serviceInstantiatorDefinition-> addMethodCall ( 'addService', [nova referência ($ definitionID)] ); } } }
}

Indicando inicialização de serviço condicional

A solução acima funciona, mas tem um grande problema: definir se o serviço deve ser inicializado deve ser definido no arquivo de definição de serviço, que é acessado durante o tempo de compilação do contêiner-ou seja, antes de começarmos a usar os serviços em nosso aplicativo. Também podemos desejar desativar o serviço com base no valor do tempo de execução em alguns casos, como quando o usuário administrador desativa o tipo Comentário por meio das configurações do aplicativo, que são salvas no banco de dados.

Para resolver esse problema, podemos fazer com que o próprio serviço indique se ele deve ser inicializado. Para isso, adicionamos o método isServiceEnabled à sua interface:

 interface AutomaticallyInstantiatedServiceInterface
{ //... função pública isServiceEnabled (): bool;
}

Por exemplo, um serviço em nosso aplicativo PHP de exemplo implementa este método assim :

 classe abstrata AbstractScript implementa AutomaticallyInstantiatedServiceInterface
{ /** * Só habilitar o serviço, se o módulo correspondente também estiver habilitado */ public function isServiceEnabled (): bool { $ enableModule=$ this-> getEnablingModule (); return $ this-> moduleRegistry-> isModuleEnabled ($ enableModule); }
}

Finalmente, o serviço ServiceInstantiatorInterface pode identificar os serviços que devem ser inicializados:

 classe ServiceInstantiator implementa ServiceInstantiatorInterface
{ //... public function initializeServices (): void { $ enabledServices=array_filter ( $ this-> serviços, fn ($ service)=> $ service-> isServiceEnabled () ); foreach ($ enabledServices as $ service) { $ service-> initialize (); } }
}

Dessa forma, podemos pular a inicialização de um serviço não apenas ao configurar o contêiner de serviço, mas também dinamicamente ao executar o aplicativo.

Registrando diferentes serviços de contêiner para diferentes comportamentos

Os aplicativos PHP não estão restritos a apenas um contêiner de serviço. Por exemplo, o aplicativo pode se comportar de maneira diferente dependendo de uma determinada condição, como estar no lado do administrador ou voltado para o usuário. Isso significa que, dependendo do contexto, o aplicativo precisará registrar diferentes conjuntos de serviços.

Para conseguir isso, podemos dividir o arquivo de configuração services.yaml em vários subarquivos e registrar cada um deles sempre que necessário.

Esta definição para services.yaml deve sempre ser carregada porque registrará todos os serviços encontrados em Services/:

 serviços: _defaults: public: true autowire: true GraphQLAPI \ GraphQLAPI \ Services \: recurso:'src/Services/*'

E este outra definição para Conditional/Admin/services.yaml é condicional, carregada apenas quando no lado do administrador, registrando todos os serviços encontrados em Condicional/Admin/Serviços/:

 serviços: _defaults: public: true autowire: true GraphQLAPI \ GraphQLAPI \ Conditional \ Admin \ Services \: recurso:'src/Conditional/Admin/Services/*'

O código a seguir sempre registra o primeiro arquivo, mas só registra o segundo quando no lado do administrador:

 self:: initServices ('services.yaml');
if (is_admin ()) { self:: initServices ('Conditional/Admin/services.yaml');
}

Agora devemos lembrar que, para produção, DependencyInjection irá despejar o contêiner de serviço compilado em um arquivo PHP. Também precisamos produzir dois dumps diferentes e carregue o correspondente para cada contexto :

 public function getCachedContainerFileName (): string
{ $ fileName='container_cache'; if (is_admin ()) { $ fileName.='_admin'; } retornar $ fileName.'.php';
}

Estabelecendo convenção sobre configuração

Convenção sobre configuração é a arte de estabelecer normas para que um projeto aplique um comportamento padrão que não apenas funcione, mas também reduza a quantidade de configuração necessária para o desenvolvedor.

As implementações desta estratégia podem exigir que coloquemos certos arquivos em certas pastas. Por exemplo, para instanciar objetos EventListener para alguma estrutura, podemos ser solicitados a colocar todos os arquivos correspondentes em uma pasta EventListeners ou atribuir a ela app \ EventListeners namespace.

Observe como as passagens do compilador podem remover tal requisito. Para identificar um serviço e tratá-lo de maneira especial, o serviço deve estender alguma classe, implementar alguma interface, receber alguma etiqueta de serviço ou exibir algum outro comportamento personalizado-independentemente de onde esteja localizado.

Graças às passagens do compilador, nosso aplicativo PHP pode naturalmente fornecer convenção sobre configuração para desenvolvedores que criam extensões, reduzindo seus inconvenientes.

Exposição de informações sobre serviços por meio da estrutura de pastas

Embora não precisemos colocar arquivos em nenhuma pasta específica, ainda podemos projetar uma estrutura lógica para o aplicativo se ele servir a algum propósito diferente de inicializar os serviços.

Em nosso aplicativo PHP de exemplo, vamos fazer com que a estrutura da pasta transmita quais serviços estão disponíveis, se eles devem ser definidos implicitamente no contêiner e em que contexto eles serão adicionados ao contêiner.

Para isso, usaremos a seguinte estrutura :

  • Todas as fachadas para acessar um serviço específico vão em Facades/
  • Todos os serviços que são sempre inicializados vão para Services/
  • Todos os serviços condicionais, que podem ou não ser inicializados dependendo do contexto, vá em Conditional/{ConditionName}/Services
  • Todas as implementações de serviços substituindo a implementação padrão, fornecida por alguns pacotes, vá em Overrides/Services
  • Todos os serviços que são acessados ​​por meio de seu contrato em vez de diretamente como uma implementação, como o serviço ServiceInstantiatorInterface , podem ser colocados em qualquer lugar desde sua definição no contêiner deve ser explícito :
 serviços:
_defaults:
public: true
autowire: truePoP \ Root \ Container \ ServiceInstantiatorInterface:
classe: \ PoP \ Root \ Container \ ServiceInstantiator

A estrutura que usamos depende inteiramente de nós, com base nas necessidades de nosso aplicativo.

Conclusão

Criar uma arquitetura robusta para um aplicativo PHP, mesmo quando for apenas para nossa própria equipe de desenvolvimento, já é um desafio. Para essas situações, usar injeção de dependência e serviços de contêiner pode simplificar muito a tarefa.

Além disso, se também precisamos permitir que terceiros-que podem não entender totalmente como o aplicativo funciona-forneçam extensões, o desafio fica maior. Ao usar o componente DependencyInjection, podemos criar passagens do compilador para configurar e inicializar o aplicativo automaticamente, removendo essa necessidade do desenvolvedor.

A postagem Construindo aplicativos PHP extensíveis com Symfony DI apareceu primeiro no LogRocket Blog .