Автоматическая инъекция зависимостей

В webman автоматическая инъекция зависимостей является необязательной функцией, которая по умолчанию отключена. Если вам нужна автоматическая инъекция зависимостей, рекомендуется использовать php-di. Ниже приведены способы интеграции webman с php-di.

Установка

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

Измените конфигурацию config/container.php, итоговое содержимое будет следующим:

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

В файле config/container.php в конечном итоге возвращается экземпляр контейнера, соответствующий стандарту PSR-11. Если вы не хотите использовать php-di, вы можете создать и вернуть другой экземпляр контейнера, соответствующий стандарту PSR-11.

Инъекция через конструктор

Создайте app/service/Mailer.php (если каталог не существует, создайте его самостоятельно) со следующим содержимым:

<?php
namespace app\service;

class Mailer
{
    public function mail($email, $content)
    {
        // Код отправки почты опущен
    }
}

Содержимое app/controller/UserController.php будет следующим:

<?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');
    }
}

В нормальных условиях, для создания экземпляра app\controller\UserController, потребуется следующий код:

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

При использовании php-di разработчику не нужно вручную создавать экземпляр Mailer в контроллере, webman автоматически позаботится об этом. Если в процессе создания Mailer есть зависимости от других классов, webman также автоматически создаст и инъектирует их. Разработчику не требуется никаких дополнительных инициализаций.

Внимание
Только экземпляры, созданные фреймворком или php-di, могут пройти автоматическую инъекцию зависимостей. Экземпляры, созданные вручную с помощью new, не смогут пройти автоматическую инъекцию зависимостей. Если требуется инъекция, используйте интерфейс support\Container вместо выражения new, например:

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

// Экземпляры, созданные с помощью ключевого слова new, не могут пройти инъекцию зависимостей
$user_service = new UserService;
// Экземпляры, созданные с помощью ключевого слова new, не могут пройти инъекцию зависимостей
$log_service = new LogService($path, $name);

// Экземпляры, созданные с помощью Container, могут пройти инъекцию зависимостей
$user_service = Container::get(UserService::class);
// Экземпляры, созданные с помощью Container, могут пройти инъекцию зависимостей
$log_service = Container::make(LogService::class, [$path, $name]);

Инъекция через аннотации

Кроме автоматической инъекции зависимостей через конструктор, мы также можем использовать аннотации для инъекции. Продолжая предыдущий пример, изменим app\controller\UserController следующим образом:

<?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');
    }
}

Этот пример использует аннотацию @Inject для инъекции и аннотацию @var для указания типа объекта. Этот пример аналогичен инъекции через конструктор, но код более лаконичен.

Внимание
webman до версии 1.4.6 не поддерживает инъекцию параметров контроллера, например следующий код не поддерживается в webman <= 1.4.6

<?php
namespace app\controller;

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

class UserController
{
    // Инъекция параметров контроллера до версии 1.4.6 не поддерживается
    public function register(Request $request, Mailer $mailer)
    {
        $mailer->mail('hello@webman.com', 'Hello and welcome!');
        return response('ok');
    }
}

Пользовательская инъекция конструктора

Иногда параметры, передаваемые в конструктор, могут быть не экземплярами классов, а строками, числами, массивами и т. д. Например, конструктор Mailer может потребовать передачи ip-адреса и порта 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)
    {
        // Код отправки почты опущен
    }
}

В этом случае нельзя напрямую использовать ранее описанную автоматическую инъекцию конструктора, так как php-di не может определить, каковы значения $smtp_host и $smtp_port. В этом случае можно попробовать использовать пользовательскую инъекцию.

В config/dependence.php (если файл не существует, создайте его самостоятельно) добавьте следующий код:

return [
    // ... здесь опущены другие конфигурации

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

Таким образом, когда потребуется получить экземпляр app\service\Mailer для инъекции, будет автоматически использоваться созданный в этой конфигурации экземпляр app\service\Mailer.

Мы заметили, что в файле config/dependence.php для создания экземпляра Mailer используется new, что в данном примере не является проблемой. Но представьте, если класс Mailer зависел от других классов или использовал аннотационную инъекцию, использование инициализации через new не позволит осуществить автоматическую инъекцию. Решение состоит в использовании пользовательского интерфейса инъекции через методы Container::get(имя класса) или Container::make(имя класса, [параметры конструктора]) для инициализации классов.

Инъекция интерфейса по собственному проектированию

В реальных проектах мы предпочитаем программировать на основе интерфейсов, а не конкретных классов. Например, в app\controller\UserController следует использовать app\service\MailerInterface вместо app\service\Mailer.

Определим интерфейс MailerInterface.

<?php
namespace app\service;

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

Определим реализацию интерфейса 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)
    {
        // Код отправки почты опущен
    }
}

Используйте интерфейс MailerInterface, а не конкретную реализацию.

<?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');
    }
}

В config/dependence.php определите реализацию интерфейса MailerInterface следующим образом.

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]);
    }
];

Таким образом, когда функцию нужно использовать интерфейс MailerInterface, автоматически будет применяться его реализация Mailer.

Преимущество программирования на основе интерфейсов заключается в том, что, когда нам нужно заменить какой-либо компонент, не нужно изменять бизнес-код, достаточно изменить конкретную реализацию в файле config/dependence.php. Это также очень полезно при написании модульных тестов.

Другие пользовательские инъекции

В config/dependence.php можно не только определять зависимости классов, но и задавать другие значения, такие как строки, числа, массивы и т. д.

Например, в config/dependence.php можно определить следующее:

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

В этом случае мы можем использовать @Inject для инъекции smtp_host и smtp_port в свойства класса.

<?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)
    {
        // Код отправки почты опущен
        echo "{$this->smtpHost}:{$this->smtpPort}\n"; // Выводит 192.168.1.11:25
    }
}

Внимание: в @Inject("key") используются двойные кавычки

Дополнительные материалы

Пожалуйста, ознакомьтесь с руководством php-di