Я хотів би розповісти трохи більше деталей на додаток до чудової відповіді @ryanF.
Я хотів би підсумувати причини додавання сховища для користувацьких сутностей, навести приклади, як це зробити, а також пояснити, як відкрити ці методи репозиторію як частину веб-API.
Відмова: Я лише описую прагматичний підхід, як це зробити для сторонніх модулів - основні команди мають свої стандарти, яких вони дотримуються (чи ні).
Загалом, мета сховища - приховати логіку, пов'язану із зберіганням.
Клієнт сховища не повинен дбати про те, чи зберігається об'єкт зберігається в пам'яті в масиві, отримується з бази даних MySQL, отриманої з віддаленого API або з файлу.
Я припускаю, що основна команда Magento зробила це, щоб вони могли змінити або замінити ORM у майбутньому. Зараз у Магенто ORM складається з Моделей, Моделей ресурсів та Колекцій.
Якщо сторонній модуль використовує лише сховища, Magento може змінити, як і де зберігаються дані, і модуль продовжить працювати, незважаючи на ці глибокі зміни.
Сховища , як правило, такі методи , як findById()
, findByName()
, put()
або remove()
.
В Magento вони зазвичай називаються getbyId()
, save()
і delete()
, навіть не роблячи вигляд , що вони роблять що -то ще , але операції CRUD DB.
Методи сховища Magento 2 можуть бути легко викриті як ресурси API, що робить їх цінними для інтеграції з сторонніми системами або безголовими екземплярами Magento.
Msgstr "Чи слід додати сховище для моєї спеціальної сутності?"
Як завжди, відповідь така
"Це залежить".
Якщо коротко розповісти, якщо ваші сутності будуть використовуватися іншими модулями, то так, ви, ймовірно, хочете додати сховище.
Тут є ще один фактор, який враховується: у Magento 2 сховища можуть бути легко відкриті як Web 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
, метод методу 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, щоб визначити, як перетворити дані в 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
.
Технічно це потрібно лише в тому випадку, якщо ви хочете, щоб інші модулі могли додати атрибути до вашої сутності.
Якщо це так, вам також потрібно додати ще одну геттер / пара задач, за умовою, що називається 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);
}
}
Витяг імен властивостей у константи дозволяє тримати їх в одному місці. Вони можуть використовуватися парою getter / setter, а також сценарієм настройки, який створює таблицю бази даних. В іншому випадку вилучення їх у константи не має ніякої користі.
Результат пошуку
Це 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>
ref на ідентифікатор, визначений у 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/isissue/195
Перевірте пестик , купіть Commercebug і станьте патроном @alanstorm