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

 

На цьому на сьогодні все. Успіхів!