
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);
}
}
На цьому на сьогодні все. Успіхів!