@ryanFの優れた答えに加えて、もう少し詳しく説明したいと思います。
カスタムエンティティ用のリポジトリを追加する理由を要約し、その方法の例を示し、Web APIの一部としてこれらのリポジトリメソッドを公開する方法についても説明します。
免責事項:サードパーティのモジュールに対してこれを行う実際的なアプローチのみを説明しています-コアチームは独自の標準に従っています(または準拠していません)。
一般に、リポジトリの目的は、ストレージ関連のロジックを隠すことです。
リポジトリのクライアントは、返されたエンティティが配列内のメモリに保持されているか、MySQLデータベースから取得されているか、リモートAPIまたはファイルから取得されているかを気にするべきではありません。
Magentoのコアチームがこれを行ったため、将来ORMを変更または交換できると思います。Magentoでは、ORMは現在、モデル、リソースモデル、およびコレクションで構成されています。
サードパーティのモジュールがリポジトリのみを使用する場合、Magentoはデータの保存方法と保存場所を変更でき、これらの大きな変更にもかかわらず、モジュールは引き続き機能します。
リポジトリは、一般的のようなメソッドを持ってfindById()
、findByName()
、put()
またはremove()
。
Magentoのでは、これらは一般的に呼ばれているgetbyId()
、save()
とdelete()
も、彼らは何かが、CRUD DB操作を行っているふりをしていません、。
Magento 2リポジトリメソッドは、APIリソースとして簡単に公開できるため、サードパーティシステムまたはヘッドレスMagentoインスタンスとの統合に役立ちます。
「カスタムエンティティのリポジトリを追加する必要がありますか?」
いつものように、答えは
"場合によります"。
簡単に言うと、エンティティが他のモジュールで使用される場合、はい、おそらくリポジトリを追加する必要があります。
ここで考慮されるもう1つの要素があります。Magento2では、リポジトリを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
、エンティティ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);
}
重要!ここにタイムシンクがあります!
ここには、間違えた場合にデバッグするのが難しいいくつかの落とし穴があります。
- しないでください使用PHP7あなたはRESTのAPIにこれをフックしたい場合は、スカラー引数の型または戻り値の型を!
- すべてのメソッドに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()
andと呼ばれる規則により、別のゲッター/セッターのペアも追加する必要があり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);
}
このインターフェイスは、2つのメソッド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
リポジトリとデータモデルの実装を作成する
次のステップは、3つのインターフェースの実装を作成することです。
リポジトリ
本質的に、リポジトリは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
特に、コレクションの条件を設定する構文は、EAVかフラットテーブルエンティティかによって異なるため、フィルターの条件AND
とOR
条件を正しく取得することはトリッキーです。
ほとんどの場合、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演算子を使用して、個別のフィルターグループを結合します。
ふぅ
これは作品の最大のビットでした。他のインターフェイスの実装は単純です。
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);
}
}
プロパティ名を定数に抽出すると、それらを1つの場所に保持できます。これらは、ゲッター/セッターのペア、およびデータベーステーブルを作成するセットアップスクリプトで使用できます。そうでなければ、それらを定数に抽出しても利点はありません。
SearchResult
SearchResultsInterface
それは、フレームワークのクラスからのすべての機能を継承することができるので、実装するの3つのインタフェースの最も単純です。
<?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)
。メソッド引数の変数名はであるため、
2つの名前は一致する必要/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の