Я хотел бы дать немного больше деталей в дополнение к отличному ответу @ryanF.
Я хотел бы суммировать причины добавления репозитория для пользовательских объектов, привести примеры, как это сделать, а также объяснить, как представить эти методы репозитория как часть веб-API.
Отказ от ответственности: я только описываю прагматичный подход, как сделать это для сторонних модулей - у основных команд есть свои собственные стандарты, которым они следуют (или нет).
В общем, цель репозитория - скрыть логику, связанную с хранилищем.
Клиент хранилища не должен заботиться о том, хранится ли возвращенная сущность в памяти в массиве, извлекается ли она из базы данных MySQL, извлекается ли из удаленного API или из файла.
Я предполагаю, что основная команда Magento сделала это, чтобы они могли изменить или заменить ORM в будущем. В Magento ORM в настоящее время состоит из моделей, моделей ресурсов и коллекций.
Если сторонний модуль использует только репозитории, Magento может изменить способ и место хранения данных, и модуль продолжит работать, несмотря на эти глубокие изменения.
Хранилища , как правило, такие методы , как findById()
, findByName()
, put()
или remove()
.
В Magento их обычно называют getbyId()
, save()
иdelete()
, даже не притворяясь, они делают что-то еще, кроме операций CRUD DB.
Методы хранилища Magento 2 можно легко представить в виде ресурсов API, что делает их полезными для интеграции со сторонними системами или безголовыми экземплярами Magento.
Msgstr "Должен ли я добавить репозиторий для моей пользовательской сущности?"
Как всегда, ответ
"Это зависит".
Короче говоря, если ваши сущности будут использоваться другими модулями, то да, вы, вероятно, захотите добавить репозиторий.
Здесь есть еще один фактор: в Magento 2 репозитории могут быть легко представлены как веб-API, то есть ресурсы REST и SOAP.
Если вам это интересно из-за интеграции системы сторонних производителей или установки Magento без головы, то, опять же, да, вы, вероятно, захотите добавить репозиторий для вашей сущности.
Как добавить репозиторий для моей пользовательской сущности?
Предположим, вы хотите представить свою сущность как часть REST API. Если это не так, вы можете пропустить следующую часть при создании интерфейсов и перейти к разделу «Создание реализации репозитория и модели данных» ниже.
Создать хранилище и интерфейсы модели данных
Создайте папки Api/Data/
в вашем модуле. Это просто соглашение, вы можете использовать другое место, но вы не должны.
Репозиторий идет в Api/
папку. Data/
Подкаталог для позже.
В Api/
, создайте интерфейс PHP с методами, которые вы хотите представить. Согласно соглашениям Magento 2 все имена интерфейсов заканчиваются суффиксом Interface
.
Например, дляHamburger
сущности я бы создал интерфейс Api/HamburgerRepositoryInterface
.
Создайте интерфейс хранилища
Репозитории Magento 2 являются частью доменной логики модуля. Это означает, что не существует фиксированного набора методов, которые должен реализовывать репозиторий.
Это полностью зависит от цели модуля.
Однако на практике все репозитории довольно похожи. Они являются обертками для функциональности CRUD.
Большинство из них имеют методы getById
, save
, delete
и getList
.
Может быть больше, например метод CustomerRepository
has 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 или возвращаемые типы, если вы хотите подключить это к REST API!
- Добавьте аннотации PHPDoc для всех аргументов и тип возврата для всех методов!
- Используйте полные имена классов в блоке PHPDoc!
Аннотации анализируются в Magento Framework, чтобы определить, как преобразовать данные в JSON или XML. Импорт классов (то есть use
операторов) не применяется!
Каждый метод должен иметь аннотацию с любыми типами аргументов и типом возвращаемого значения. Даже если метод не принимает аргументов и ничего не возвращает, он должен иметь аннотацию:
/**
* @return void
*/
Скалярные типы ( string
, int
, float
и bool
) также должны быть определены, как аргументы и в качестве возвращаемого значения.
Обратите внимание, что в приведенном выше примере аннотации для методов, которые возвращают объекты, также указываются как интерфейсы.
Все интерфейсы возвращаемого типа находятся в Api\Data
пространстве имен / каталоге.
Это означает, что они не содержат никакой бизнес-логики. Они просто мешки с данными.
Мы должны создать эти интерфейсы дальше.
Создать интерфейс DTO
Я думаю, что Magento называет эти интерфейсы «моделями данных», это имя мне совсем не нравится.
Этот тип класса обычно известен как объект передачи данных или DTO .
Эти классы DTO имеют только методы получения и установки для всех своих свойств.
Причина, по которой я предпочитаю использовать 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\CollectionFactory as HamburgerCollectionFactory;
use VinaiKopp\Kitchen\Model\ResourceModel\Hamburger\Collection;
class HamburgerRepository implements HamburgerRepositoryInterface
{
/**
* @var HamburgerFactory
*/
private $hamburgerFactory;
/**
* @var HamburgerCollectionFactory
*/
private $hamburgerCollectionFactory;
/**
* @var HamburgerSearchResultInterfaceFactory
*/
private $searchResultFactory;
public function __construct(
HamburgerFactory $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
должны быть объединены с помощью оператора ИЛИ .
Отдельные группы фильтров объединяются с помощью логического оператора AND .
Фу
Это была самая большая часть работы. Другие реализации интерфейса проще.
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 расширяет 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, $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);
}
}
Извлечение имен свойств в константы позволяет хранить их в одном месте. Они могут использоваться парой получателя / установщика, а также сценарием установки, который создает таблицу базы данных. В противном случае нет смысла извлекать их в константы.
SearchResult
Это SearchResultsInterface
самый простой из трех реализуемых интерфейсов, поскольку он может наследовать всю свою функциональность от класса фреймворка.
<?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">
заполнитель :id
должен совпадать с именем аргумента преобразованного метода public function getById($id)
.
Два имени должны совпадать, например /V1/vinaikopp_hamburgers/:hamburgerId
, не будет работать, так как имя переменной аргумента метода равно $id
.
Для этого примера я установил доступность <resource ref="anonymous"/>
. Это означает, что ресурс открыт публично без каких-либо ограничений!
Чтобы сделать ресурс доступным только зарегистрированному клиенту, используйте <resource ref="self"/>
. В этом случае специальное слово me
в URL-адресе конечной точки ресурса будет использоваться для заполнения переменной аргумента $id
идентификатором текущего пользователя, вошедшего в систему.
Посмотрите на клиента Magento etc/webapi.xml
и, CustomerRepositoryInterface
если вам это нужно.
Наконец, <resources>
можно также использовать для ограничения доступа к ресурсу учетной записи администратора. Для этого установите в качестве <resource>
ссылки идентификатор, определенный в etc/acl.xml
файле.
Например, <resource ref="Magento_Customer::manage"/>
будет ограничивать доступ к любой учетной записи администратора, который имеет право управлять клиентами.
Пример запроса API с использованием curl может выглядеть следующим образом:
$ curl -X GET http://example.com/rest/V1/vinaikopp_hamburgers/123
Примечание: написание этого началось как ответ на https://github.com/astorm/pestle/issues/195
Проверьте пестик , купите Commercebug и станьте покровителем @alanstorm