除了@ryanF的出色回答,我想提供更多细节。
我想总结一下为自定义实体添加存储库的原因,提供示例说明,并说明如何将这些存储库方法作为Web API的一部分公开。
免责声明:我仅描述一种实用的方法,如何对第三方模块执行此操作-核心团队具有(或不遵循)自己的标准。
通常,存储库的目的是隐藏与存储相关的逻辑。
存储库的客户端不应该关心返回的实体是否保存在数组的内存中,是否从MySQL数据库检索,是从远程API还是从文件中获取。
我认为Magento核心团队是这样做的,因此他们将来可以更改或替换ORM。在Magento中,ORM当前由模型,资源模型和集合组成。
如果第三方模块仅使用存储库,Magento可以更改数据的存储方式和存储位置,尽管进行了这些深层更改,该模块仍将继续工作。
库一般都有类似的方法findById(),findByName(),put()或remove()。
在Magento的这些常用称为getbyId(),save()和delete(),甚至没有假装他们正在做别的什么,但CRUD操作数据库。
Magento 2存储库方法可以轻松地作为API资源公开,使其对于与第三方系统或无头Magento实例集成非常有价值。
“我应该为自定义实体添加存储库吗?”。
与往常一样,答案是
“这取决于”。
简而言之,如果您的实体将被其他模块使用,那么可以,您可能想要添加一个存储库。
这里还有一个重要的因素:在Magento 2中,存储库可以轻松地作为Web API(即REST和SOAP)资源公开。
如果由于第三方系统集成或无用的Magento设置而使您感兴趣,那么再次,是的,您可能想为实体添加存储库。
如何为自定义实体添加存储库?
假设您想将您的实体公开为REST API的一部分。如果不是这样,则可以跳过接下来创建接口的部分,直接转到下面的“创建存储库和数据模型实现”。
创建存储库和数据模型接口
Api/Data/在模块中创建文件夹。这只是约定,您可以使用其他位置,但不可以。
存储库进入Api/文件夹。该Data/子目录供以后使用。
在中Api/,使用您要公开的方法创建一个PHP接口。根据Magento 2约定,所有接口名称均以后缀结尾Interface。
例如,对于一个Hamburger实体,我将创建interface Api/HamburgerRepositoryInterface。
创建存储库界面
Magento 2存储库是模块域逻辑的一部分。这意味着,存储库没有必须实现的固定方法集。
这完全取决于模块的目的。
但是,实际上所有存储库都非常相似。它们是CRUD功能的包装。
最有方法getById,save,delete和getList。
可能会有更多,例如CustomerRepository具有方法get,该方法通过电子邮件获取客户,从而getById用于通过实体ID检索客户。
这是汉堡实体的示例存储库接口:
<?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);
}
重要!这里是时间的迷!
如果您弄错了这里的一些陷阱,则很难调试:
- 如果要将其挂接到REST API中,请勿使用PHP7标量参数类型或返回类型!
- 为所有参数添加PHPDoc注释,并为所有方法添加返回类型!
- 在PHPDoc块中使用完全合格的类名!
Magento框架会解析注释,以确定如何将数据与JSON或XML相互转换。类导入(即use语句)不适用!
每个方法都必须具有带任何参数类型和返回类型的注释。即使一个方法不带参数也不返回任何内容,它也必须具有注释:
/**
* @return void
*/
标量类型(string,int,float和bool)也必须指定,无论是参数和返回值。
请注意,在上面的示例中,用于返回对象的方法的注释也被指定为接口。
返回类型的接口都在Api\Data名称空间/目录中。
这表明它们不包含任何业务逻辑。它们只是数据包。
接下来,我们必须创建这些接口。
创建DTO界面
我认为Magento将这些接口称为“数据模型”,这个名字我一点都不喜欢。
这种类型的类通常称为数据传输对象或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以一个示例为例,该方法返回一个对象数组,我不会在这篇文章中添加成分代码。
可扩展数据接口?
在上述示例中,HamburgerInterface扩展了ExtensibleDataInterface。
从技术上讲,仅当您希望其他模块能够向实体添加属性时才需要这样做。
如果是这样,您还需要按照惯例称为getExtensionAttributes()和来添加另一个getter / setter对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必须使用OR运算符组合。
使用逻辑AND运算符组合单独的过滤器组。
ew
这是最大的工作。其他接口实现更为简单。
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对以及创建数据库表的Setup脚本都可以使用它们。否则,将它们提取为常量没有任何好处。
搜索结果
这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当前登录客户的ID 填充参数变量。
看一下Magento客户etc/webapi.xml,CustomerRepositoryInterface如果需要的话。
最后,<resources>还可以使用来将对资源的访问限制为管理员用户帐户。为此,将<resource>ref 设置为etc/acl.xml文件中定义的标识符。
例如,<resource ref="Magento_Customer::manage"/>将访问权限限制为有权管理客户的任何管理员帐户。
使用curl的示例API查询可能如下所示:
$ curl -X GET http://example.com/rest/V1/vinaikopp_hamburgers/123
注:写这篇开始作为一个答案https://github.com/astorm/pestle/issues/195
退房杵,买Commercebug并成为patreon @alanstorm的