В предыдущей статье мы рассмотрели базовую структуру модуля в M2: читать тут. В этой статье поговорим об инъекции (внедрении) зависимостей в Magento 2. Как ее использовать и для чего она нужна.

Итак, внедрение зависимостей — это паттерн проектирования предназначенный для того, чтобы предоставлять какому-либо объекту зависимости, необходимые для его работы. Это более совершенная альтернатива наследованию, позволяющая уменьшить связанность классов и компонентов между собой.

В основе этого подхода лежит принцип Dependency Inversion (инверсия зависимостей) из принципов SOLID, который предполагает использование абстракций вместо конкретных классов, где детали реализации должны зависеть от абстракций, а абстракции не должны зависеть от деталей реализации.

ObjectManager

За инъекцию зависимостей в M2 отвечает специальный класс ObjectManager (далее OM), который создается при инициализации приложения. OM вызывается автоматически и его запрещено использовать напрямую, поскольку тогда зависимости перестают быть явными и весь смысл использования Dependency Injection теряется, однако, бывают и исключения, которые будут приведены ниже. А вот и код самого OM:

<?php
/**
 * Magento object manager. Responsible for instantiating objects taking into account:
 * - constructor arguments (using configured, and provided parameters)
 * - class instances life style (singleton, transient)
 * - interface preferences
 *
 * Intentionally contains multiple concerns for best performance
 *
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Magento\Framework\ObjectManager;

class ObjectManager implements \Magento\Framework\ObjectManagerInterface
{
    /**
     * @var \Magento\Framework\ObjectManager\FactoryInterface
     */
    protected $_factory;

    /**
     * List of shared instances
     *
     * @var array
     */
    protected $_sharedInstances = [];

    /**
     * @var ConfigInterface
     */
    protected $_config;

    /**
     * @param FactoryInterface $factory
     * @param ConfigInterface $config
     * @param array &$sharedInstances
     */
    public function __construct(FactoryInterface $factory, ConfigInterface $config, &$sharedInstances = [])
    {
        $this->_config = $config;
        $this->_factory = $factory;
        $this->_sharedInstances = &$sharedInstances;
        $this->_sharedInstances[\Magento\Framework\ObjectManagerInterface::class] = $this;
    }

    /**
     * Create new object instance
     *
     * @param string $type
     * @param array $arguments
     * @return mixed
     */
    public function create($type, array $arguments = [])
    {
        return $this->_factory->create($this->_config->getPreference($type), $arguments);
    }

    /**
     * Retrieve cached object instance
     *
     * @param string $type
     * @return mixed
     */
    public function get($type)
    {
        $type = ltrim($type, '\\');
        $type = $this->_config->getPreference($type);
        if (!isset($this->_sharedInstances[$type])) {
            $this->_sharedInstances[$type] = $this->_factory->create($type);
        }
        return $this->_sharedInstances[$type];
    }

    /**
     * Configure di instance
     * Note: All arguments should be pre-processed (sort order, translations, etc) before passing to method configure.
     *
     * @param array $configuration
     * @return void
     */
    public function configure(array $configuration)
    {
        $this->_config->extend($configuration);
    }
}

Приложение использует конструктор класса, чтобы получить информацию о зависимостях объекта. При инициализации, OM заполняет объект его зависимостями, определенными в специальном конфигурационном файле di.xml (тут про di.xml) и помещает их в конструктор класса. Если класс не найден, то возвращается исключение (Exception). Аргументы собираются рекурсивно, то есть зависимости заполняются не только на верхнем уровне, но и на всех низ лежащих (зависимости зависимостей) до того момента пока не будут подгружены все.

Итак, основные задачи OM:

  1. Создание объектов в фабриках и прокси-классах

  2. Возвращение singleton-инстанса класса, т.е. единственного экземпляра класса

  3. Автоматическая инициализация аргументов в конструкторах классов

А в файле di.xml:

  1. Происходит настройка OM для работы с зависимостями.

  2. Указывается предпочтительный класс реализации интерфейса (Service Contract), который потом передается в конструктор класса, а также позволяет подменять классы другими.

  3. Можно подменять аргументы классов своими, использовать прокси, виртуальные типы, плагины и т.д.

Когда можно использовать Object Manager?

Бывают случаи, когда использование OM оправдано:

  • при использовании магических методов __wakeup() и __sleep();

  • в тестах;

  • в классах-фабриках и прокси-классах.

Как быть, когда нужно несколько экземпляров класса?

Для таких случаев нужно применять фабрики. Фабрики необходимы для создания объектов.

Для того, чтобы ее использовать, нужно добавить нужный класс через use и в конце дописать ключевое слово Factory вот так:

use Magento\Framework\Controller\Result\RawFactory;

А затем просто подключить данный класс через конструктор.

public function __construct(RawFactory $resultRawFactory) {    
    $this->resultRawFactory = $resultRawFactory;
}

Далее, чтобы получить новый экземпляр класса, необходимо использовать публичный метод фабрики под названием create().

$resultRaw = $this->resultRawFactory->create();

Таким образом, мы получим необходимый экземпляр класса, а M2 сама создаст нужные объекты при переходе в production mode, либо на лету, если активен developer mode и положит его в папку /generated/code. Выглядеть сгенерированный класс будет таким образом:

<?php

namespace Magento\Framework\Controller\Result;

/**
 * Factory class for @see \Magento\Framework\Controller\Result\Raw
 */
class RawFactory
{
    /**
     * Object Manager instance
     *
     * @var \Magento\Framework\ObjectManagerInterface
     */
    protected $_objectManager = null;
  
    /**
     * Instance name to create
     *
     * @var string
     */
    protected $_instanceName = null;
  
    /**
     * Factory constructor
     *
     * @param \Magento\Framework\ObjectManagerInterface $objectManager
     * @param string $instanceName
     */
    public function __construct(
        \Magento\Framework\ObjectManagerInterface $objectManager,
         $instanceName = '\\Magento\\Framework\\Controller\\Result\\Raw'
    ) {
        $this->_objectManager = $objectManager;
        $this->_instanceName = $instanceName;
    }
  
    /**
     * Create class instance with specified parameters
     *
     * @param array $data
     * @return \Magento\Framework\Controller\Result\Raw
     */
    public function create(array $data = [])
    {
        return $this->_objectManager->create($this->_instanceName, $data);
    }
}

Как можно заметить из кода выше, в созданном классе используется object manager. Как упоминалось ранее, в классах-фабриках это допустимо.

Что такое preference?

Preference — это переопределение класса или интерфейса, а также связь интерфейса и его конкретной реализации.

Бывают случаи, когда мы хотим заменить какой-либо класс своей собственной имплементацией, что не очень желательно и лучше использовать плагины, когда это возможно, но тем не менее.

Preference также используется для определения Service Contract, чтобы при подключении класса в конструкторе мы использовали абстракцию, а не конкретный класс.

Выглядит это так, где for — это класс, который переопределяем, а type — класс, которым заменяем:

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Magento\Framework\Search\Adapter\OptionsInterface"
                type="Magento\CatalogSearch\Model\Adapter\Options"/>
</config>

Что такое Proxy?

Proxy (Заместитель) — это паттерн проектирования, который предполагает создание суррогата объекта и контроль доступа к нему.

В M2 данный подход применяется, если объект слишком “тяжелый”. Это позволяет подгружать его только в случае необходимости (ленивая загрузка), тем самым ускоряя работу приложения.

Для того, чтобы применить данный подход, нужно выполнить его настройку в di.xml. Например:

<type name="Magento\Framework\DataObject\Copy\Config">
    <arguments>
        <argument name="dataStorage" xsi:type="object">Magento\Framework\DataObject\Copy\Config\Data\Proxy</argument>
    </arguments>
</type>

В узле <type> указывается класс, в конструктор которого передается объект, на который будет создан прокси.

Далее в узле <arguments> мы перечисляем необходимые подменяемые на прокси аргументы. В данном случае аргумент dataStorage. В значении ноду указывается путь к классу и в конце через слеш добавляется \Proxy

Изменения выполняются только в конфигурационном файле di.xml. Внутри класса ничего делать не надо. M2 сама сгенерирует нужные классы и положит их в папку /generated

Что такое Virtual Type и Type?

Подход, который позволяет создавать существующий класс с кастомными аргументами конструктора. Таким образом, мы создаем как бы “виртуальный” класс, который имеет все те же свойства и методы родительского класса, но с новыми элементами конструктора без какого-либо влияния на изначальный класс, т.е “старый” класс может использоваться как и ранее. Определяется Virtual Type в конфигурационном файле di.xml:

<virtualType name="layoutFileSourceBase" 
             type="Magento\Framework\View\File\Collector\Base">    
  <arguments>        
    <argument name="subDir" xsi:type="string">layout</argument>    
  </arguments>
</virtualType>

В узле <virtualType> атрибут name содержит имя нашего виртуального типа, в атрибуте type указан “родительский” класс, в котором будут заменяться аргументы.

В узле <arguments> перечисляются аргументы, значения которых мы хотим заменить на свои. В данном случае аргумент subDir будет иметь значение layout.

Затем этот виртуальный тип можно подключить в какой-нибудь класс через тот же di.xml через узел <type> в качестве аргумента subject класса Magento\Framework\View\File\Collector\Decorator\ModuleOutput следующим образом:

<type name="Magento\Framework\View\File\Collector\Decorator\ModuleOutput">    
  <arguments>        
    <argument name="subject" xsi:type="object">layoutFileSourceBase</argument>    
  </arguments>
</type>

В отличие от Virtual Type, Type добавляет аргументы к реальному классу.


Мы рассмотрели как в Magento 2 используется Dependency Injection и основные способы его применения на практике. Такой подход упрощает процесс разработки, структурирует приложение.

Комментарии (0)