0

Простой DI контейнер для Wordpress

Внедрение зависимостей (он же Dependency Injection, он же DI) - мощный подход, позволяющий нам уменьшить связанность кода, делая его гибким для расширения. С помощью DI мы избавляем наши компоненты от необходимости построения своих зависимостей и делегируем эту работу внешнему механизму. Если простыми словами, то вместо того, чтобы делать так:

class OrderManager
{
    private $cache;
    
    public function __construct()
    {
        $this->cache = new FileCache();
    }
}

Нужно делать вот так:

class OrderManager
{
    private $cache;

    public function __construct(CacheInterface $cache)
    {
        $this->cache= $cache;
    }
}

В данной статье мы реализуем простой контейнер для управления зависимостями в ваших Wordpress проектах. Статья будет полезна в первую очередь для владельцев сайтов и интернет-магазинов, функционал которых достаточно сложный и обладает нетривиальной логикой. Также статья будет полезна для разработчиков тем и плагинов.

Требования к контейнеру

  • Контейнер должен предоставлять простой интерфейс создания объектов.
  • Контейнер должен предоставлять простой механизм конфигурации зависимостей.
  • Контейнер должен быть совместим с PSR-11 (Container interface).

Реализация

Мы не будем использовать composer, чтобы подтянуть ContainerInterface из PSR-11, поскольку в перспективе можем попасть на namespace collision с другими плагинами. Вместо этого мы сразу реализуем эти методы в нашем контейнере. Давайте напишем базовую структуру класса:

if ( ! defined('ABSPATH')) {
    exit;
}

class WPContainer
{
    /**
     * Контейнер будет глобален для всего сайта.
     * @var Container
     */
    private static $instance;

    /**
     * Список зависимостей.
     * @var array
     */
    private $bindings = [];

    /**
     * Container constructor.
     * @param array $bindings
     */
    private function __construct($bindings = [])
    {
        $this->bindings = $bindings;
    }

    /**
     * Этим методом мы будем получать наш глобальный инстанс.
     * @param array $bindings
     */
    public static function instance($bindings = [])
    {
        if (null === self::$instance) {
            self::$instance = new self($bindings);
        }

        return self::$instance;
    }

    /**
     * PSR 11 implementation
     * @param string $id
     */
    public function has($id)
    {
        return isset($this->bindings[$id]);
    }

    /**
     * PSR 11 implementation.
     * @param string $id
     * @return mixed
     * @throws Exception
     */
    public function get($id)
    {
        if ($this->has($id)) {
            return $this->resolve($this->bindings[$id]);
        }
        else {
            throw new Exception('Dependency ' . $id . ' not found.');
        }
    }

    /**
     * Получаем нужную зависимость из callback функции.
     * @param string $dependency
     * @return mixed
     */
    private function resolve($dependency)
    {
        return call_user_func($dependency, $this);
    }
}

На данном этапе мы реализовали самый простой DI контейнер, который соответствует стандарту PSR-11. Давайте попробуем его использовать. Для начала, инициализируем инстанс с конфигурациями зависимостей. Откроем файл functions.php нашей темы и напишем следующий код.

// Предполагаем, что класс контейнера лежит в той же папке
require_once 'WPContainer.php';

$dependencies = [
    OrderManager::class => function ($container) {
        return new OrderManager($container->get(CacheInterface::class));
    },
    CacheInterface::class => function ($container) {
        return new FileCache();
    }
];

WPContainer::instance($dependencies);

Теперь, когда нам понадобится класс OrderManager, мы можем создать его с помощью контейнера:

$orderManager = WPContainer::instance()->get(OrderManager::class);

Какие преимущества мы получаем? Самое главное - это слабая связанность кода. Каждый класс системы теперь не занимается созданием нужных ему объектов. Он лишь описывает зависимости, которые ему необходимы для работы и знает, как эти зависимости использовать. Если мы будем явно использовать класс FileCache в разных частях нашего сайта (с помощью оператора new), то возникнут проблемы, если нам понадобиться изменить тип кеширования (например на Redis). Используя DI container, все что нам нужно будет сделать, это заменить реализацию интерфейса CacheInterface в файле functions.php.

Текущий класс контейнера обладает одним маленьким недостатком. Стандарт PSR-11 требует выбрасывать исключение, если указанной зависимости не было найдено в конфигурации. При разработке сайта на начальных этапах, может быть неизвестно какие зависимости нам понадобятся в будущем. Поэтому такая реализация контейнера будет обязывать нас держать в голове все зависимости, которые мы конфигурируем. Давайте улучшим наш класс контейнера, добавив новый метод make.

/**
 * Пробуем создать зависимость через метод get. При срабатывании исключения возвращаем новый экземпляр требуемого класса.
 * @param string $abstract
 * @return mixed
 */
public function make($abstract)
{
    try {
        return $this->get($abstract);
    }
    catch (Exception $e) {
        return new $abstract();
    }
}

Теперь, используя метод make, если наш класс OrderManager не будет иметь зависимостей, контейнер просто вернет его экземпляр без выброса исключения. Когда в будущем мы захотим добавить кеширование, останется просто объявить нужную конфигурацию.

На этом у меня все) Всем спасибо за внимание.