Container de DI
No Yii3, as classes não buscam o que precisam: elas pedem tudo no construtor, e o container de injeção de dependência monta os objetos. Isso vale para actions, serviços, middlewares e listeners. Este capítulo mostra como o container resolve as dependências sozinho e como configurá-lo quando ele não consegue.
O container vem do pacote yiisoft/di, e a sintaxe das definições é do yiisoft/definitions. O template já instala e cria o container a partir dos grupos di e di-web (ou di-console) da configuração.
Autowiring
Quando o container precisa de uma classe que não tem definição, ele lê o construtor dela por reflexão e resolve cada parâmetro pelo tipo. É o autowiring. Foi assim que todas as actions deste guia receberam WebViewRenderer, ResponseFactoryInterface ou SessionInterface sem você escrever configuração.
O autowiring resolve sozinho:
- Parâmetros tipados com uma classe concreta: o container cria a instância, resolvendo as dependências dela do mesmo jeito.
- Parâmetros com valor padrão, quando não há outra informação: vale o padrão.
E precisa de ajuda em:
- Interfaces: o container não sabe qual implementação usar.
- Tipos escalares sem valor padrão (
string,int,array): não há de onde tirar o valor.
Para esses casos você escreve uma definição.
Outra coisa importante: o container guarda cada objeto que cria. Pedir a mesma classe duas vezes devolve a mesma instância, durante toda a requisição. Isso tem consequências que o capítulo Imutabilidade discute.
O exemplo
Uma loja precisa avisar o cliente quando o pedido chega. A interface, em src/Conceitos/Notificacao/NotificadorInterface.php:
<?php
declare(strict_types=1);
namespace App\Conceitos\Notificacao;
interface NotificadorInterface
{
public function enviar(string $para, string $mensagem): void;
}Uma implementação que grava no log, em src/Conceitos/Notificacao/NotificadorPorLog.php:
<?php
declare(strict_types=1);
namespace App\Conceitos\Notificacao;
use Psr\Log\LoggerInterface;
final readonly class NotificadorPorLog implements NotificadorInterface
{
public function __construct(
private LoggerInterface $logger,
private string $canal = 'geral',
) {}
public function enviar(string $para, string $mensagem): void
{
$this->logger->info('[{canal}] Para {para}: {mensagem}', [
'canal' => $this->canal,
'para' => $para,
'mensagem' => $mensagem,
'category' => 'conceitos',
]);
}
}Outra que só guarda as mensagens em memória, útil em testes, em src/Conceitos/Notificacao/NotificadorEmMemoria.php:
<?php
declare(strict_types=1);
namespace App\Conceitos\Notificacao;
final class NotificadorEmMemoria implements NotificadorInterface
{
/** @var list<string> */
private array $enviadas = [];
public function enviar(string $para, string $mensagem): void
{
$this->enviadas[] = "$para: $mensagem";
}
/**
* @return list<string>
*/
public function enviadas(): array
{
return $this->enviadas;
}
}O serviço que usa o notificador, em src/Conceitos/Notificacao/AvisoDePedido.php. Ele depende de uma interface e de uma string, os dois casos que o autowiring não resolve:
<?php
declare(strict_types=1);
namespace App\Conceitos\Notificacao;
final class AvisoDePedido
{
private string $prefixo = '';
public function __construct(
private readonly NotificadorInterface $notificador,
private readonly string $remetente,
) {}
public function usarPrefixo(string $prefixo): void
{
$this->prefixo = $prefixo . ' ';
}
public function avisar(string $cliente, int $pedido): string
{
$mensagem = "{$this->prefixo}Seu pedido $pedido foi recebido. Responda para {$this->remetente}.";
$this->notificador->enviar($cliente, $mensagem);
return $mensagem;
}
}E um segundo serviço, em src/Conceitos/Notificacao/RelatorioDiario.php:
<?php
declare(strict_types=1);
namespace App\Conceitos\Notificacao;
final readonly class RelatorioDiario
{
public function __construct(
private NotificadorInterface $notificador,
) {}
public function enviar(string $para, int $pedidos): void
{
$this->notificador->enviar($para, "Hoje foram $pedidos pedidos.");
}
}As definições
As definições ficam em config/common/di/conceitos.php. Todo arquivo dessa pasta é carregado sozinho:
<?php
declare(strict_types=1);
use App\Conceitos\Notificacao\AvisoDePedido;
use App\Conceitos\Notificacao\NotificadorEmMemoria;
use App\Conceitos\Notificacao\NotificadorInterface;
use App\Conceitos\Notificacao\NotificadorPorLog;
use App\Conceitos\Notificacao\RelatorioDiario;
use App\Conceitos\Saudacao;
use Psr\Log\LoggerInterface;
use Yiisoft\Definitions\Reference;
/** @var array $params */
return [
Saudacao::class => [
'__construct()' => [
'mensagem' => $params['conceitos']['saudacao'],
'itensPorPagina' => $params['conceitos']['itensPorPagina'],
],
],
NotificadorInterface::class => NotificadorPorLog::class,
NotificadorPorLog::class => static fn(LoggerInterface $logger) => new NotificadorPorLog($logger, canal: 'pedidos'),
AvisoDePedido::class => [
'__construct()' => [
'remetente' => $params['conceitos']['remetente'],
],
'usarPrefixo()' => ['[Guia Yii3]'],
],
RelatorioDiario::class => [
'__construct()' => [
'notificador' => Reference::to(NotificadorEmMemoria::class),
],
],
NotificadorEmMemoria::class => [
'class' => NotificadorEmMemoria::class,
'reset' => function (): void {
$this->enviadas = [];
},
],
];A chave do array é o que se pede ao container, normalmente um nome de classe ou interface. O valor diz como construir. Cada formato a seguir aparece no arquivo.
Interface para classe
NotificadorInterface::class => NotificadorPorLog::class,Quem pedir NotificadorInterface recebe um NotificadorPorLog. É a forma mais comum de definição: o código depende da interface, e a configuração escolhe a implementação. Trocar para um envio por e-mail é mudar esta linha.
Array: class, __construct() e métodos
AvisoDePedido::class => [
'__construct()' => [
'remetente' => $params['conceitos']['remetente'],
],
'usarPrefixo()' => ['[Guia Yii3]'],
],class: a classe a instanciar. Pode ser omitida quando é a própria chave.__construct(): argumentos do construtor. Com chaves de texto, os argumentos vão pelo nome, e os que faltam continuam no autowiring. Aqui sóremetentefoi informado; onotificadorvem da definição da interface.metodo(): um método chamado depois de criar o objeto, com os argumentos da lista.$propriedade: uma propriedade pública preenchida depois de criar o objeto, como'$timeout' => 30.
Métodos e propriedades são aplicados na ordem em que aparecem.
Closure como fábrica
NotificadorPorLog::class => static fn(LoggerInterface $logger) => new NotificadorPorLog($logger, canal: 'pedidos'),Quando montar o objeto exige código, use uma função. Os parâmetros dela são resolvidos pelo container, como num construtor. Neste caso um array com __construct() daria o mesmo resultado; a closure vale a pena quando há lógica, como escolher a classe por uma condição ou chamar um método de fábrica de outra biblioteca. Se a lógica crescer, mova-a para um método estático de uma classe fábrica e use [MinhaFabrica::class, 'criar'] como definição.
Referências
RelatorioDiario::class => [
'__construct()' => [
'notificador' => Reference::to(NotificadorEmMemoria::class),
],
],Um nome de classe como argumento de __construct() seria tratado como string. O Reference::to() diz "busque isto no container". Aqui ele faz o RelatorioDiario usar o notificador em memória, enquanto o resto da aplicação continua com o notificador por log.
O yiisoft/definitions tem variações:
ReferencesArray::from([...]): uma lista de referências, usada noconfig/common/di/logger.phppara os targets do log.DynamicReference::to(...): uma definição completa (array ou closure) usada como argumento. O objeto é criado novo, sem passar pelo cache do container. O template usa isso emconfig/web/di/application.phppara montar o dispatcher de middlewares.
Nomes que não são classes
A chave não precisa ser uma classe. O arquivo de log do app define 'log.pagamentos' para um segundo FileTarget, e quem precisar dele usa Reference::to('log.pagamentos'). Isso resolve o caso de duas configurações diferentes da mesma classe.
Testando
Uma action que usa os dois serviços, em src/Web/Conceitos/Container/Action.php:
<?php
declare(strict_types=1);
namespace App\Web\Conceitos\Container;
use App\Conceitos\Notificacao\AvisoDePedido;
use App\Conceitos\Notificacao\NotificadorEmMemoria;
use App\Conceitos\Notificacao\RelatorioDiario;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
final readonly class Action
{
public function __construct(
private ResponseFactoryInterface $responseFactory,
private AvisoDePedido $aviso,
private RelatorioDiario $relatorio,
private NotificadorEmMemoria $memoria,
) {}
public function __invoke(): ResponseInterface
{
$mensagem = $this->aviso->avisar('ana@exemplo.com', 1001);
$this->relatorio->enviar('gerente@exemplo.com', 12);
$response = $this->responseFactory->createResponse();
$response->getBody()->write(
"Aviso: $mensagem\n"
. 'Em memória: ' . implode(' | ', $this->memoria->enviadas()) . "\n",
);
return $response->withHeader('Content-Type', 'text/plain; charset=utf-8');
}
}Com a rota /conceitos/container:
Aviso: [Guia Yii3] Seu pedido 1001 foi recebido. Responda para loja@guia.test.
Em memória: gerente@exemplo.com: Hoje foram 12 pedidos.- O prefixo veio do
usarPrefixo()e o remetente dos params. - O aviso foi para o log, pelo
NotificadorPorLogcom o canalpedidos:runtime/logs/app.logregistra[info][conceitos] [pedidos] Para ana@exemplo.com: .... - O relatório foi para o
NotificadorEmMemoria, e a action enxerga a mensagem porque recebeu a mesma instância que oRelatorioDiario.
Reset
O último item da configuração tem a chave reset:
NotificadorEmMemoria::class => [
'class' => NotificadorEmMemoria::class,
'reset' => function (): void {
$this->enviadas = [];
},
],A função de reset devolve o serviço ao estado inicial. Ela roda com acesso às propriedades privadas do objeto (o $this é o próprio serviço) e é chamada pelo Yiisoft\Di\StateResetter. No PHP tradicional, cada requisição começa do zero e isso não importa. Em servidores de longa duração, como RoadRunner, o mesmo container atende várias requisições, e o runner chama o StateResetter entre elas. O exemplo completo está em Imutabilidade.
Para usar reset com uma closure, mova a closure para a chave definition: ['definition' => static fn() => ..., 'reset' => function () {...}]. Da mesma forma funciona a chave tags, que agrupa serviços para receber todos de uma vez com TagReference::to('nome').
Pedindo ao container direto
Evite chamar $container->get() nas suas classes. Uma classe que recebe ContainerInterface para buscar dependências esconde do que depende e fica difícil de testar. Peça a dependência no construtor.
Quando você precisa de objetos novos a cada uso, e não da instância compartilhada, use o Yiisoft\Factory\Factory: ele aceita as mesmas definições, mas o create() monta um objeto novo a cada chamada.
TIP
Com APP_DEBUG=true, o container valida as definições na inicialização. Uma chave errada, como __contruct(), gera logo na primeira requisição o erro class "App\Conceitos\Saudacao" does not have the public method with name "__contruct", em vez de ser ignorada.
Comparando com o Yii2
Yii::$app->get('componente')eYii::$containerviram o container injetado nos construtores. Não existe acesso global.- A seção
componentsvira as definições deconfig/common/di/econfig/web/di/. Não há diferença entre "componente" e "serviço". - O formato
['class' => ..., 'propriedade' => valor]do Yii2 continua parecido, mas propriedades levam$na frente e chamadas de método levam(). Yii::createObject()vira oFactory::create().