Posted on: July 22, 2020 04:29 PM
Posted by: Renato
Views: 948
Recipiente de injeção de dependência do Laravel em profundidade
Laravel’s Dependency Injection Container in Depth
[Laravel] (https://laravel.com/) possui um poderoso contêiner de Inversão de controle (IoC) / Injeção de dependência (DI). Infelizmente, a [documentação oficial] (https://laravel.com/docs/6.x/container) não cobre todas as funcionalidades disponíveis, por isso decidi experimentar e documentar por mim mesmo. O seguinte é baseado no [Laravel 5.4.26] (https://github.com/laravel/framework/tree/6.x/src/Illuminate/Container) - outras versões podem variar.
Introdução à injeção de dependência
Não tentarei explicar os princípios por trás do DI / IoC aqui - se você não estiver familiarizado com eles, poderá ler [O que é injeção de dependência?] (Http://fabien.potencier.org/what-is- dependência-injeção.html) por Fabien Potencier (criador da estrutura [Symfony] (http://symfony.com/)).
Acessando o Container
Existem várias maneiras de acessar a instância do Container * no Laravel, mas a mais simples é chamar o método auxiliar app ()
:
$ container = app ();
Não vou descrever as outras maneiras hoje - em vez disso, quero me concentrar na própria classe Container.
Nota: Se você ler os [documentos oficiais] (https://laravel.com/docs/6.x/container), ele usará $ this-> app
em vez de$ container
.
(* Nos aplicativos Laravel, na verdade, é uma subclasse de Container chamada [Application] (https://github.com/laravel/framework/blob/6.x/src/Illuminate/Foundation/Application.php) (é por isso que o assistente é chamado app ()
), mas para este post vou descrever apenas os métodos [Container] (https://github.com/laravel/framework/blob/6.x/src/Illuminate/Container/Container.php).)
Usando Illuminate \ Container Outside Laravel
Para usar o Container fora do Laravel, [instale-o] (https://packagist.org/packages/illuminate/container) e depois:
use Illuminate \ Container \ Container;
$ container = Container :: getInstance ();
Uso básico
O uso mais simples é digitar o construtor da sua classe com as classes que você deseja injetar:
classe MyClass
{
private $ dependency;
função pública __construct (AnotherClass $ dependency)
{
$ this-> dependency = $ dependency;
}
}
Em vez de usar new MyClass
, use o métodomake ()
do Container:
$ instance = $ container-> make (MyClass :: class);
O contêiner instanciará automaticamente as dependências, portanto isso é funcionalmente equivalente a:
$ instance = new MyClass (new AnotherClass ());
(Exceto que AnotherClass
poderia ter algumas dependências próprias - nesse caso, o Container as instanciaria recursivamente até que não houvesse mais.)
Exemplo prático
Aqui está um exemplo mais prático com base nos [PHP-DI docs] (http://php-di.org/doc/getting-started.html) - separando a funcionalidade de correspondência do registro do usuário:
class Mailer
{
função pública mail ($ destinatário, $ content)
{
// Envia um email para o destinatário
// ...
}
}
class UserManager
{
private $ mailer;
função pública __construct (Mailer $ mailer)
{
$ this-> mailer = $ mailer;
}
registro de função pública ($ email, $ password)
{
// Crie a conta do usuário
// ...
// Envie um email ao usuário para dizer olá!
$ this-> mailer-> mail ($ email, 'Olá e bem-vindo!');
}
}
use Illuminate \ Container \ Container;
$ container = Container :: getInstance ();
$ userManager = $ container-> make (UserManager :: class);
$ userManager-> register ('[email protected] ',' MySuperSecurePassword! ');
Vinculando interfaces a implementações
O Container facilita a codificação para uma interface e instancia uma instância concreta em tempo de execução. Primeiro defina as interfaces:
interface MyInterface {/ * ... * /}
interface AnotherInterface {/ * ... * /}
E declare as classes concretas que implementam essas interfaces. Eles podem depender de outras interfaces (ou classes concretas como antes):
class MyClass implementa MyInterface
{
private $ dependency;
função pública __construct (AnotherInterface $ dependency)
{
$ this-> dependency = $ dependency;
}
}
Use bind ()
para mapear cada interface para uma classe concreta:
$ container-> bind (MyInterface :: class, MyClass :: class);
$ container-> bind (OutraInterface :: classe, OutraClasse :: classe);
Por fim, passe o nome da interface em vez do nome da classe para make ()
:
$ instance = $ container-> make (MyInterface :: class);
Nota: Se você esquecer de vincular uma interface, receberá um erro fatal um pouco enigmático:
Erro fatal: Unchaught ReflectionException: a classe MyInterface não existe`
Isso ocorre porque o contêiner tentará para instanciar a interface (new MyInterface
), que não é uma classe válida.
Exemplo prático
Aqui está um exemplo prático disso - uma camada de cache permutável:
interface Cache
{
public function get ($ key);
função pública put ($ key, $ value);
}
class RedisCache implementa Cache
{
função pública get ($ key) {/ * ... * /}
função pública put ($ key, $ value) {/ * ... * /}
}
class Worker
{
private $ cache;
função pública __construct (Cache $ cache)
{
$ this-> cache = $ cache;
}
public function result ()
{
// Use o cache para algo ...
$ result = $ this-> cache-> get ('worker');
if ($ result === null) {
$ result = do_something_slow ();
$ this-> cache-> put ('trabalhador', $ resultado);
}
retornar $ resultado;
}
}
use Illuminate \ Container \ Container;
$ container = Container :: getInstance ();
$ container-> bind (Cache :: classe, RedisCache :: classe);
$ result = $ container-> make (Worker :: class) -> resultado ();
Vinculação de classes abstratas e concretas
A
associação também pode ser usada com classes abstratas:
```php $ container-> bind (MyAbstract :: class, MyConcreteClass :: class);
Ou para substituir uma classe concreta por uma subclasse:
```php
$ container-> bind (MySQLDatabase :: class, CustomMySQLDatabase :: class);
Ligações personalizadas
Se a classe exigir configuração adicional, você pode passar um fechamento em vez de um nome de classe como o segundo parâmetro para bind ()
:
$ container-> bind (Database :: class, function (Container $ container) {
return novo MySQLDatabase (MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS);
});
Sempre que a interface do banco de dados for necessária, uma nova instância do MySQLDatabase será criada e usada, com os valores de configuração especificados. (Para compartilhar uma única instância, consulte Singletons abaixo.) O fechamento recebe a instância Container como o primeiro parâmetro e pode ser usado para instanciar outras classes, se necessário:
$ container-> bind (Logger :: class, function (Container $ container) {
$ sistema de arquivos = $ container-> make (Sistema de arquivos :: classe);
retornar novo FileLogger ($ filesystem, 'logs / error.log');
});
Um fechamento também pode ser usado para personalizar como uma classe concreta é instanciada:
$ container-> bind (GitHub \ Client :: class, function (Container $ container) {
$ client = new GitHub \ Client;
$ client-> setEnterpriseUrl (GITHUB_HOST);
retorna $ client;
});
Resolvendo
retornos de chamada Em vez de substituir completamente a ligação, você pode usar resolving ()
para registrar um retorno de chamada chamado depois que a ligação é revolvida:
$ container-> resolving (GitHub \ Client :: classe, função ($ client, Container $ container) {
$ client-> setEnterpriseUrl (GITHUB_HOST);
});
Se houver vários retornos de chamada, todos eles serão chamados. Eles também trabalham para interfaces e classes abstratas:
$ container-> resolving (Logger :: class, function (Logger $ logger) {
$ logger-> setLevel ('debug');
});
$ container-> resolving (FileLogger :: classe, função (FileLogger $ logger) {
$ logger-> setFilename ('logs / debug.log');
});
$ container-> bind (Logger :: classe, FileLogger :: classe);
$ logger = $ container-> make (Logger :: classe);
Também é possível adicionar um retorno de chamada que sempre é chamado, não importa qual classe seja resolvida - mas acho que provavelmente é útil apenas para registrar / depurar:
$ container-> resolving (function ($ objeto, Container $ container) {
// ...
});
Estendendo uma classe
Como alternativa, você também pode usar extend ()
para quebrar uma classe e retornar um objeto diferente:
$ container-> extend (APIClient :: class, function ($ client, Container $ container) {
retorna novo APIClientDecorator ($ client);
});
O objeto resultante ainda deve implementar a mesma interface, caso contrário, você receberá um erro ao usar a dica de tipo.
Singletons
Com a ligação automática e o bind ()
, uma nova instância será criada (ou o fechamento será chamado) toda vez que for necessário. Para compartilhar uma única instância, use singleton ()
em vez de bind ()
:
$ container-> singleton (Cache :: class, RedisCache :: class);
Ou com um fechamento:
$ container-> singleton (Database :: class, function (Container $ container) {
retorna novo MySQLDatabase ('localhost', 'testdb', 'user', 'pass') ;
});
Para tornar uma classe concreta um singleton, passe essa classe sem o segundo parâmetro:
$ container-> singleton (MySQLDatabase :: class);
Em cada caso, o objeto singleton será criado na primeira vez que for necessário e, em seguida, reutilizado a cada vez subseqüente. Se você já possui uma instância que deseja reutilizar, use o método instance ()
. Por exemplo, o Laravel usa isso para garantir que a instância Singleton Container seja retornada sempre que for injetada em uma classe:
$ container-> instance (Container :: class, $ container);
# Nomes de ligação arbitrária
Você pode usar qualquer sequência arbitrária em vez de um nome de classe / interface - embora não seja possível usar dicas de tipo para recuperá-la e precisará usarmake ()
:
$ container-> bind ('banco de dados', MySQLDatabase :: class);
$ db = $ container-> make ('banco de dados');
Para suportar simultaneamente uma classe / interface e um nome abreviado, use alias ()
:
$ container-> singleton (Cache :: class, RedisCache :: class);
$ container-> alias (Cache :: classe, 'cache');
$ cache1 = $ container-> make (Cache :: class);
$ cache2 = $ container-> make ('cache');
assert ($ cache1 === $ cache2);
Armazenando valores arbitrários
Você também pode usar o contêiner para armazenar valores arbitrários - por exemplo, dados de configuração:
$ container-> instance ('database.name', 'testdb');
$ db_name = $ container-> make ('nome do banco de dados');
Ele suporta a sintaxe de acesso ao array, o que torna isso mais natural:
$ container ['
$ db_name = $ container ['database.name'];
Quando combinado com as ligações de fechamento, você pode ver por que isso pode ser útil:
$ container-> singleton ('database', function (Container $ container) {
return new MySQLDatabase (
$ container ['database.host' ],
$ container ['database.name'],
$ container ['database.user'],
$ container ['database.pass']
);
});
(O próprio Laravel não usa o contêiner para configuração - ele usa uma classe [Config] separada (https://github.com/laravel/framework/blob/6.x/src/Illuminate/Config/Repository.php) em vez disso - mas [PHP-DI] (http://php-di.org/doc/php-definitions.html#values) faz.)
Dica: A sintaxe da matriz também pode ser usada em vez de make ()
ao instanciar objetos:
$ db = $ container ['database'];
Injeção de Dependência para Funções e Métodos
Até agora, vimos DI para construtores, mas o Laravel também suporta DI para funções arbitrárias:
function do_something (Cache $ cache) {/ * ... * /}
$ result = $ container-> call ('do_something');
Parâmetros adicionais podem ser passados como uma matriz ordenada ou associativa:
função php show_product (Cache $ cache, $ id, $ tab = 'details') {/ * ... * /}
// show_product ($ cache, 1)
$ container-> call ('show_product', [1]);
$ container-> call ('show_product',
// show_product ($ cache, 1, 'spec')
$ container-> call ('show_product', [1, 'spec']);
$ container-> call ('show_product', ['id' => 1, 'tab' => 'especificação']);
Isso pode ser usado para qualquer método que possa ser chamado:
Closures`
$ encerramento = function (Cache $ cache) {/ * ... * /};
$ container-> call ($ fechamento);
Métodos estáticos
classe php SomeClass
{
função estática pública staticMethod (Cache $ cache) {/ * ... * /}
}
$ container-> call ([' SomeClass ',' staticMethod ']);
// ou:
$ container-> call ('SomeClass :: staticMethod' );
Métodos de instância
class PostController
{
índice de função pública (Cache $ cache) {/ * ... * /}
função pública show (Cache $ cache, $ id) {/ * ... * /}
}
$ controller = $ container-> make (PostController :: class);
$ container-> call ([$ controller, 'index']);
$ container-> call ([$ controller, 'show'], ['id' => 1]);
Atalho para chamar métodos de instância
Existe um atalho para instanciar uma classe e chamar um método de uma só vez - use a sintaxe ClassName @ methodName
:
$ container-> call ('PostController @ index ');
$ container-> call ('PostController @ show', ['id' => 4]);
O contêiner é usado para instanciar a classe. Isso significa:
- Dependências são injetadas no construtor (assim como no método).
- Você pode definir a classe como um singleton se desejar que ela seja reutilizada.
- Você pode usar uma interface ou nome arbitrário em vez de uma classe concreta.
Por exemplo, isso funcionará:
class PostController
{
public function __construct (Request $ request) {/ * ... * /}
índice da função pública (Cache $ cache) {/ * ... * /}
}
$ container-> singleton ('post', PostController :: class);
$ container-> call ('post @ index');
Finalmente, você pode passar um “método padrão” como o terceiro parâmetro. Se o primeiro parâmetro for um nome de classe sem método especificado, o método padrão será chamado. O Laravel usa isso para implementar [manipuladores de eventos] (https://laravel.com/docs/6.x/events#registering-events-and-listeners):
$ container-> call (MyEventHandler :: class, $ parameters , 'lidar com');
// Equivalente a:
$ container-> call ('MyEventHandler @ handle', $ parameters);
Ligações de chamada de método O métodobindMethod ()
pode ser usado para substituir uma chamada de método, por exemplo, para passar parâmetros adicionais:
$ container-> bindMethod ('PostController @ index', function ($ controller, $ container) {
$ posts = get_posts (...);
retornar $ controller-> index ($ posts);
});
Tudo isso funcionará, chamando o fechamento em vez do método original:
$ container-> call ('PostController @ index');
$ container-> call ('PostController', [], 'index');
$ container-> call ([novo PostController, 'index']);
No entanto, quaisquer parâmetros adicionais para call ()
não são passados para o fechamento e, portanto, não podem ser usados.
$ container-> call ('PostController @ index', ['Não utilizado :-(']);
Notas: Este método não faz parte da [Interface do contêiner] (https://github.com/laravel/framework/blob/6.x/src/Illuminate/Contracts/Container/Container.php), apenas o concreta [classe Container] (https://github.com/laravel/framework/blob/6.x/src/Illuminate/Container/Container.php). Consulte [o PR onde foi adicionado] (https://github.com/laravel/framework/pull/16800) para
saber por que os parâmetros são ignorados.
Ligações contextuais
Às vezes, você deseja usar implementações diferentes de uma interface em lugares diferentes . Aqui está um exemplo adaptado do [Laravel docs] (https://laravel.com/docs/5.4/container#contextual-binding):
$ container
-> when (PhotoController :: class)
-> needs ( Sistema de arquivos :: classe)
-> give (LocalFilesystem :: class);
$ container
-> when (VideoController :: class)
-> needs (Sistema de arquivos :: class)
-> give (S3Filesystem :: class);
Agora o PhotoController e o VideoController podem depender da interface do sistema de arquivos, mas cada um receberá uma implementação diferente. Você também pode usar um fechamento para give ()
, assim como você pode com bind ()
:
$ container
-> when (VideoController :: class)
-> needs (Filesystem :: class)
-> give (function () {
return Storage :: disk ('s3');
});
Ou uma dependência nomeada:
$ container-> instance ('s3', $ s3Filesystem);
$ container
-> when (VideoController :: class)
-> needs (Sistema de arquivos :: classe)
-> give ('s3');
Vinculando parâmetros a primitivas
Você também pode vincular primitivas (strings, inteiros, etc.) passando um nome de variável paraneeds ()
(em vez de uma interface) e passando o valor paragive ()
:
$ container
-> when (MySQLDatabase :: class)
-> needs ('$ username')
-> give (DB_USER);
Você pode usar um fechamento para atrasar a recuperação do valor até que seja necessário:
$ container
-> when (MySQLDatabase :: class)
-> needs ('$ username')
-> give (function () {
retornar config ('database.user');
});
Aqui você não pode passar uma classe ou uma dependência nomeada (por exemplo, give ('database.user')
) porque seria retornada como um valor literal - para fazer isso, seria necessário usar um fechamento:
$ container
-> when (MySQLDatabase :: class)
-> needs ('$ username')
-> give (function (Container $ container) {
return $ container ['database.user'];
});
Tagging
Você pode usar o contêiner para” marcar “ligações relacionadas:
$ container-> tag (MyPlugin :: class, 'plugin');
$ container-> tag (AnotherPlugin :: class, 'plugin');
E então recupere todas as instâncias marcadas como uma matriz:
foreach ($ container-> tagged ('plugin') como $ plugin) {
$ plugin-> init ();
}
Ambos os parâmetrostag ()
também aceitam matrizes:
$ container-> tag ([MyPlugin :: class, AnotherPlugin :: class], 'plugin');
$ container-> tag (MyPlugin :: class, ['plugin', 'plugin.admin']);
Rebinding
Nota: Isso é um pouco mais avançado e raramente é necessário - fique à vontade para ignorá-lo!
Um retorno de chamada rebinding ()
é chamado quando uma ligação ou instância é alterada após ele já foi usado - por exemplo, aqui a classe da sessão é substituída após ter sido usada pela classe Auth, portanto, a classe Auth precisa ser informada da alteração:
$ container-> singleton (Auth :: classe, função (Container $ container) {
$ auth = new Auth;
$ auth-> setSession ($ container-> make (Session :: class));
$ container-> rebinding (Session :: classe, função ($ container, $ session) use ($ auth) {
$ auth-> setSession ($ session);
});
retorne $ auth;
});
$ container-> instance (Session :: class, new Session (['nome de usuário' => 'dave']));
$ auth = $ container-> make (Auth :: class);
echo $ auth-> nome de usuário (); // dave
$ container-> instance (Session :: class, new Session (['nome de usuário' => 'danny']));
echo $ auth-> nome de usuário (); // danny
(Para obter mais informações sobre religação, consulte [aqui] (https://stackoverflow.com/questions/38974593/laravels-ioc-container-rebinding-abstract-types) e [aqui] (https://code.tutsplus.com / tutorials / digging-to-laravels-ioc-container - cms-22167).)
refresh ()
Há também um método de atalho, refresh ()
, para lidar com esse padrão comum:
$ container-> singleton (Auth :: classe, função (Container $ container) {
$ auth = new Auth;
$ auth-> setSession ($ container-> make (Session :: class));
$ container-> refresh ( Session :: classe, $ auth, 'setSession');
retorna $ auth;
});
Ele também retorna a instância ou ligação existente (se houver), para que você possa fazer isso:
// Isso funciona apenas se você chamar singleton () ou bind () na classe
$ container-> singleton (Session :: class);
$ container-> singleton (Auth :: classe, função (Container $ container) {
$ auth = new Auth;
$ auth-> setSession ($ container-> refresh (Sessão :: classe, $ auth, 'setSession'));
retornar $ auth;
});
(Pessoalmente, acho essa sintaxe mais confusa e prefiro a versão mais detalhada acima!)
Nota: Esses métodos não fazem parte da [Interface do contêiner] (https://github.com/laravel/framework /blob/5.4/src/Illuminate/Contracts/Container/Container.php), apenas a [classe Container] concreta (https://github.com/laravel/framework/blob/6.x/src/Illuminate/Container/Container.php) .
Substituindo Parâmetros do Construtor
O método makeWith ()
permite que você passe parâmetros adicionais para o construtor. Ele ignora quaisquer instâncias ou singletons existentes e pode ser útil para criar várias instâncias de uma classe com parâmetros diferentes enquanto ainda injeta dependências:
class Post
{
public function __construct (Database $ db, int $ id) {/ *. .. * /}
}
$ post1 = $ container-> makeWith (Post :: class, ['id' => 1]);
$ post2 = $ container-> makeWith (Post :: class, ['id' => 2]);
Nota: No Laravel 5.3 e abaixo, era simplesmente make ($ class, $ parameters)
. Foi [removido no Laravel 5.4] (https://github.com/laravel/internals/issues/391), mas depois [adicionado novamente como makeWith ()] (https://github.com/laravel/framework/ pull / 18271) em 5.4.16 . No Laravel 5.5, parece que ele será [revertido para a sintaxe do Laravel 5.3] (https://github.com/laravel/framework/pull/19201) .
Outros métodos
Que abrange todos os métodos que eu acho que são útil - mas, para finalizar, aqui está um resumo dos métodos públicos restantes …
bound ()
O método bound ()
retorna true se a classe ou o nome tiver sido associado com bind ()
, singleton ()
, instance ()
ou alias ()
.
if (! $ container-> bound ('database.user')) {
// ...
}
Você também pode usar a sintaxe de acesso a matriz eisset ()
:
if (isset ($ recipiente [ 'database.user'])!) {
// ...
}
Pode ser redefinido com unset ()
, que remove a ligação / instance / alias especificada.
unset ($ container ['database.user']);
var_dump ($ container-> bound ('database.user')); // false
bindIf ()
bindIf ()
faz a mesma coisa que bind ()
, exceto que ele só registra uma ligação se ainda não existir (consulte bound ()
acima) . Poderia ser usado para registrar uma ligação padrão em um pacote e permitir que o usuário a substituísse.
$ container-> bindIf (Loader :: class, FallbackLoader :: class);
Não existe nenhum método `singletonIf ()`, mas você pode usar `bindIf ($ abstract, $ concrete, true)` em vez disso:
``` php
$ container-> bindIf (Loader :: class, FallbackLoader :: class, true) ;
Ou escreva-o na íntegra:
if (! $ Container-> bound (Loader :: class)) {
$ container-> singleton (Loader :: class, FallbackLoader :: class);
}
resolved ()
O métodoresolved ()
retorna true se uma classe tiver sido resolvida anteriormente.
var_dump ($ container-> resolved (Database :: class)); // false
$ container-> make (Database :: class);
var_dump ($ container-> resolvido (Database :: class)); // true
Não sei ao certo o que é útil … É redefinido se unset ()
for usado (consulte bound ()
acima).
unset ($ container [Database :: class]);
var_dump ($ container-> resolvido (Database :: class)); // false
factory ()
O métodofactory ()
retorna um fechamento que não aceita parâmetros e chamamake ()
.
$ dbFactory = $ container-> factory (Database :: class);
$ db = $ dbFactory ();
Não sei ao certo o que é útil …
wrap ()
O método wrap ()
fecha um fechamento para que suas dependências sejam injetadas quando executadas. O método de quebra aceita uma matriz de parâmetros; o fechamento retornado não tem parâmetros:
$ cacheGetter = function (Cache $ cache, $ key) {
retornar $ cache-> get ($ key);
};
$ usernameGetter = $ container-> wrap ($ cacheGetter, ['nome de usuário']);
$ nome de usuário = $ nome de usuárioGetter ();
Não sei para que serve, pois o fechamento não requer parâmetros …
Nota: Este método não faz parte da [Interface do contêiner] (https://github.com/laravel /framework/blob/5.4/src/Illuminate/Contracts/Container/Container.php), apenas a [classe Container] concreta (https://github.com/laravel/framework/blob/5.4/src/Illuminate/Container/ Container.php) .
afterResolving ()
O método afterResolving ()
funciona exatamente da mesma forma que resolving ()
, exceto que os retornos de chamada “afterResolving” são chamados após os retornos de chamada “resolvendo”. Não tenho certeza de quando isso seria útil …
E finalmente …
isShared ()
- Determina se um determinado tipo é um singleton / instância compartilhadoisAlias ()
- Determina se um determinado string é um alias registradohasMethodBinding ()
- Determina se o contêiner possui um determinado método de ligaçãogetBindings ()
- Recupera a matriz bruta de todas as ligações registradasgetAlias ($ abstract)
- Resolve um alias para o classe subjacente / nome da ligaçãoforgetInstance ($ abstract)
- Limpa um único objeto de instânciaforgetInstances ()
- Limpa todos os objetos de instânciaflush ()
- Limpa todas as ligações e instâncias, redefinindo efetivamente o contêinersetInstance ()
- Substitui a instância usada porgetInstance ()
(Dica: usesetInstance (null)
para limpá-la, então a seguir vez que gerará uma nova instância)
Nota: Nenhum dos métodos nesta última seção faz parte da [Interface do contêiner] (https://github.com/laravel/framework/tree/6.x/src/ Illuminate / Contracts / Container / Container.php) .
- Renato Lucena
Donate to Site
Renato
Developer