В предыдущей статье мы рассмотрели базовую структуру модуля в 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:
Создание объектов в фабриках и прокси-классах
Возвращение singleton-инстанса класса, т.е. единственного экземпляра класса
Автоматическая инициализация аргументов в конструкторах классов
А в файле di.xml:
Происходит настройка OM для работы с зависимостями.
Указывается предпочтительный класс реализации интерфейса (Service Contract), который потом передается в конструктор класса, а также позволяет подменять классы другими.
Можно подменять аргументы классов своими, использовать прокси, виртуальные типы, плагины и т.д.
Когда можно использовать 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 и основные способы его применения на практике. Такой подход упрощает процесс разработки, структурирует приложение.