Как Magento 2 взаимодействует с Vue Storefront

от автора

Привет! Меня зовут Павел и я занимаюсь бэкенд разработкой. Как уже писал AndreyHabr, многие из наших проектов основаны на стеке Adobe Magento 2 (для краткости далее я буду называть ее M2) в качестве бэкенда и Vue Storefront (VS) в качестве фронтенда.

Я не буду подробно останавливаться на архитектуре стека VS/M2 — мы уже писали об этом ранее. Предлагаю ознакомиться с данной статьей для более полного понимания изложенного ниже.

Сегодня я расскажу о взаимодействии с VS изнутри M2: посмотрим на реализацию индексеров, обсудим особенности их работы, после чего я кратко расскажу как создать свой индексер для кастомной сущности.

Погнали!

Модули интеграции

Официальные модули интеграции M2 и VS разрабатывает компания Divante. Модули представлены в их репозитории на Github и распространяются под лицензией MIT. Пакет интеграционных модулей включает в себя:

  • Divante_ReviewApi;
  • Divante_VsbridgeIndexerCore;
  • Divante_VsbridgeIndexerCatalog;
  • Divante_VsbridgeIndexerCms;
  • Divante_VsbridgeIndexerTax;
  • Divante_VsbridgeIndexerReview;
  • Divante_VsbridgeIndexerAgreement;
  • Divante_VsbridgeDownloadable.

Несложно догадаться, что все модули кроме первого занимаются индексацией различных сущностей с целью размещения их в Elasticsearch, используемый VS в качестве хранилища сущностей. Модуль Divante_ReviewApi реализует API для работы с отзывами (создание, удаление, получение коллекций).

Из коробки модули обеспечивают индексацию следующих сущностей:

  • Категории и продукты каталога;
  • CMS страницы и блоки;
  • Отзывы;
  • Налоги;
  • Соглашения;
  • Ссылки на скачиваемые продукты.

Из этого следует, что связка M2 и VS прямо после установки способна выполнять все базовые задачи интернет-магазина и требует минимальной настройки для начала применения.

Индексируем!

Посмотрим вблизи на реализацию индексеров. Для примера возьмем индексер CMS Page из стандартной поставки Divante. В целом, индексеры VSBridge работают на тех же принципах, что и другие индексеры в M2.

Объявляется новый индексер:

<indexer id="vsbridge_cms_page_indexer" view_id="vsbridge_cms_page_indexer" class="Divante\VsbridgeIndexerCms\Model\Indexer\CmsPage">     <title translate="true">Vsbridge Cms Page Indexer</title>     <description translate="true">Update Cms Pages in Elastic</description> </indexer>

etc/indexer.xml

Где объявляется класс-индексер. Данный класс реализует общий для всех индексеров интерфейс \Magento\Framework\Indexer\ActionInterface, поэтому функционально схож с остальными индексерами M2. Его задача — запуск индексации в разных ситуациях. Здесь нас интересует свойство $cmsPageAction, содержащее Action класс, извлекающий сущности для индексации:

public function execute($ids) {     $stores = $this->storeManager->getStores();      foreach ($stores as $store) {         $this->indexHandler->saveIndex($this->cmsPageAction->rebuild($store->getId(), $ids), $store);         $this->indexHandler->cleanUpByTransactionKey($store, $ids);         $this->cacheProcessor->cleanCacheByTags($store->getId(), ['cmsPage']);     } }

Model/Indexer/CmsPage.php

Action класс содержит единственный метод rebuild, который получает коллекцию и готовит ее для индексации:

public function rebuild($storeId = 1, array $pageIds = []) {     $lastPageId = 0;      do {         $cmsPages = $this->resourceModel->loadPages($storeId, $pageIds, $lastPageId);          foreach ($cmsPages as $pageData) {             $lastPageId = (int)$pageData['page_id'];             $pageData['id'] = $lastPageId;             $pageData['content'] = $pageData['content'];             $pageData['active'] = (bool)$pageData['is_active'];              if (isset($pageData['sort_order'])) {                 $pageData['sort_order'] = (int)$pageData['sort_order'];             }              unset($pageData['creation_time'], $pageData['update_time'], $pageData['page_id']);             unset($pageData['created_in']);             unset($pageData['is_active'], $pageData['custom_theme'], $pageData['website_root']);              yield $lastPageId => $pageData;         }     } while (!empty($cmsPages)); }

Model/Indexer/Action/CmsPage.php

Обратите внимание, что коллекция элементов извлекается методом loadPages ресурсной модели следующим образом:

public function loadPages($storeId = 1, array $pageIds = [], $fromId = 0, $limit = 1000) {     $metaData = $this->getCmsPageMetaData();     $linkFieldId = $metaData->getLinkField();      $select = $this->getConnection()->select()->from(['cms_page' => $metaData->getEntityTable()]);     $select->join(         ['store_table' => $this->resource->getTableName('cms_page_store')],         "cms_page.$linkFieldId = store_table.$linkFieldId",         []     )->group("cms_page.$linkFieldId");      $select->where(         'store_table.store_id IN (?)',         [             Store::DEFAULT_STORE_ID,             $storeId,         ]     );      if (!empty($pageIds)) {         $select->where('cms_page.page_id IN (?)', $pageIds);     }      $select->where('is_active = ?', 1);     $select->where('cms_page.page_id > ?', $fromId)         ->limit($limit)         ->order('cms_page.page_id');      return $this->getConnection()->fetchAll($select); }

Model/ResourceModel/CmsPage.php

Данный метод может извлекать данные итеративно, коллекциями по 1000 элементов, либо только определенный набор элементов с IDs, указанными в переменной $pageIds.

Полученную коллекцию IndexHandler класса Indexer сохраняет в базу elasticsearch:

public function saveIndex(Traversable $documents, StoreInterface $store) {     try {         $index = $this->getIndex($store);         $storeId = (int)$store->getId();         $batchSize = $this->indexOperations->getBatchIndexingSize();          foreach ($this->batch->getItems($documents, $batchSize) as $docs) {             foreach ($index->getDataProviders() as $dataProvider) {                 if (!empty($docs)) {                     $docs = $dataProvider->addData($docs, $storeId);                 }             }              if (!empty($docs)) {                 $bulkRequest = $this->indexOperations->createBulk()->addDocuments(                     $index->getName(),                     $index->getType(),                     $docs                 );                  $this->indexOperations->optimizeEsIndexing($storeId, $index->getName());                 $response = $this->indexOperations->executeBulk($storeId, $bulkRequest);                 $this->indexOperations->cleanAfterOptimizeEsIndexing($storeId, $index->getName());                 $this->bulkLogger->log($response);             }              $docs = null;         }          if ($index->isNew()) {             $this->indexOperations->switchIndexer($store->getId(), $index->getName(), $index->getIdentifier());         }          $this->indexOperations->refreshIndex($store->getId(), $index);     } catch (ConnectionDisabledException $exception) {         // do nothing, ES indexer disabled in configuration     } catch (ConnectionUnhealthyException $exception) {         $this->indexerLogger->error($exception->getMessage());         $this->indexOperations->cleanAfterOptimizeEsIndexing($storeId, $index->getName());         throw $exception;     } }

Модуль Divante_VsbridgeIndexerCore: Indexer/GenericIndexerHandler.php

На этом этапе важно упомянуть класс Mapper, который задает типы полей объекта для elasticsearch.

public function getMappingProperties() {     $properties = [         'id' => ['type' => FieldInterface::TYPE_LONG],         'active' => ['type' => FieldInterface::TYPE_BOOLEAN],         'sort_order' => ['type' => FieldInterface::TYPE_LONG],         //compatible with product/category attribute mapping         'page_layout' => ['type' => FieldInterface::TYPE_KEYWORD],         'identifier' => ['type' => FieldInterface::TYPE_KEYWORD],     ];      foreach ($this->textFields as $field) {         $properties[$field] = ['type' => FieldInterface::TYPE_TEXT];     }      $mappingObject = new \Magento\Framework\DataObject();     $mappingObject->setData('properties', $properties);      $this->eventManager->dispatch(         'elasticsearch_cms_page_mapping_properties',         ['mapping' => $mappingObject]     );      return $mappingObject->getData(); }

Index/Mapping/CmsPage.php

Это может быть необходимо, в том числе, если поле должно участвовать в полнотекстовом поиске — в этом случае полю надо задать тип keyword.

Данный класс используется в файле vsbridge_indices.xml, который задает идентификатор индекса и класс Mapper для полей объектов в индексе:

<index identifier="cms_page" mapping="Divante\VsbridgeIndexerCms\Index\Mapping\CmsPage">     <data_providers>         <data_provider name="content">Divante\VsbridgeIndexerCms\Model\Indexer\DataProvider\Page\ContentData</data_provider>     </data_providers> </index>

etc/vsbridge_indices.xml

Полная индексация элементов производится путем выполнения команды bin/magento indexer:reindex, которая последовательно запускает все индексеры М2. Также можно запустить конкретный индексер, указав его идентификатор в параметрах команды: bin/magento indexer:reindex <indexer_identifier>.

Из описанной процедуры индексации следуют несколько важных выводов:

  • Объект в elasticsearch не обязан полностью отражать объект в БД M2. Его можно сократить, убрав некоторые поля, или наоборот дополнить — к примеру, на этапе выборки приджоинить таблицу с дополнительными данными. Также данные можно модифицировать после выборки. Это дает свободу и удобство при размещении объектов в elasticsearch, например если нужно разрешать поля с foreign ключами в их значения;
  • Объект для индексации не обязательно доставать из базы — он может быть взят откуда угодно, к примеру, получен через запрос к внешнему источнику;
  • Выборку можно производить посредством стандартных инструментов М2, например через репозитории или коллекции, а не сырым запросом в БД, как в примере выше.

Актуализация данных в elasticsearch

Конечно же, запускать полную реиндексацию каждый раз, когда какой либо объект меняется в базе, было бы расточительно. Для отслеживания изменения объектов в базе и их актуализацией в elasticsearch следит механизм M2, который называется mview. Данный механизм состоит из триггеров в таблице, за которой производится наблюдение, индексной таблицы в которой фиксируются ID измененных сущностей, а также cron-задачи, которая считывает ID измененных сущностей из индексной таблицы и запускает индексер с указанием этих ID (вспомним, что метод loadPages ресурсной модели может принимать в качестве аргумента массив ID). Рассмотрим каждый из элементов механизма подробнее.

Создание триггеров инициируется созданием файла mview.xml:

<view id="vsbridge_cms_page_indexer" class="Divante\VsbridgeIndexerCms\Model\Indexer\CmsPage" group="indexer">     <subscriptions>         <table name="cms_page" entity_column="page_id"/>     </subscriptions> </view>

etc/mview.xml

После присвоения индексеру статуса Scheduled указанная в файле таблица приобретает триггеры на добавление, изменение и удаление объектов в таблице. ID модифицированного объекта добавляется индексную таблицу с указанием версии изменений:

Крон-задача один раз в минуту (по умолчанию) делает выборку из этих таблиц, составляет массив ID измененных объектов, после чего запускает соответствующий индексер. Таким образом, данные в elasticsearch постоянно сохраняют актуальность максимально экономичным способом.

Добавляем индексер для своей сущности

Главным достоинством этого механизма является способность к расширению. Представим, что у нас есть сущность, которую необходимо разместить в эластике. Назовем ее whiteRabbit. Создадим для ее индексации стандартный M2 модуль, после чего создадим свой индексер:

<?xml version="1.0" encoding="UTF-8"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Indexer/etc/indexer.xsd">     <indexer id="vsbridge_white_rabbit_indexer" view_id="vsbridge_white_rabbit_indexer"              class="RSHB\WhiteRabbit\Model\Indexer\WhiteRabbit">         <title translate="true">White Rabbits Indexer</title>         <description translate="true">Update White Rabbits in Elastic</description>     </indexer> </config>

etc/indexer.xml

А также сразу зададим таблицу за которой будем следить и идентификатор индекса, в котором будут храниться наши объекты:

<?xml version="1.0" encoding="UTF-8"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:noNamespaceSchemaLocation="urn:magento:framework:Mview/etc/mview.xsd">     <view id="vsbridge_white_rabbit_indexer" class="RSHB\WhiteRabbit\Model\Indexer\WhiteRabbit" group="indexer">         <subscriptions>             <table name="rshb_white_rabbit” entity_column="id" />         </subscriptions>     </view> </config>

etc/mview.xml

<?xml version="1.0" encoding="UTF-8"?> <indices xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Divante_VsbridgeIndexerCore:etc/vsbridge_indices.xsd">     <index identifier="white_rabbit" mapping="RSHB\WhiteRabbit\Index\Mapping\WhiteRabbit" /> </indices>

etc/vsbridge_indices.xml

Создадим индексер, маппер и экшен:

<?php namespace RSHB\WhiteRabbit\Model\Indexer;  use Divante\VsbridgeIndexerCore\Indexer\StoreManager; use Exception; use Magento\Framework\Indexer\ActionInterface as IndexerActionInterface; use Magento\Framework\Mview\ActionInterface as MviewActionInterface; use Divante\VsbridgeIndexerCore\Indexer\GenericIndexerHandler as IndexerHandler; use RSHB\WhiteRabbit\Model\Indexer\Action\WhiteRabbit as WhiteRabbitAction;  /**  * Class WhiteRabbit  * @package RSHB\WhiteRabbit\Model\Indexer  */ class WhiteRabbit implements IndexerActionInterface, MviewActionInterface {     /**      * @var IndexerHandler      */     private $indexHandler;      /**      * @var WhiteRabbitAction      */     private $WhiteRabbitAction;      /**      * @var StoreManager      */     private $storeManager;      /**      * WhiteRabbit constructor.      * @param IndexerHandler $indexerHandler      * @param WhiteRabbitAction $action      * @param StoreManager $storeManager      */     public function __construct(         IndexerHandler $indexerHandler,         WhiteRabbitAction $action,         StoreManager $storeManager     ) {         $this->indexHandler = $indexerHandler;         $this->whiteRabbitAction = $action;         $this->storeManager = $storeManager;     }      /**      * @inheritdoc      * @throws Exception      */     public function execute($ids)     {         $stores = $this->storeManager->getStores();          foreach ($stores as $store) {             $this->indexHandler->saveIndex($this->whiteRabbitAction->rebuild($ids), $store);             $this->indexHandler->cleanUpByTransactionKey($store, $ids);         }     }      /**      * @inheritdoc      * @throws Exception      */     public function executeFull()     {         $stores = $this->storeManager->getStores();         foreach ($stores as $store) {             $this->indexHandler->saveIndex($this->whiteRabbitAction->rebuild(), $store);             $this->indexHandler->cleanUpByTransactionKey($store);         }     }      /**      * @inheritdoc      * @throws Exception      */     public function executeList(array $ids)     {         $this->execute($ids);     }      /**      * @inheritdoc      * @throws Exception      */     public function executeRow($id)     {         $this->execute([$id]);     } }

Model/Indexer/WhiteRabbit.php

<?php namespace RSHB\WhiteRabbit\Index\Mapping;  use Divante\VsbridgeIndexerCore\Api\Mapping\FieldInterface; use Divante\VsbridgeIndexerCore\Api\MappingInterface; use Magento\Framework\DataObject; use Magento\Framework\Event\ManagerInterface as EventManager;  /**  * Class WhiteRabbit  * @package RSHB\WhiteRabbit\Index\Mapping  */ class WhiteRabbit implements MappingInterface {     /**      * @var EventManager      */     private $eventManager;      /**      * WhiteRabbit constructor.      * @param EventManager $eventManager      */     public function __construct(         EventManager $eventManager     ) {         $this->eventManager = $eventManager;     }      /**      * @inheritdoc      */     public function getMappingProperties()     {         $properties = [             'id' => ['type' => FieldInterface::TYPE_LONG]         ];          $mappingObject = new DataObject();         $mappingObject->setData('properties', $properties);          $this->eventManager->dispatch(             'elasticsearch_white_rabbit_mapping_properties',             ['mapping' => $mappingObject]         );          return $mappingObject->getData();     } }

Index/Mapping/WhiteRabbit.php

<?php namespace RSHB\WhiteRabbit\Model\Indexer\Action;  use RSHB\WhiteRabbit\Model\ResourceModel\WhiteRabbit as WhiteRabbitResource; use Traversable;  /**  * Class WhiteRabbit  * @package RSHB\WhiteRabbit\Model\Indexer\Action  */ class WhiteRabbit {     /**      * @var WhiteRabbitResource      */     private $resourceModel;      public function __construct(         WhiteRabbitResource $resourceModel     ) {         $this->resourceModel = $resourceModel;     }      /**      * Rebuild      *      * @param array $ids      * @return Traversable      */     public function rebuild(array $ids = [])     {         $lastId = 0;          do {             $rabbits = $this->resourceModel->load($ids, $lastId);              foreach ($rabbits as $rabbit) {                 $lastId = (int)$rabbit['id'];                 $rabbitData['id'] = $lastId;                 $rabbitData = $rabbit;                  yield $lastId => $rabbitData;             }         } while (!empty($rabbits));     } }

Model/Indexer/Action/WhiteRabbit.php

А также ресурсную модель:

<?php namespace RSHB\WhiteRabbit\Model\ResourceModel;  use Magento\Framework\App\ResourceConnection;  use Magento\Framework\DB\Adapter\AdapterInterface;  /**  * Class WhiteRabbit  * @package RSHB\WhiteRabbit\Model\ResourceModel  */ class WhiteRabbit {     /**      * @var ResourceConnection      */     private $resource;      /**      * Organization constructor.      * @param ResourceConnection $resourceConnection      */     public function __construct(         ResourceConnection $resourceConnection     ) {         $this->resource = $resourceConnection;     }      /**      * @param array $ids      * @param int $fromId      * @param int $limit      * @return array      */         public function load($ids, $fromId, $limit = 1000)     {         $select = $this->getConnection()->select()->from('rshb_white_rabbit');          if (!empty($ids)) {             $select->where('id IN (?)', $ids);         }          $select->where('id > ?', $fromId)             ->order('id')             ->limit($limit);          return $this->getConnection()->fetchAll($select);     }      /**      * @return AdapterInterface      */     private function getConnection()     {         return $this->resource->getConnection();     } }

Model/ResourceModel/WhiteRabbit.php

Не забудем указать идентификатор индекса для класса IndexerHandler, для чего воспользуемся виртуальным типом:

... <virtualType name="RSHB\WhiteRabbit\Indexer\WhiteRabbitIndexOperationsVirtual"              type="Divante\VsbridgeIndexerCore\Indexer\GenericIndexerHandler">     <arguments>         <argument name="indexIdentifier" xsi:type="string">white_rabbit</argument>         <argument name="typeName" xsi:type="string">white_rabbit</argument>         <argument name="alias" xsi:type="string">vue_storefront_white_rabbit</argument>     </arguments> </virtualType>  <type name="RSHB\WhiteRabbit\Model\Indexer\WhiteRabbit">     <arguments>         <argument name="indexerHandler" xsi:type="object">             RSHB\WhiteRabbit\Indexer\WhiteRabbitIndexOperationsVirtual         </argument>     </arguments> </type> ...

etc/di.xml

И, в качестве последнего штриха, создадим дата патч, который включит для нашего вновь созданного индексера режим индексации по крону:

<?php namespace RSHB\WhiteRabbit\Setup\Patch\Data;  use Exception; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\Setup\Patch\DataPatchInterface; use Psr\Log\LoggerInterface;  /**  * Class SetWhiteRabbitIndexerScheduleMode  * @package RSHB\WhiteRabbit\Setup\Patch\Data  */ class SetWhiteRabbitIndexerScheduleMode implements DataPatchInterface {     /**      * @var LoggerInterface      */     private $logger;      /**      * @var IndexerRegistry      */     private $indexerRegistry;      /**      * SetWhiteRabbitIndexerScheduleMode constructor.      * @param LoggerInterface $logger      * @param IndexerRegistry $indexerRegistry      */     public function __construct(         LoggerInterface $logger,         IndexerRegistry $indexerRegistry     ) {         $this->logger = $logger;         $this->indexerRegistry = $indexerRegistry;     }      /**      * @return DataPatchInterface|void      */     public function apply()     {         try {             $indexer = $this->indexerRegistry->get('vsbridge_white_rabbit_indexer');             $indexer->setScheduled(true);         } catch (Exception $e) {             $this->logger->critical($e);         }     }      /**      * @inheritDoc      */     public static function getDependencies()     {         return [];     }      /**      * @inheritDoc      */     public function getAliases()     {         return [];     } }

Setup/Patch/Data/SetWhiteRabbitIndexerScheduleMode.php

На этом наш индексер готов.

Подведение итогов

Механизм индексации данных позволяет действовать M2 и VS автономно, обеспечивая слабую связанность и, как следствие, повышение надежности и быстродействия. Гибкий механизм масштабирования позволяет использовать elasticsearch для хранения разнообразной информации, а скорость выборки и поиска позволяет фонтенду работать быстро.

Напоследок хотелось бы заострить внимание на некоторых нюансах работы индексеров, с которыми мы сталкиваись в процессе работы:

  • Если у объекта меняется набор полей или их типы, необходимо удалить индекс из ealasticsearch и запустить полную индексацию. Это происходит по причине несоответствия типов полей новых объектов с теми, что уже есть в индексе;
  • Индексация не запустится, если данные в таблицу попадают при помощи дампа. В этом случае также нужно запустить индексацию вручную;
  • Если в процессе индексации в объект elasticsearch включаются данные из другого объекта (к примеру, разрешается foreign ключ в его значение при помощи join`а таблицы) то необходимо отслеживать изменения этой таблицы и обеспечивать внесение в индексную таблицу ID элементов, для которых изменились данные, связанные foreign ключами.

На этом все. Пишите код с удовольствием, используйте правильные решения и да пребудет с вами сила.

ссылка на оригинал статьи https://habr.com/ru/company/rshb/blog/521692/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *