Репозитории Magento 2, интерфейсы и веб-API

Magento 2 представиляет репозитории для большинства основных объектов, таких как продукты, заказы, клиенты и т. д.

В этом посте хотелось бы объяснить причины, по которым вы должны хотеть или не хотеть создавать репозитории для своих пользовательских объектов, и, надеюсь, показать, как их создать, если вы придите к выводу, что для вас  это имеет смысл.

Репозитории Magento 2

Отказ от ответственности:

Этот пост описывает только прагматичный подход для создания репозиториев для настраиваемых объектов для сторонних модулей.

Основные команды Magento имеют свои собственные стандарты, описанное ниже может им не соответствовать.

В общем, цель репозитория заключается в том, чтобы скрыть логику, связанную с хранением.

Клиента репозитория не должно волновать, сохраняется ли возвращенный объект в памяти в виде массива, извлекается из базы данных MySQL, извлекается из удаленного API или из файла.

Я полагаю, что команда Magento сделала это, чтобы в дальнейшем иметь возможность сменить или заменить ORM. В Magento ORM в настоящее время состоит из моделей,ресурс-моделей и коллекций.

Если сторонний модуль использует только репозитории, то Magento может изменить то как и где хранятся данных, а модуль будет продолжать работать, несмотря на эти глубокие изменения.

У репозиториев есть такие методы, как findById (), findByName (), put () или remove ().

В Magento они обычно называются getbyId (), save () и delete (), и не делают ничего другого, кроме как совершают операции CRUD DB.

Методы репозитория Magento 2 могут быть легко представлены в виде ресурсов API, что делает их ценными для интеграции с системами сторонних производителей или другими экземплярами Magento.

Должен ли я добавить репозиторий для моего настраиваемого объекта?

Хороший вопрос. Давайте добавим слово «Зачем», чтобы сделать его еще лучше:

«Зачем мне добавлять репозиторий для моего настраиваемого объекта?».

Как всегда, ответ

«Все относительно».

Короче говоря, если ваши объекты будут использоваться другими модулями, то да, вам, вероятно, стоит добавить репозиторий.

Есть еще один фактор, который следует добавить в уравнение: в Magento 2 репозитории могут быть легко представлены как Web API — это ресурсы REST и SOAP.

Если это интересно для вас из-за необходимости интеграции со сторонними системами или другой Magento, то опять же, да, вы, вероятно, захотите добавить репозиторий для своей сущности.

Как добавить репозиторий для моего настраиваемого объекта?

Предположим, вы хотите использовать свою сущность как часть REST API. Если это не так, вы можете пропустить все что касается создания интерфейсов и перейти прямо к «Создание репозитория и реализации модели данных» ниже.

Создание интерфейсов репозитория и модели данных

Создайте папку Api/Data/ в вашем модуле. Это просто соглашение, вы можете использовать другое место, но это не желательно.

Интерфейс репозитория мы будем хранить в папке Api/. Подкаталог Api/Data/, мы будем использовать позже.

В каталоге Api/ создайте интерфейс PHP с методами, которые вы хотите открыть. Согласно соглашениям Magento 2, все имена интерфейсов заканчиваются суффиком Interface.

Например, для объекта Hamburger я бы создал интерфейс Api/HamburgerRepositoryInterface.

Создание интерфейса репозитория

Репозитории Magento 2 являются частью логики модуля. Это означает, что нет определенного набора методов, которые должен реализовать репозиторий.
Все полностью зависит от цели модуля.

Однако на практике все репозитории весьма схожи. Они являются оболочками для функций CRUD.

Большинство из них имеют методы getById, save, delete и getList.
Больше того, например, у CustomerRepository есть метод get, который извлекает клиента по адресу электронной почты, тогда как getById используется для извлечения клиента по идентификатору объекта.

Вот пример интерфейса репозитория для объекта Гамбургер:

<?php
 
namespace VinaiKopp\Kitchen\Api;
 
use Magento\Framework\Api\SearchCriteriaInterface;
use VinaiKopp\Kitchen\Api\Data\HamburgerInterface;
 
interface HamburgerRepositoryInterface
{
    /**
     * @param int $id
     * @return \VinaiKopp\Kitchen\Api\Data\HamburgerInterface
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     */
    public function getById($id);
 
    /**
     * @param \VinaiKopp\Kitchen\Api\Data\HamburgerInterface $hamburger
     * @return \VinaiKopp\Kitchen\Api\Data\HamburgerInterface
     */
    public function save(HamburgerInterface $hamburger);
 
    /**
     * @param \VinaiKopp\Kitchen\Api\Data\HamburgerInterface $hamburger
     * @return void
     */
    public function delete(HamburgerInterface $hamburger);
 
    /**
     * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
     * @return \VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface
     */
    public function getList(SearchCriteriaInterface $searchCriteria);
}

Важно! 

Здесь есть несколько ошибок, которые может быть трудно локализовать:

  • НЕ используйте типы скалярных аргументов PHP7 или возвращаемые типы, если вы хотите подключить их к API REST!
  • Добавьте аннотации PHPDoc для всех аргументов и возвращаемого типа ко всем методам!
  • Используйте полностью квалифицированные имена классов в блоке PHPDoc

Аннотации анализируются Magento Framework, чтобы определить, как конвертировать данные в JSON или XML. Импорт классов (т. е.  оператора use над классом) не применяются!

Каждый метод должен иметь аннотацию со всеми типами аргументов и возвращаемым типом. Даже если метод не принимает аргументов и ничего не возвращает, он должен иметь аннотацию:

/**
 * @return void
 */

Скалярные типы (string, int, float и bool) также должны быть указаны как для аргументов, так и для возвращаемого значения.

Обратите внимание, что в приведенном выше примере аннотации для методов, возвращающих объекты, также указаны как интерфейсе.

Интерфейсы возвращающие тип все находятся в Api\Data пространстве имен/каталоге.

В Magento 2 это означает, что они не содержат никакой бизнес-логики. Они просто «мешки с данными».

Мы создадим эти интерфейсы потом.

Создание интерфейса DTO

Magento называет эти интерфейсы «data models», это название мне совсем не нравится.

Этот тип классов более известен как объект передачи данных или DTO.

Эти классы DTO имеют только методы getter и setter для всех своих свойств.

Причина, по которой я предпочитаю использовать наименование «DTO»  а не «дата-модели», состоит в том, что так сложнее спутать с другими моделями (ORM, ресурс-моделями  или вью-моделями)  … слишком много моделей в Magento.

Те же ограничения типов для PHP7, которые применяются к интерфейсам репозиториев, также применяются к интерфейсам DTO.

Кроме того, каждый метод должен иметь аннотацию со всеми типами аргументов и возвращаемым типом.

<?php
 
namespace VinaiKopp\Kitchen\Api\Data;
 
use Magento\Framework\Api\ExtensibleDataInterface;
 
interface HamburgerInterface extends ExtensibleDataInterface
{
    /**
     * @return int
     */
    public function getId();
 
    /**
     * @param int $id
     * @return void
     */
    public function setId($id);
 
    /**
     * @return string
     */
    public function getName();
 
    /**
     * @param string $name
     * @return void
     */
    public function setName($name);
 
    /**
     * @return \VinaiKopp\Kitchen\Api\Data\IngredientInterface[]
     */
    public function getIngredients();
 
    /**
     * @param \VinaiKopp\Kitchen\Api\Data\IngredientInterface[] $ingredients
     * @return void
     */
    public function setIngredients(array $ingredients);
 
    /**
     * @return string[]
     */
    public function getImageUrls();
 
    /**
     * @param string[] $urls
     * @return void
     */
    public function setImageUrls(array $urls);
 
    /**
     * @return \VinaiKopp\Kitchen\Api\Data\HamburgerExtensionInterface|null
     */
    public function getExtensionAttributes();
 
    /**
     * @param \VinaiKopp\Kitchen\Api\Data\HamburgerExtensionInterface $extensionAttributes
     * @return void
     */
    public function setExtensionAttributes(HamburgerExtensionInterface $extensionAttributes);
}

Если метод извлекает или возвращает массив, тип элементов в массиве должен быть указан в аннотации PHPDoc, а затем квадратная скобка открытая и закрытая [].

Это справедливо как для скалярных значений (например, int []), так и для объектов (например, IngredientInterface []).

Обратите внимание, я использую Api\Data\IngredientInterface в качестве примера для метода, возвращающего массив объектов.

ExtensibleDataInterface?

В приведенном выше примере HamburgerInterface расширяет ExtensibleDataInterface.
Технически это требуется только в том случае, если вы хотите, чтобы другие модули могли добавлять атрибуты в ваши сущности.

Если это так, вам также необходимо добавить пару getter/setter, по соглашению имеющие названия getExtensionAttributes () и setExtensionAttributes ().

Именование возвращаемого типа этого метода очень важно!

Magento 2 фреймворк создаст интерфейс, реализацию и фабрику для реализации, если вы назовете их правильно. Однако детали этой механики выходят за рамки этой статьи.
Просто знайте, если интерфейс объекта, который вы хотите сделать расширяемым, называется \VinaiKopp\Kitchen\Api\Data\HamburgerInterface, тогда тип атрибутов расширения должен быть \VinaiKopp\Kitchen\ Api\Data\HamburgerExtensionInterface. Поэтому слово Extension должно быть вставлено после имени сущности прямо перед суффиксом Interface.

Если вы не хотите, чтобы ваш объект расширялся, тогда интерфейс DTO не должен расширять какой-либо другой интерфейс, и методы getExtensionAttributes () и setExtensionAttributes () могут быть опущены.

Достаточно о интерфейсе DTO, время возвратится к интерфейсу репозитория.

getList () возвращает тип SearchResults

Метод getList метода репозитория возвращает еще один тип, то есть экземпляр SearchResultsInterface.

Метод getList мог бы, конечно, просто вернуть массив объектов, соответствующих указанному SearchCriteria, но возврат экземпляра SearchResults позволяет добавить некоторые полезные метаданные к возвращаемым значениям.

Вы можете увидеть, как это работает ниже в реализации метода getList () репозитория.

Вот пример интерфейса результата поиска гамбургеров:

<?php
 
namespace VinaiKopp\Kitchen\Api\Data;
 
use Magento\Framework\Api\SearchResultsInterface;
 
interface HamburgerSearchResultInterface extends SearchResultsInterface
{
    /**
     * @return \VinaiKopp\Kitchen\Api\Data\HamburgerInterface[]
     */
    public function getItems();
 
    /**
     * @param \VinaiKopp\Kitchen\Api\Data\HamburgerInterface[] $items
     * @return void
     */
    public function setItems(array $items);
}

Весь этот интерфейс делает это, переопределяет типы для двух методов getItems() и setItems() родительского интерфейса.

Резюме по интерфейсам

Теперь у нас есть следующие интерфейсы:

  • \VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface
  • \VinaiKopp\Kitchen\Api\Data\HamburgerInterface
  • \VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface

Репозиторий ничего не расширяет,
HamburgerInterface расширяет \Magento\Framework\Api\ExtensibleDataInterface,
И HamburgerSearchResultInterface расширяет \Magento\Framework\Api\SearchResultsInterface.

 

Создание реализаций репозитория и модели данных

Следующим шагом будет создание реализаций трех интерфейсов.

Репозиторий

По сути, репозиторий использует ORM для выполнения своей работы.

Методы getById (), save () и delete () довольно просты.
HamburgerFactory внедряется в репозиторий в качестве аргумента конструктора, как это видно ниже.

public function getById($id)
{
    $hamburger = $this->hamburgerFactory->create();
    $hamburger->getResource()->load($hamburger, $id);
    if (! $hamburger->getId()) {
        throw new NoSuchEntityException(__('Unable to find hamburger with ID "%1"', $id));
    }
    return $hamburger;
}
 
public function save(HamburgerInterface $hamburger)
{
    $hamburger->getResource()->save($hamburger);
    return $hamburger;
}
 
public function delete(HamburgerInterface $hamburger)
{
    $hamburger->getResource()->delete($hamburger);
}

Теперь самая интересная часть репозитория — метод getList ().
Метод getList () должен преобразовывать условия SerachCriteria в вызовы методов в коллекции.

Сложной частью этого является правильное использование условий AND и OR для фильтров коллекции, особенно потому, что синтаксис для постановки условий в коллекции отличается в зависимости от того, является ли это EAV или плоская табличная сущность.

В большинстве случаев getList () может быть реализован, как показано в примере ниже.

<?php
 
namespace VinaiKopp\Kitchen\Model;
 
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SortOrder;
use Magento\Framework\Exception\NoSuchEntityException;
use VinaiKopp\Kitchen\Api\Data\HamburgerInterface;
use VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface;
use VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterfaceFactory;
use VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface;
use VinaiKopp\Kitchen\Model\ResourceModel\Hamburger\Collection as HamburgerCollectionFactory;
use VinaiKopp\Kitchen\Model\ResourceModel\Hamburger\Collection;
 
class HamburgerRepository implements HamburgerRepositoryInterface
{
    /**
     * @var Hamburger
     */
    private $hamburgerFactory;
 
    /**
     * @var HamburgerCollectionFactory
     */
    private $hamburgerCollectionFactory;
 
    /**
     * @var HamburgerSearchResultInterfaceFactory
     */
    private $searchResultFactory;
 
    public function __construct(
        Hamburger $hamburgerFactory,
        HamburgerCollectionFactory $hamburgerCollectionFactory,
        HamburgerSearchResultInterfaceFactory $hamburgerSearchResultInterfaceFactory
    ) {
        $this->hamburgerFactory = $hamburgerFactory;
        $this->hamburgerCollectionFactory = $hamburgerCollectionFactory;
        $this->searchResultFactory = $hamburgerSearchResultInterfaceFactory;
    }
 
    // ... getById, save and delete methods listed above ...
 
    public function getList(SearchCriteriaInterface $searchCriteria)
    {
        $collection = $this->collectionFactory->create();
 
        $this->addFiltersToCollection($searchCriteria, $collection);
        $this->addSortOrdersToCollection($searchCriteria, $collection);
        $this->addPagingToCollection($searchCriteria, $collection);
 
        $collection->load();
 
        return $this->buildSearchResult($searchCriteria, $collection);
    }
 
    private function addFiltersToCollection(SearchCriteriaInterface $searchCriteria, Collection $collection)
    {
        foreach ($searchCriteria->getFilterGroups() as $filterGroup) {
            $fields = $conditions = [];
            foreach ($filterGroup->getFilters() as $filter) {
                $fields[] = $filter->getField();
                $conditions[] = [$filter->getConditionType() => $filter->getValue()];
            }
            $collection->addFieldToFilter($fields, $conditions);
        }
    }
 
    private function addSortOrdersToCollection(SearchCriteriaInterface $searchCriteria, Collection $collection)
    {
        foreach ((array) $searchCriteria->getSortOrders() as $sortOrder) {
            $direction = $sortOrder->getDirection() == SortOrder::SORT_ASC ? 'asc' : 'desc';
            $collection->addOrder($sortOrder->getField(), $direction);
        }
    }
 
    private function addPagingToCollection(SearchCriteriaInterface $searchCriteria, Collection $collection)
    {
        $collection->setPageSize($searchCriteria->getPageSize());
        $collection->setCurPage($searchCriteria->getCurrentPage());
    }
 
    private function buildSearchResult(SearchCriteriaInterface $searchCriteria, Collection $collection)
    {
        $searchResults = $this->searchResultFactory->create();
 
        $searchResults->setSearchCriteria($searchCriteria);
        $searchResults->setItems($collection->getItems());
        $searchResults->setTotalCount($collection->getSize());
 
        return $searchResults;
    }
}

Фильтры в пределах FilterGroup должны быть объединены с помощью оператора OR.
Отдельные группы фильтров объединяются с помощью логического оператора.

Это была самая большая работа. Другие реализации интерфейса проще.

DTO

Первоначально Magento предлагала разработчикам реализовывать DTO в виде отдельных классов, отличных от модели сущности.

Разработчики ядра реализовали это только для модуля клиента (\Magento\Customer\Api\Data\CustomerInterface реализована \Magento\Customer\Model\Data\ Customer, а не \Magento\Customer\Model\Customer).
Во всех остальных случаях модель объекта реализует интерфейс DTO (например, \Magento\Catalog\Api\Data\ProductInterface реализуется \Magento\Catalog\Model\Product).

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

На данный момент я принял прагматичное решение использовать модель в качестве реализации интерфейса DTO. Если вы считаете, что использовать отдельную модель данных более грамотное решение, вы в праве так делать. Оба подхода работают на практике.

Если DTO inteface расширяет Magento\Framework\Api\ExtensibleDataInterface, модель должен расширять Magento\Framework\Model\AbstractExtensibleModel.
Если вам не нужна расширяемость, модель может просто продолжать расширение базового класса модели ORM Magento\Framework\Model\AbstractModel.

Так как пример HamburgerInterface расширяет ExtensibleDataInterface, модель гамбургера расширяет AbstractExtensibleModel, например:

<?php
 
namespace VinaiKopp\Kitchen\Model;
 
use Magento\Framework\Model\AbstractExtensibleModel;
use VinaiKopp\Kitchen\Api\Data\HamburgerExtensionInterface;
use VinaiKopp\Kitchen\Api\Data\HamburgerInterface;
 
class Hamburger extends AbstractExtensibleModel implements HamburgerInterface
{
    const NAME = 'name';
    const INGREDIENTS = 'ingredients';
    const IMAGE_URLS = 'image_urls';
 
    protected function _construct()
    {
        $this->_init(ResourceModel\Hamburger::class);
    }
 
    public function getName()
    {
        return $this->_getData(self::NAME);
    }
 
    public function setName($name)
    {
        $this->setData(self::NAME);
    }
 
    public function getIngredients()
    {
        return $this->_getData(self::INGREDIENTS);
    }
 
    public function setIngredients(array $ingredients)
    {
        $this->setData(self::INGREDIENTS, $ingredients);
    }
 
    public function getImageUrls()
    {
        $this->_getData(self::IMAGE_URLS);
    }
 
    public function setImageUrls(array $urls)
    {
        $this->setData(self::IMAGE_URLS, $urls);
    }
 
    public function getExtensionAttributes()
    {
        return $this->_getExtensionAttributes();
    }
 
    public function setExtensionAttributes(HamburgerExtensionInterface $extensionAttributes)
    {
        $this->_setExtensionAttributes($extensionAttributes);
    }
}

Извлечение имен свойств в константы позволяет сохранить их в одном месте. Они могут использоваться парой getter/setter, а также скриптом Setup, который создает таблицу базы данных. В противном случае нет никакой выгоды в том, чтобы извлекать их в константы.

SearchResult

SearchResultsInterface — это самый простой из трех реализованных интерфейсов, поскольку он наследует все функциональные возможности из класса framework.

<?php
 
namespace VinaiKopp\Kitchen\Model;
 
use Magento\Framework\Api\SearchResults;
use VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface;
 
class HamburgerSearchResult extends SearchResults implements HamburgerSearchResultInterface
{
 
}

Настраивает параметры ObjectManager

Несмотря на то, что имплементации завершены, мы по-прежнему не можем использовать интерфейсы в качестве зависимостей других классов, поскольку диспетчер объектов Magento Framework не знает, какие имплементации использовать. Нам нужно добавить конфигурацию etc/di.xml с настройками.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" type="VinaiKopp\Kitchen\Model\HamburgerRepository"/>
    <preference for="VinaiKopp\Kitchen\Api\Data\HamburgerInterface" type="VinaiKopp\Kitchen\Model\Hamburger"/>
    <preference for="VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface" type="VinaiKopp\Kitchen\Model\HamburgerSearchResult"/>
</config>

Как можно разместить репозиторий как ресурс API?

Эта часть действительно проста, это вознаграждение за проделанную работу, по созданию интерфейсов, имплементацию и проводку их вместе.

Все, что нам нужно сделать, это создать файл etc/webapi.xml.

<?xml version="1.0"?>
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
    <route method="GET" url="/V1/vinaikopp_hamburgers/:id">
        <service class="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" method="getById"/>
        <resources>
            <resource ref="anonymous"/>
        </resources>
    </route>
    <route method="GET" url="/V1/vinaikopp_hamburgers">
        <service class="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" method="getList"/>
        <resources>
            <resource ref="anonymouns"/>
        </resources>
    </route>
    <route method="POST" url="/V1/vinaikopp_hamburgers">
        <service class="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" method="save"/>
        <resources>
            <resource ref="anonymous"/>
        </resources>
    </route>
    <route method="PUT" url="/V1/vinaikopp_hamburgers">
        <service class="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" method="save"/>
        <resources>
            <resource ref="anonymous"/>
        </resources>
    </route>
    <route method="DELETE" url="/V1/vinaikopp_hamburgers">
        <service class="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" method="delete"/>
        <resources>
            <resource ref="anonymous"/>
        </resources>
    </route>
</routes>

Обратите внимание, что эта конфигурация не только позволяет использовать репозиторий как конечные точки REST, но также предоставляет методы как часть SOAP API.

В первом примере маршрута <route method = «GET» url = «/V1/vinaikopp_hamburgers/:id»>, placeholder: id должен соответствовать имени аргумента сопоставленному методу, public function getById($id) ,
Два имени должны совпадать, например /V1/vinaikopp_hamburgers/:hamburgerId не будет работать, поскольку имя переменной аргумента метода равно $id.

В этом примере я установил доступность <resource ref = «anonymous» />. Это означает, что ресурс публикуется публично без каких-либо ограничений!
Чтобы сделать ресурс доступным только зарегистрированному клиенту, используйте <resource ref = «self» />. В этом случае специальное слово me в URL конечной точки ресурса будет использоваться для заполнения переменной аргумента $id идентификатором текущего зарегистрированного клиента.
Если вам это нужно, посмотрите на Magento Customer etc/webapi.xml и CustomerRepositoryInterface.

Наконец, <resources> также может использоваться для ограничения доступа к ресурсу учетной записи администратора. Для этого установите <resource> ref в идентификатор, определенный в файле etc/acl.xml.
Например, <resource ref = «Magento_Customer::manage» /> ограничит доступ к любой учетной записи администратора, которая имеет привилегию управлять клиентами.

Обратите внимание, что методы POST и PUT HTTP сопоставляются с одним и тем же методом интерфейса хранилища save ().
REST обрабатывает создание (вставки a.k.a.) и обновления как отдельные действия, в Magento оба обрабатываются одним и тем же методом save ().

Источник: http://vinaikopp.com/2017/02/18/magento2_repositories_interfaces_and_webapi/

Репозитории Magento 2, интерфейсы и веб-API: 1 комментарий

  1. tsum

    Привет. Отличная статья! По поводу использоваия DTO — «The recommended way to work with Magento 2 is to have dedicated data objects that implement data API interfaces. Over time, the
    core team plan to refactor all modules to use this approach.» Это рекомендация написана в материалах к сертифицированному курсу.

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

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

5 × один =