Symfony 4: трейты и внедрение зависимости через свойство

Symfony 4: трейты и внедрение зависимости через свойство

Внедрение зависимости - паттерн, без которого работать с Symfony просто невозможно. Однако, если инъекция через конструктор и через метод является вполне обычным делом, то о внедрении через свойство сказано не так уж и много. Поэтому предлагаю рассмотреть эту тему на простом примере.

Прежде чем приступим - статей по данному паттерну предостаточно, но лично я в качестве дополнительных материалов хотел бы порекомендовать следующие:

Представим следующую ситуацию: во многих различных сервисах мы хотим использовать логгирование. При обычных раскладах код выглядел бы примерно так:

<?php

namespace App\Service;

use Psr\Log\LoggerInterface;

class SomeService
{
    /**
     * @var LoggerInterface
     */
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
    ...
}

 

В нашем же случае мы не хотим дублировать внедрение в конструктор в каждом классе, но и выстраивать какую-то иерархию не вариант, поскольку (как уже говорилось) сервисы абсолютно разные. Тем не менее, выход есть - горизонтальное расширение. Другими словами, напишем трейт, который будем включать там, где необходимо. 

Создадим в директории src поддиректорию Traits и в ней трейт HasLogger:

<?php

namespace App\Traits;

use Psr\Log\LoggerInterface;

trait HasLogger
{
    /**
     * @var LoggerInterface|null
     */
    private $logger;

    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function logInfo(string $message, array $context = [])
    {
        if ($this->logger) {
            $this->logger->info($message, $context);
        }
    }
}

 

Поясню: 

  • для внедрения в свойство (Property Injection) порой также используется термин Setter Injection. Это одно и то же, и подразумевает наличие сеттера, в котором мы устанавливаем соответствующее свойство класса
  • зачем нужен условный оператор if в методе logInfo? Нет никакой гарантии, что пользователь вызовет наш сеттер, поэтому мы должны предусмотреть эту ситуацию
  • метод logInfo является обёрткой для метода логгера info (смотрите код LoggerInterface). Разумеется, никто не мешает сделать аналогичные обёртки для других методов (alert, emergency, warning, notice и т.д.)

Если мы предполагаем, что логгер будет использоваться в большинстве сервисов, то заставлять девелопера постоянно вызывать сеттер - не самое лучшее решение. Было бы неплохо, если бы при инстанциировании сервиса, устанавливалось и свойство $logger. Что ж, это возможно, и делается довольно просто - в трейте добавим над сеттером аннотацию @required:

/**
 * @required
 */
public function setLogger(LoggerInterface $logger)
{
    $this->logger = $logger;
}

 

Дело сделано - сеттер будет вызываться автоматически. Теперь класс сервиса примет такой вид:

<?php

namespace App\Service;

use App\Traits\HasLogger;

class SomeService
{
    use HasLogger;

    public function doSomething()
    {
        // do something
        $this->logInfo('Some message');
    }
}

 

И вдогонку замечу, что вместо того, чтобы писать обёртку для каждого из методов логгера, можно было бы в трейте реализовать следующее:

public function writeLog(string $methodName, string $message, array $context = [])
{
    if ($this->logger && method_exists($this->logger, $methodName)) {
        $this->logger->{$methodName}($message, $context);
    }
}

 

На этом на сегодня всё. Успехов!