Injeção de Dependência Automática

No Webman, a injeção de dependência automática é uma funcionalidade opcional, que está desativada por padrão. Se você precisar da injeção de dependência automática, é recomendado utilizar o php-di. A seguir, apresentamos como integrar o Webman com o php-di.

Instalação

composer require psr/container ^1.1.1 php-di/php-di ^6.3 doctrine/annotations ^1.14

Modifique a configuração config/container.php, o conteúdo final deve ser como abaixo:

$builder = new \DI\ContainerBuilder();
$builder->addDefinitions(config('dependence', []));
$builder->useAutowiring(true);
$builder->useAnnotations(true);
return $builder->build();

O arquivo config/container.php retorna uma instância de contêiner que está em conformidade com a especificação PSR-11. Se você não deseja usar o php-di, pode criar e retornar outra instância de contêiner que esteja em conformidade com PSR-11.

Injeção pelo Construtor

Crie o arquivo app/service/Mailer.php (se o diretório não existir, crie por conta própria) com o conteúdo a seguir:

<?php
namespace app\service;

class Mailer
{
    public function mail($email, $content)
    {
        // Código para enviar email omitido
    }
}

O arquivo app/controller/UserController.php deve conter o seguinte:

<?php
namespace app\controller;

use support\Request;
use app\service\Mailer;

class UserController
{
    private $mailer;

    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function register(Request $request)
    {
        $this->mailer->mail('hello@webman.com', 'Hello and welcome!');
        return response('ok');
    }
}

Normalmente, o seguinte código é necessário para completar a instanciação de app\controller\UserController:

$mailer = new Mailer;
$user = new UserController($mailer);

Com o uso do php-di, o desenvolvedor não precisa instanciar manualmente o Mailer, o Webman cuida disso automaticamente. Se a instanciação do Mailer requerer a injeção de outras classes, o Webman também fará a injeção automaticamente. O desenvolvedor não precisa realizar nenhuma operação de inicialização.

Atenção
Apenas instâncias criadas pelo framework ou php-di podem completar a injeção de dependência automática. Instâncias criadas manualmente com new não conseguirão realizar a injeção de dependência; para injeção, deve-se usar a interface support\Container para substituir as instruções new, por exemplo:

use app\service\UserService;
use app\service\LogService;
use support\Container;

// Instâncias criadas com a palavra-chave new não conseguem realizar injeção de dependência
$user_service = new UserService;
// Instâncias criadas com a palavra-chave new não conseguem realizar injeção de dependência
$log_service = new LogService($path, $name);

// Instâncias criadas com Container podem realizar injeção de dependência
$user_service = Container::get(UserService::class);
// Instâncias criadas com Container podem realizar injeção de dependência
$log_service = Container::make(LogService::class, [$path, $name]);

Injeção por Anotações

Além da injeção automática via construtor, também podemos usar a injeção por anotações. Continuando o exemplo anterior, modifique app\controller\UserController para o seguinte:

<?php
namespace app\controller;

use support\Request;
use app\service\Mailer;
use DI\Annotation\Inject;

class UserController
{
    /**
     * @Inject
     * @var Mailer
     */
    private $mailer;

    public function register(Request $request)
    {
        $this->mailer->mail('hello@webman.com', 'Hello and welcome!');
        return response('ok');
    }
}

Este exemplo utiliza a anotação @Inject para a injeção, e a anotação @var para declarar o tipo do objeto. Este exemplo produz o mesmo efeito que a injeção por construtor, mas com um código mais conciso.

Atenção
O Webman não suporta a injeção de parâmetros em controladores antes da versão 1.4.6; por exemplo, o seguinte código não será suportado quando o Webman for <=1.4.6:

<?php
namespace app\controller;

use support\Request;
use app\service\Mailer;

class UserController
{
    // A injeção de parâmetros em controladores não é suportada antes da versão 1.4.6
    public function register(Request $request, Mailer $mailer)
    {
        $mailer->mail('hello@webman.com', 'Hello and welcome!');
        return response('ok');
    }
}

Injeção Personalizada por Construtor

Às vezes, os parâmetros passados para o construtor podem não ser instâncias de classe, mas sim strings, números, arrays, etc. Por exemplo, o construtor do Mailer pode precisar receber o IP e a porta do servidor SMTP:

<?php
namespace app\service;

class Mailer
{
    private $smtpHost;

    private $smtpPort;

    public function __construct($smtp_host, $smtp_port)
    {
        $this->smtpHost = $smtp_host;
        $this->smtpPort = $smtp_port;
    }

    public function mail($email, $content)
    {
        // Código para enviar email omitido
    }
}

Nessa situação, não é possível usar diretamente a injeção automática do construtor como mencionado anteriormente, pois o php-di não consegue determinar quais são os valores de $smtp_host e $smtp_port. Podemos tentar realizar uma injeção personalizada.

No arquivo config/dependence.php (caso o arquivo não exista, crie-o) adicione o seguinte código:

return [
    // ... outras configurações omitidas

    app\service\Mailer::class =>  new app\service\Mailer('192.168.1.11', 25);
];

Assim, quando a injeção de dependência precisar de uma instância de app\service\Mailer, a instância criada por esta configuração será automaticamente utilizada.

Notamos que, em config/dependence.php, o uso de new para instanciar a classe Mailer não apresenta problemas neste exemplo. Porém, imagine que a classe Mailer depende de outras classes ou que utiliza injeção por anotações: usar new para inicializar não permitirá a injeção automática. A solução é utilizar a injeção através de interfaces personalizadas, utilizando Container::get(classe) ou Container::make(classe, [parâmetros do construtor]) para inicializar a classe.

Injeção por Interface Personalizada

Em projetos reais, preferimos programar voltados para interfaces, em vez de classes concretas. Por exemplo, o app\controller\UserController deve importar app\service\MailerInterface em vez de app\service\Mailer.

Defina a interface MailerInterface.

<?php
namespace app\service;

interface MailerInterface
{
    public function mail($email, $content);
}

Defina a implementação da interface MailerInterface.

<?php
namespace app\service;

class Mailer implements MailerInterface
{
    private $smtpHost;

    private $smtpPort;

    public function __construct($smtp_host, $smtp_port)
    {
        $this->smtpHost = $smtp_host;
        $this->smtpPort = $smtp_port;
    }

    public function mail($email, $content)
    {
        // Código para enviar email omitido
    }
}

Importe a interface MailerInterface em vez da implementação concreta.

<?php
namespace app\controller;

use support\Request;
use app\service\MailerInterface;
use DI\Annotation\Inject;

class UserController
{
    /**
     * @Inject
     * @var MailerInterface
     */
    private $mailer;

    public function register(Request $request)
    {
        $this->mailer->mail('hello@webman.com', 'Hello and welcome!');
        return response('ok');
    }
}

No arquivo config/dependence.php, defina a interface MailerInterface com a seguinte implementação.

use Psr\Container\ContainerInterface;
return [
    app\service\MailerInterface::class => function(ContainerInterface $container) {
        return $container->make(app\service\Mailer::class, ['smtp_host' => '192.168.1.11', 'smtp_port' => 25]);
    }
];

Assim, quando for necessário usar a interface MailerInterface, a implementação Mailer será utilizada automaticamente.

A vantagem de programar orientado a interfaces é que, quando precisamos substituir um componente, não precisamos alterar o código de negócios, apenas a implementação concreta no config/dependence.php. Isso também é muito útil para testes unitários.

Outras Injeções Personalizadas

O config/dependence.php não só pode definir dependências de classes, mas também outros valores, como strings, números, arrays, etc.

Por exemplo, config/dependence.php pode ser definido como abaixo:

return [
    'smtp_host' => '192.168.1.11',
    'smtp_port' => 25
];

Nesse caso, podemos usar @Inject para injetar smtp_host e smtp_port nas propriedades da classe.

<?php
namespace app\service;

use DI\Annotation\Inject;

class Mailer
{
    /**
     * @Inject("smtp_host")
     */
    private $smtpHost;

    /**
     * @Inject("smtp_port")
     */
    private $smtpPort;

    public function mail($email, $content)
    {
        // Código para enviar email omitido
        echo "{$this->smtpHost}:{$this->smtpPort}\n"; // Exibirá 192.168.1.11:25
    }
}

Atenção: @Inject("key") deve ser escrito em aspas duplas.

Mais Conteúdo

Consulte o manual do php-di para mais informações.