除了@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的