IT-Storm

Здравый смысл - всему голова, ибо даже мудрец не сможет оспорить его и при этом, остаться мудрецом

Menu

Magento 2: Object Manager: экземпляры объектов, фабрики

Magento 2: Object Manager: экземпляры объектов, фабрики

Это был долгий и извилистый путь, но конец близок! В этой, предпоследней статье нашего руководства по object manager (диспетчеру объектов), мы собираемся обсудить работу с  Instance Objects  (экземплярами объектов) и non-injectable (не внедряемыми) объектами в Magento 2.
Эта статья предполагает базовое знакомство с концепциями Magento 2. Ещё, рассмотрим Factories (фабрики). Если вы вдруг запутаетесь, вы можете начать с самого начала.

Давайте приступим!

Образец кода
Мы подготовили небольшой образец модуля Magento 2, на который будем ссылаться и который будем использовать в этой статье. Модуль находится на Bitbucket, рекомендуем скачать его (без клонирования) и скопировать в ваш каталог с модулями Magento 2.
Чтобы проверить правильность установки модуля, попробуйте выполнить следующие команды:

bin/magento module:enable Pulsestorm_TutorialInstanceObjects
bin/magento ps:tutorial-instance-objects
Если вы видите, сообщение: You've installed Pulsestorm_TutorialInstanceObjects!, все готово.
 

Объекты: Shared/Unshared, Singleton/Instance

Ранее мы представили два метода диспетчера объектов для создания экземпляров объектов.
$object  = $manager->create('Pulsestorm\TutorialInstanceObjects\Model\Example');
$object  = $manager->get('Pulsestorm\TutorialInstanceObjects\Model\Example');
Метод create будет создавать экземпляр нового объекта при каждом вызове. Метод get создаст экземпляр объекта один раз, а последующие вызовы get будут возвращать тот же объект (то есть это классический Singleton). Это поведение похоже на фабрики getModel и getSingleton в Magento 1.
Mage::getModel('group/class');         // ->create, or instance object
Mage::getSingleton('group/class');     // ->get, or instance object
Чего мы не рассмотрели, так это того, как dependency injection system (автоматическая система внедрения зависимостей) конструктора решает, какой метод использовать, когда встречает параметр конструктора. Рассмотрим конструктор, который выглядит следующим образом.
//...
use Pulsestorm\TutorialInstanceObjects\Model\Example;
//...
public function __construct(Example $example)
{
    //is $example created with `get` or `create`?
    $this->example = $example?
}
Мы знаем, что параметр конструктора будет объектом Pulsestorm\TutorialInstanceObjects\Model\Example, но мы не знаем, будет ли это новый экземпляр объекта Pulsestorm\TutorialInstanceObjects\Model\Example или Singleton - тот же объект Pulsestorm\TutorialInstanceObjects\Model\Example который (возможно) передавался/вводился в другие конструкторы.

По умолчанию все объекты, созданные с помощью автоматического внедрения зависимостей конструктора, являются «singleton-ish» (одноэлементными) объектами, то есть они создаются с помощью метода get диспетчера объектов (object manager).

Если вам нужен новый экземпляр объекта, т. е. вы хотите, чтобы диспетчер объектов использовал create, вам потребуется добавить некоторую дополнительную конфигурацию <type/> в файл di.xml вашего модуля.
<!-- File: app/code/Pulsestorm/TutorialInstanceObjects/etc/di.xml --> 
<config>
    <!-- ... -->
    <type name="Pulsestorm\TutorialInstanceObjects\Model\Example" shared="false">
        <!-- ... arguments/argument tags here if you want to change injected arguments -->
    </type>
</config>
Это тот же тег <type/>, который мы видели в нашем руководстве по замене аргументов. Атрибут name должен быть именем класса, поведение которого вы хотите изменить.
Новым для нас атрибутом здесь является "shared" (общий\доступный). Если для параметра shared установлено значение false, то Magento 2, каждый раз, когда он встречает Pulsestorm\TutorialObjectManager1\Model\Example в качестве автоматически внедряемого аргумента конструктора - будет использовать метод create для создания экземпляра объекта . Атрибут shared не влияет на объекты, созданные непосредственно с помощью ключевого слова PHP "new" или с помощью двух методов диспетчера объектов (create\get).

Этот атрибут называется "shared" из-за деталей реализации в ObjectManager (диспетчере объектов). Когда вы используете get для создания экземпляра объекта, диспетчер объектов сохраняет все уже созданные экземпляры объектов в массиве _sharedInstances.
#File: lib/internal/Magento/Framework/ObjectManager/ObjectManager.php
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];
}
Когда вы настраиваете определенный тип (то есть определенный класс PHP) с shared="false", вы сообщаете Magento 2, что не хотите использовать этот массив _sharedInstances.

Итак, все что вам нужно помнить, это то, что по умолчанию используется shared="true", и вы получите одноэлементный/глобальный объект - Singleton. Если вы измените конфигурацию вашего типа на shared="false", автоматическая система внедрения зависимостей - начнет создавать новый экземпляр для параметра конструктора каждый раз, когда программист создает экземпляр объекта, классу которого назначен атрибут shared="false".
 

Magento 2 Factories (Фабрики)

Хотя shared атрибут является полезным в тех случаях, когда вам нужно внедрить зависимость, чтобы она была совершенно новым объектом экземпляра, это не является идеальным решением для всех (или для большинства) случаев, когда вам не нужны Singleton (синглтоны).

Одна проблема с share (общим) доступом заключается в том, что введенная зависимость по-прежнему зависит от того, является ли внедряемый объект общим или необщим (shared or un-shared). Во многих случаях вам просто нужен новый экземпляр объекта, и возможно вам будет трудно (или даже невозможно) реорганизовать свои объектные настройки для этого.

Прекрасным примером этого являются объекты данных CRUD, такие как объекты страницы CMS Magento или объекты продукта каталога. В Magento 1 вы бы создали объект страницы CMS, подобный этому:
Mage::getModel('page/cms')->load($id);
В Magento 2 такие объекты называются «non-injectables» (неинъекционными). Внедрение зависимостей предназначено для объектов, которые «делают что-то» или «предоставляют какую-то услугу». Но эти «non-injectables» объекты данных - предназначены для «идентификации конкретной вещи». Далее мы рассмотрим, как использовать такие объекты без автоматического внедрения зависимостей в конструктор, связывающего нам руки.

Во-первых, ничто не мешает вам напрямую создать экземпляр объекта с помощью нового метода PHP.
$product = new \Magento\Cms\Model\Page;
Однако, сделать это, ваш объект страницы CMS потеряет все функции диспетчера объектов Magento.

К счастью, разработчики ядра Magento 2 не оставляют нас. В Magento 2 экземпляры таких "non-injectable" (невнедряемых) объектов создаются через "factory objects" (фабричные объекты). Вот как это делается в Magento 2, пример заменит 1000 слов:
#File: app/code/Pulsestorm/TutorialInstanceObjects/Command/Testbed.php

public function __construct(
    \Magento\Cms\Model\PageFactory $pageFactory = 
)
{
    $this->pageFactory = $pageFactory;
    return parent::__construct();
}
//...
public function execute(InputInterface $input, OutputInterface $output)
{
    $page = $this->pageFactory->create();
    foreach($page->getCollection() as $item)
    {
        $output->writeln($item->getId() . '::' . $item->getTitle());
    }

    $page = $this->pageFactory->create()->load(1);        
    var_dump($page->getData());
}
Вот что происходит: в методе __constructor выполняется команда автоматического внедрения зависимостеи конструктора Magento для создания объекта Magento\Cms\Model\PageFactory, а затем (согласно соглашению Magento) присваивает этот объект свойству pageFactory:
#File: app/code/Pulsestorm/TutorialInstanceObjects/Command/Testbed.php
public function __construct(
    \Magento\Cms\Model\PageFactory $pageFactory 
)
{
    $this->pageFactory = $pageFactory;
}
Затем в методе execute, мы используем этот объект фабрики для создания объекта страницы CMS (используя метод создания фабрики create):
$page = $this->pageFactory->create();
В Magento 1, приведенное выше эквивалентно примерно следующему:
$page = Mage::getModel('cms/page');
В Magento 1 у нас был набор фабричных методов (getModel, helper, createBlock). В Magento 2 — у каждого неинжектируемого объекта есть свой фабричный объект.

Фабрики существуют для любого класса модели. Получить фабрику, можно напечатав Factory после имени класса. Выше мы хотели создать объекты Magento\Cms\Model\Page, поэтому фабричный класс был:
Object we Want: Magento\Cms\Model\Page
Factory to Use: Magento\Cms\Model\PageFactory
Аналогично можно получить фабрику для объекта продукта:
Object we Want: Magento\Catalog\Model\Product         
Factory to Use: Magento\Catalog\Model\ProductFactory
Как только мы создадим экземпляр класса с помощью фабрики, нам станут доступны большинство (если не все) нашх старыл методов Magento 1 CRUD, таких как load, getData, getCollection и т. д.
#File: app/code/Pulsestorm/TutorialInstanceObjects/Command/Testbed.php
$page = $this->pageFactory->create();
foreach($page->getCollection() as $item)
{
    $this->output($item->getId() . '::' . $item->getTitle());
}

$page = $this->pageFactory->create()->load(1);
Может потребоваться некоторое время, чтобы привыкнуть к этому, но по сравнению с подверженной ошибкам конфигурацией XML, необходимой для использования фабричных методов Magento 1, это уже большая победа.
 

Factory Definitions (Фабричные определения) и генерация кода

И последнее, что нужно рассказать о фабричных объектах в Magento 2, что может ответить на несколько вопросов в вашей голове.

Фу! Мне нужно определить кучу кода фабричных классов? Вы с ума сошли?.
Где я могу увидеть, как выглядит фабричный объект?

Если мы начнем со второго и более взрослого вопроса, вас может ожидать небольшой сюрприз. Основываясь на полном имени класса фабрики (Magento\Cms\Model\PageFactory), вы можете найти его в одном из следующих мест:
app/code/Magento/Cms/Model/PageFactory.php
lib/internal/Magento/Cms/Model/PageFactory.php
Однако ни один из этих файлов не существует.

Это потому, что Magento 2 использует автоматическую генерацию кода для создания фабричных классов. Возможно, вы помните эту генерацию кода из статьи про прокси-объект. Если вы действительно запускали приведенный выше код, вы найдете класс PageFactory в следующем месте:
var/generation/Magento/Cms/Model/PageFactory.php
Хотя подробности выходят за рамки этой статьи, но всякий раз, когда PHP встречает имя класса, оканчивающееся на Factory и автозагрузчик (autoloader) не может загрузить этот класс (потому что нет соответствующих определения для файла такого класса) - Magento 2 автоматически создаст фабрику.
Более подробно об этом процессе можно узнать здесь (magento.stackexchange.com).
 

Factories for All (Фабрики для всех)

Фабрики предназначены не только для core (основного) кода Magento — они будут работать с любым классом модуля. Образец модуля, который вы установили, включает объект Pulsestorm\TutorialInstanceObjects\Model\Example. Давайте заменим метод __construct на тот, который добавляет фабричный класс для объекта Example:
//...
use Pulsestorm\TutorialInstanceObjects\Model\ExampleFactory;
//...
class Testbed extends Command
{
    protected $exampleFactory;
    public function __construct(ExampleFactory $example)
    {
        $this->exampleFactory = $example;
        return parent::__construct();
    }
}
Затем мы используем эту фабрику в методе execute:
protected function execute(InputInterface $input, OutputInterface $output)
{
    $example = $this->exampleFactory->create();
    $output->writeln(
        "You just used a"                . "\n\n    "
        get_class($this->exampleFactory) . "\n\n" . 
        "to create a \n\n    "           . 
        get_class($example) . "\n"); 
}
Запустив нашу команду с указанным выше execute методом, и вы должны увидеть следующее:
$ php bin/magento ps:tutorial-instance-objects
You just used a

    Pulsestorm\TutorialInstanceObjects\Model\ExampleFactory

to create a 

    Pulsestorm\TutorialInstanceObjects\Model\Example
Как видите, этот код отработал без проблем, несмотря на то, что мы никогда не определяли класс Pulsestorm\TutorialInstanceObjects\Model\ExampleFactory. Вы можете найти определение фабрики в папке сгенерированного кода (var/generation).
#File: var/generation/Pulsestorm/TutorialInstanceObjects/Model/ExampleFactory.php
<?php
namespace Pulsestorm\TutorialInstanceObjects\Model;

/**
 * Factory class for @see \Pulsestorm\TutorialInstanceObjects\Model\Example
 */
class ExampleFactory
{
    protected $_objectManager = null;

    protected $_instanceName = null;

    public function __construct(
        \Magento\Framework\ObjectManagerInterface $objectManager, 
        $instanceName = '\\Pulsestorm\\TutorialInstanceObjects\\Model\\Example'
    )
    {
        $this->_objectManager = $objectManager;
        $this->_instanceName = $instanceName;
    }

    public function create(array $data = array())
    {
        return $this->_objectManager->create($this->_instanceName, $data);
    }
}
Что касается конкретной реализации — прямо сейчас метод create фабрики принимает массив параметров и использует диспетчер объектов для создания объекта. Однако будущие версии Magento могут изменить работу этих фабрик. Благодаря тому, что фреймворк генерирует эти фабрики, Magento 2 избавляет нас от чреватой ошибками работы по кодированию этого шаблона, а основная команда сохраняет контроль над тем, как работают фабрики.

В завершение
Завершив эту статью и ее шесть предшественников, мы получили довольно хорошее представление об объектной системе Magento, о том, как эта система влияет на форму кодовой базы Magento, и (что наиболее важно) базовое понимание, которое имеет решающее значение, если мы хотим разумно писать код для Magento 2 в реальных проектах.

Однако есть еще одна вещь, которую нам нужно рассмотреть напоследок, и это система плагинов объектов. Система плагинов является истинным преемником системы перезаписи классов Magento 1, и, обладая глубоким пониманием диспетчера объектов и автоматического внедрения зависимостей конструктора, мы готовы заняться этим в следующий раз в нашем последнем руководстве по диспетчеру объектов.

Magento 2