Gostaria de dar um pouco mais de detalhes, além da excelente resposta do @ryanF.
Gostaria de resumir os motivos para adicionar um repositório para entidades personalizadas, dar exemplos de como fazer isso e também explicar como expor esses métodos de repositório como parte da API da Web.
Isenção de responsabilidade: estou descrevendo apenas uma abordagem pragmática de como fazer isso para módulos de terceiros - as equipes principais têm seus próprios padrões que seguem (ou não).
Em geral, o objetivo de um repositório é ocultar a lógica relacionada ao armazenamento.
Um cliente de um repositório não deve se importar se a entidade retornada é mantida na memória em uma matriz, é recuperada de um banco de dados MySQL, obtida de uma API remota ou de um arquivo.
Suponho que a equipe principal do Magento fez isso para que eles possam alterar ou substituir o ORM no futuro. No Magento, o ORM atualmente consiste nos modelos, modelos de recursos e coleções.
Se um módulo de terceiros usar apenas os repositórios, o Magento poderá alterar como e onde os dados são armazenados, e o módulo continuará funcionando, apesar dessas mudanças profundas.
Repositórios têm geralmente métodos como findById()
, findByName()
, put()
ou remove()
.
Em Magento estes comumente são chamados getbyId()
, save()
e delete()
, nem mesmo fingir que estão fazendo qualquer outra coisa, mas as operações CRUD DB.
Os métodos de repositório Magento 2 podem ser facilmente expostos como recursos de API, tornando-os valiosos para integrações com sistemas de terceiros ou instâncias Magento sem cabeça.
"Devo adicionar um repositório para minha entidade personalizada?"
Como sempre, a resposta é
"Depende".
Para resumir uma longa história, se suas entidades serão usadas por outros módulos, sim, você provavelmente deseja adicionar um repositório.
Há outro fator que conta aqui: no Magento 2, os repositórios podem ser facilmente expostos como recursos da API da Web - que são REST e SOAP.
Se isso for interessante para você por causa de integrações de sistemas de terceiros ou uma configuração sem cabeçalho do Magento, novamente, sim, você provavelmente deseja adicionar um repositório para sua entidade.
Como adiciono um repositório à minha entidade personalizada?
Vamos supor que você deseja expor sua entidade como parte da API REST. Se isso não for verdade, você pode pular a próxima parte da criação das interfaces e ir direto para "Criar a implementação do repositório e do modelo de dados" abaixo.
Crie as interfaces de repositório e modelo de dados
Crie as pastas Api/Data/
no seu módulo. Isso é apenas convenção, você pode usar um local diferente, mas não deve.
O repositório entra na Api/
pasta. O Data/
subdiretório é para mais tarde.
Em Api/
, crie uma interface PHP com os métodos que você deseja expor. De acordo com as convenções do Magento 2, todos os nomes de interface terminam no sufixo Interface
.
Por exemplo, para uma Hamburger
entidade, eu criaria a interface Api/HamburgerRepositoryInterface
.
Crie a interface do repositório
Os repositórios Magento 2 fazem parte da lógica do domínio de um módulo. Isso significa que não há um conjunto fixo de métodos que um repositório precisa implementar.
Depende inteiramente da finalidade do módulo.
No entanto, na prática, todos os repositórios são bastante semelhantes. Eles são invólucros para a funcionalidade CRUD.
A maioria tem os métodos getById
, save
, delete
e getList
.
Pode haver mais, por exemplo, CustomerRepository
tem um método get
, que busca um cliente por email, pelo qual getById
é usado para recuperar um cliente por ID da entidade.
Aqui está um exemplo de interface de repositório para uma entidade de hambúrguer:
<?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);
}
Importante! Aqui estão timesinks!
Existem algumas dicas aqui que são difíceis de depurar se você errar:
- NÃO use tipos de argumentos escalares do PHP7 ou tipos de retorno, se desejar conectar isso à API REST!
- Adicione anotações PHPDoc para todos os argumentos e o tipo de retorno para todos os métodos!
- Use nomes de classe totalmente qualificados no bloco PHPDoc!
As anotações são analisadas pelo Magento Framework para determinar como converter dados de e para JSON ou XML. As importações de classe (ou seja, use
instruções) não são aplicadas!
Todo método precisa ter uma anotação com qualquer tipo de argumento e o tipo de retorno. Mesmo que um método não use argumentos e não retorne nada, ele precisa ter a anotação:
/**
* @return void
*/
Tipos escalares ( string
, int
, float
e bool
) também tem que ser especificado, tanto para os argumentos e como um valor de retorno.
Observe que no exemplo acima, as anotações para métodos que retornam objetos também são especificadas como interfaces.
As interfaces do tipo de retorno estão todas no Api\Data
namespace / diretório.
Isso é para indicar que eles não contêm nenhuma lógica comercial. Eles são simplesmente sacos de dados.
Temos que criar essas interfaces a seguir.
Crie a interface DTO
Eu acho que o Magento chama essas interfaces de "modelos de dados", um nome que eu não gosto.
Esse tipo de classe é comumente conhecido como um objeto de transferência de dados ou DTO .
Essas classes DTO têm apenas getters e setters para todas as suas propriedades.
A razão pela qual prefiro usar o DTO sobre o modelo de dados é que é menos fácil confundir com os modelos de dados ORM, modelos de recursos ou modelos de exibição ... muitas coisas já são modelos no Magento.
As mesmas restrições em relação à digitação do PHP7 que se aplicam aos repositórios também se aplicam aos DTOs.
Além disso, todo método deve ter uma anotação com todos os tipos de argumentos e o tipo de retorno.
<?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);
}
Se um método recuperar ou retornar uma matriz, o tipo dos itens na matriz deverá ser especificado na anotação PHPDoc, seguido por um colchete de abertura e fechamento []
.
Isso é verdade tanto para valores escalares (por exemplo int[]
) quanto para objetos (por exemplo IngredientInterface[]
).
Observe que estou usando um Api\Data\IngredientInterface
como exemplo para um método retornando uma matriz de objetos, não adicionarei o código dos ingredientes a este post difícil.
ExtensibleDataInterface?
No exemplo acima, o HamburgerInterface
estende o ExtensibleDataInterface
.
Tecnicamente, isso é necessário apenas se você desejar que outros módulos possam adicionar atributos à sua entidade.
Nesse caso, você também precisará adicionar outro par getter / setter, por convenção chamada getExtensionAttributes()
e setExtensionAttributes()
.
A nomeação do tipo de retorno desse método é muito importante!
A estrutura do Magento 2 irá gerar a interface, a implementação e a fábrica para a implementação, se você der o nome correto. Os detalhes dessas mecânicas estão fora do escopo deste post.
Apenas saiba que, se a interface do objeto que você deseja tornar extensível for chamada \VinaiKopp\Kitchen\Api\Data\HamburgerInterface
, o tipo de atributo de extensão deverá ser \VinaiKopp\Kitchen\Api\Data\HamburgerExtensionInterface
. Portanto, a palavra Extension
deve ser inserida após o nome da entidade, logo antes do Interface
sufixo.
Se você não deseja que sua entidade seja extensível, a interface do DTO não precisa estender nenhuma outra interface e os métodos getExtensionAttributes()
e setExtensionAttributes()
podem ser omitidos.
Chega de informações sobre a interface do DTO, por enquanto, hora de retornar à interface do repositório.
O tipo de retorno getList () SearchResults
O método do repositório getList
retorna ainda outro tipo, ou seja, uma SearchResultsInterface
instância.
getList
Obviamente, o método poderia retornar apenas uma matriz de objetos que correspondesse ao especificado SearchCriteria
, mas o retorno de uma SearchResults
instância permite adicionar alguns metadados úteis aos valores retornados.
Você pode ver como isso funciona abaixo na getList()
implementação do método do repositório .
Aqui está o exemplo de interface de resultado de pesquisa de hambúrguer:
<?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);
}
Tudo o que essa interface faz é substituir os tipos dos dois métodos getItems()
e setItems()
da interface pai.
Resumo das interfaces
Agora temos as seguintes interfaces:
\VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface
\VinaiKopp\Kitchen\Api\Data\HamburgerInterface
\VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface
O repositório estende nada,
o HamburgerInterface
estende a \Magento\Framework\Api\ExtensibleDataInterface
,
ea HamburgerSearchResultInterface
estende a \Magento\Framework\Api\SearchResultsInterface
.
Crie as implementações de repositório e modelo de dados
O próximo passo é criar as implementações das três interfaces.
O Repositório
Em essência, o repositório usa o ORM para fazer seu trabalho.
Os métodos getById()
, save()
e delete()
são bastante diretos.
O HamburgerFactory
é injetado no repositório como um argumento construtor, como pode ser visto um pouco mais abaixo.
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);
}
Agora, a parte mais interessante de um repositório, o getList()
método
O getList()
método precisa converter as SerachCriteria
condições em chamadas de método na coleção.
A parte complicada disso é acertar as condições AND
e OR
para os filtros, especialmente porque a sintaxe para definir as condições na coleção é diferente dependendo se é um EAV ou uma entidade de tabela plana.
Na maioria dos casos, getList()
pode ser implementado como ilustrado no exemplo abaixo.
<?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;
}
}
Os filtros em um FilterGroup
devem ser combinados usando um operador OU .
Grupos de filtros separados são combinados usando o operador AND lógico .
Ufa
Este foi o maior trabalho. As outras implementações de interface são mais simples.
O DTO
O Magento originalmente pretendia que os desenvolvedores implementassem o DTO como classes separadas, distintas do modelo de entidade.
A equipe principal só fez isso para o módulo do cliente ( \Magento\Customer\Api\Data\CustomerInterface
é implementado por \Magento\Customer\Model\Data\Customer
, não \Magento\Customer\Model\Customer
).
Em todos os outros casos, o modelo de entidade implementa a interface DTO (por exemplo, \Magento\Catalog\Api\Data\ProductInterface
é implementada por \Magento\Catalog\Model\Product
).
Perguntei aos membros da equipe principal sobre isso em conferências, mas não recebi uma resposta clara sobre o que deve ser considerado uma boa prática.
Minha impressão é que essa recomendação foi abandonada. Seria bom obter uma declaração oficial sobre isso.
Por enquanto, tomei a decisão pragmática de usar o modelo como a implementação da interface DTO. Se você achar que é mais limpo usar um modelo de dados separado, sinta-se à vontade para fazê-lo. Ambas as abordagens funcionam bem na prática.
Se a interface do DTO estender o Magento\Framework\Api\ExtensibleDataInterface
, o modelo deverá ser estendido Magento\Framework\Model\AbstractExtensibleModel
.
Se você não se importa com a extensibilidade, o modelo pode simplesmente continuar a estender a classe base do modelo ORM Magento\Framework\Model\AbstractModel
.
Como o exemplo HamburgerInterface
estende o ExtensibleDataInterface
modelo de hambúrguer, estende o AbstractExtensibleModel
, como pode ser visto aqui:
<?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);
}
}
Extrair os nomes das propriedades em constantes permite mantê-los em um só lugar. Eles podem ser usados pelo par getter / setter e também pelo script de instalação que cria a tabela do banco de dados. Caso contrário, não há benefício em extraí-los em constantes.
O SearchResult
A SearchResultsInterface
é a mais simples das três interfaces para implementar, pois pode herdar toda a sua funcionalidade de uma classe de estrutura.
<?php
namespace VinaiKopp\Kitchen\Model;
use Magento\Framework\Api\SearchResults;
use VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface;
class HamburgerSearchResult extends SearchResults implements HamburgerSearchResultInterface
{
}
Configure as preferências do ObjectManager
Embora as implementações estejam completas, ainda não podemos usar as interfaces como dependências de outras classes, já que o gerenciador de objetos do Magento Framework não sabe quais implementações usar. Precisamos adicionar uma etc/di.xml
configuração para com as preferências.
<?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>
Como o repositório pode ser exposto como um recurso de API?
Esta parte é realmente simples, é a recompensa por realizar todo o trabalho criando as interfaces, as implementações e conectando-as.
Tudo o que precisamos fazer é criar um etc/webapi.xml
arquivo.
<?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>
Observe que essa configuração não apenas habilita o uso do repositório como pontos de extremidade REST, mas também expõe os métodos como parte da API SOAP.
No primeiro exemplo de rota, <route method="GET" url="/V1/vinaikopp_hamburgers/:id">
o espaço reservado :id
deve corresponder o nome do argumento ao método mapeado public function getById($id)
.
Os dois nomes têm que combinar, por exemplo, /V1/vinaikopp_hamburgers/:hamburgerId
não iria funcionar, pois o método nome da variável argumento é $id
.
Para este exemplo, eu configurei a acessibilidade para <resource ref="anonymous"/>
. Isso significa que o recurso é exposto publicamente sem nenhuma restrição!
Para disponibilizar um recurso apenas para um cliente conectado, use <resource ref="self"/>
. Nesse caso, a palavra especial me
no URL do terminal de recurso será usada para preencher uma variável de argumento $id
com o ID do cliente conectado no momento.
Dê uma olhada no Cliente Magento etc/webapi.xml
e CustomerRepositoryInterface
se você precisar.
Por fim, <resources>
também pode ser usado para restringir o acesso a um recurso a uma conta de usuário administrador. Para fazer isso, defina a <resource>
ref como um identificador definido em um etc/acl.xml
arquivo.
Por exemplo, <resource ref="Magento_Customer::manage"/>
restringiria o acesso a qualquer conta de administrador que tenha o privilégio de gerenciar clientes.
Um exemplo de consulta da API usando curl pode ser assim:
$ curl -X GET http://example.com/rest/V1/vinaikopp_hamburgers/123
Nota: escrevendo isso começou como uma resposta para https://github.com/astorm/pestle/issues/195
Confira o pilão , compre o Commercebug e torne-se um patreon de @alanstorm