
Symfony 4: трейты и внедрение зависимости через свойство
05.05.2019 10:11 | Symfony
Внедрение зависимости - паттерн, без которого работать с Symfony просто невозможно. Однако, если инъекция через конструктор и через метод является вполне обычным делом, то о внедрении через свойство сказано не так уж и много. Поэтому предлагаю рассмотреть эту тему на простом примере.
Прежде чем приступим - статей по данному паттерну предостаточно, но лично я в качестве дополнительных материалов хотел бы порекомендовать следующие:
- Dependency Injection (Wiki)
- PHP Design Patterns - Dependency Injection
- Symfony - The DependencyInjection Component
Представим следующую ситуацию: во многих различных сервисах мы хотим использовать логгирование. При обычных раскладах код выглядел бы примерно так:
<?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);
}
}
На этом на сегодня всё. Успехов!