Best practice: via the service contract
The best practice is always to use the service contract whenever it's possible. You can find the list of reasons here: Magento 2: what are the benefits of using service contracts?
For details on how to implement a service contract I suggest you check this topic: How to implement service contract for a custom module in Magento 2?
If no service contract available
If there is no service contract available, you should use the model repository get
method. Using this method, you benefit from the magento caching system for example for the CategoryRepository
class:
public function get($categoryId, $storeId = null)
{
$cacheKey = null !== $storeId ? $storeId : 'all';
if (!isset($this->instances[$categoryId][$cacheKey])) {
/** @var Category $category */
$category = $this->categoryFactory->create();
if (null !== $storeId) {
$category->setStoreId($storeId);
}
$category->load($categoryId);
if (!$category->getId()) {
throw NoSuchEntityException::singleField('id', $categoryId);
}
$this->instances[$categoryId][$cacheKey] = $category;
}
return $this->instances[$categoryId][$cacheKey];
}
Deprecated load()
method
Magento 2 is slowly moving away from the standard CRUD system by dropping the inheritance system and implementing it via composition using the new 2.1 EntityManager you can find details here: Magento 2.1: using the entity manager
Also I suggest you read this interesting topic about the deprecated CRUD methods: Deprecated save and load methods in Abstract Model
Why not using the resource model load
The main reason is that if you use the resource model load
method, you will skip some important part of the loading system that are implemented in the model load
method, see Magento\Framework\Model\AbstractModel
:
public function load($modelId, $field = null)
{
$this->_beforeLoad($modelId, $field);
$this->_getResource()->load($this, $modelId, $field);
$this->_afterLoad();
$this->setOrigData();
$this->_hasDataChanges = false;
$this->updateStoredData();
return $this;
}
Calling the resource model load
method directly will have the following impact:
_beforeLoad
is not called: thus the model load before events are not dispatched
_afterLoad
is not called: thus the model load after events are not dispatched
- the stored data are not updated which can cause various problems (for instance if you call
prepareDataForUpdate
from Magento\Framework\Model\ResourceModel\Db\AbstractDb
)
Best Answer
I would like to give a bit more detail in addition to the excellent answer of @ryanF.
I would like to sum up the reasons to add a repository for custom entities, give examples how to do so, and also explain how to expose those repository methods as part of the Web API.
Disclaimer: I'm only describing a pragmatic approach how to do this for third party modules - the core teams have their own standards which they follow (or not).
In general, the purpose of a repository is to hide the storage related logic.
A client of a repository should not care whether the returned entity is held in memory in an array, is retrieved from a MySQL database, fetched from a remote API or from a file.
I assume the Magento core team did this so they are able to change or replace the ORM in future. In Magento the ORM currently consists of the Models, Resource Models and Collections.
If a third party module use only the repositories, Magento can change how and where data is stored, and the module will continue to work, despite these deep changes.
Repositories generally have methods like
findById()
,findByName()
,put()
orremove()
.In Magento these commonly are called
getbyId()
,save()
anddelete()
, not even pretending they are doing anything else but CRUD DB operations.Magento 2 repository methods can easily be exposed as API resources, making them valuable for integrations with third party systems or headless Magento instances.
As always, the answer is
To make a long story short, if your entities will be used by other modules, then yes, you probably want to add a repository.
There is another factor that comes into count here: in Magento 2, repositories can easily be exposed as Web API - that is REST and SOAP - resources.
If that is interesting to you because of third party system integrations or a headless Magento setup, then again, yes, you probably want to add a repository for your entity.
How do I add a repository for my custom entity?
Lets assume you want to expose your entity as part of the REST API. If that is not true, you can skip the upcoming part on creating the interfaces and go straight to "Create the repository and data model implementation" below.
Create the repository and data model interfaces
Create the folders
Api/Data/
in your module. This is just convention, you could use a different location, but you should not.The repository goes into the
Api/
folder. TheData/
subdirectory is for later.In
Create the repository interfaceApi/
, create a PHP interface with the methods you want to expose. According to Magento 2 conventions all interface names end in the suffixInterface
.For example, for a
Hamburger
entity, I would create the interfaceApi/HamburgerRepositoryInterface
.Magento 2 repositories are part of the domain logic of a module. That means, there is no fixed set of methods a repository has to implement.
It depends entirely on the purpose of the module.
However, in practice all repositories are quite similar. They are wrappers for CRUD functionality.
Most have the methods
getById
,save
,delete
andgetList
.There may be more, for example the
CustomerRepository
has a methodget
, which fetches a customer by email, wherebygetById
is used to retrieve a customer by entity ID.Here is an example repository interface for a hamburger entity:
}
Important! Here be timesinks!
There are a few gotchas here that are hard to debug if you get them wrong:
The annotations are parsed by the Magento Framework to determine how to convert data to and from JSON or XML. Class imports (that is,
use
statements) are not applied!Every method has to have an annotation with any argument types and the return type. Even if a method takes no arguments and returns nothing, it has to have the annotation:
Scalar types (
string
,int
,float
andbool
) also have to be specified, both for arguments and as a return value.Note that in the example above, the annotations for methods that return objects are specified as interfaces, too.
Create the DTO interfaceThe return type interfaces are all in the
Api\Data
namespace/directory.This is to indicate that they do not contain any business logic. They are simply bags of data.
We have to create these interfaces next.
I think Magento calls these interfaces "data models", a name I don't like at all.
This type of class is commonly known as a Data Transfer Object, or DTO.
These DTO classes only have getters and setters for all their properties.
The reason I prefer to use DTO over data model is that it is less easy to confuse with the ORM data models, resource models or view models... too many things are models in Magento already.
The same restrictions in regards to PHP7 typing that apply to repositories also apply to DTOs.
Also, every method has to have an annotation with all argument types and the return type.
If a method retrieves or returns an array, the type of the items in the array has to be specified in the PHPDoc annotation, followed by an opening and closing square bracket
[]
.This is true for both scalar values (e.g.
int[]
) as well as objects (e.g.IngredientInterface[]
).Note that I'm using an
ExtensibleDataInterface?Api\Data\IngredientInterface
as an example for a method returning an array of objects, I'll won't add the code of the ingredients to this post tough.In the example above the
HamburgerInterface
extends theExtensibleDataInterface
.Technically this is only required if you want other modules to be able to add attributes to your entity.
If so, you also need to add another getter/setter pair, by convention called
getExtensionAttributes()
andsetExtensionAttributes()
.The naming of the return type of this method is very important!
The Magento 2 framework will generate the interface, the implementation, and the factory for the implementation if you name them just right. The details of these mechanics are out of scope of this post though.
Just know, if the interface of the object you want to make extensible is called
\VinaiKopp\Kitchen\Api\Data\HamburgerInterface
, then the extension attributes type has to be\VinaiKopp\Kitchen\Api\Data\HamburgerExtensionInterface
. So the wordExtension
has to be inserted after the entity name, right before theInterface
suffix.If you do not want your entity to be extensible, then the DTO interface does not have to extend any other interface, and the
getExtensionAttributes()
andsetExtensionAttributes()
methods can be omitted.Enough about the DTO interface for now, time to return to the repository interface.
The getList() return type SearchResultsThe repository method
getList
returns yet another type, that is, aSearchResultsInterface
instance.The method
getList
could of course just return an array of objects matching the specifiedSearchCriteria
, but returning aSearchResults
instance allows adding some useful meta data to the returned values.You can see how that works below in the repository
getList()
method implementation.Here is the example hamburger search result interface:
All this interface does is it overrides the types for the two methods
Summary of interfacesgetItems()
andsetItems()
of the parent interface.We now have the following interfaces:
\VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface
\VinaiKopp\Kitchen\Api\Data\HamburgerInterface
\VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface
The repository extends nothing,
the
HamburgerInterface
extends the\Magento\Framework\Api\ExtensibleDataInterface
,and the
HamburgerSearchResultInterface
extends the\Magento\Framework\Api\SearchResultsInterface
.Create the repository and data model implementations
The next step is to create the implementations of the three interfaces.
The RepositoryIn essence, the repository uses the ORM to do it's job.
The
getById()
,save()
anddelete()
methods are quite straight forward.The
HamburgerFactory
is injected into the repository as a constructor argument, as can be seen a bit further below.Now to the most interesting part of a repository, the
getList()
method.The
getList()
method has to translate theSerachCriteria
conditions into method calls on the collection.The tricky part of that is getting the
AND
andOR
conditions for the filters right, especially since the syntax for setting the conditions on the collection is different depending on whether it is an EAV or a flat table entity.In most cases,
getList()
can be implemented as illustrated in the example below.Filters within a
FilterGroup
must be combined using an OR operator.Separate filter groups are combined using the logical AND operator.
Phew
The DTOThis was the biggest bit of work. The other interface implementations are simpler.
Magento originally intended developers to implement the DTO as separate classes, distinct from the entity model.
The core team only did this for the customer module though (
\Magento\Customer\Api\Data\CustomerInterface
is implemented by\Magento\Customer\Model\Data\Customer
, not\Magento\Customer\Model\Customer
).In all other cases the entity model implements the DTO interface (for example
\Magento\Catalog\Api\Data\ProductInterface
is implemented by\Magento\Catalog\Model\Product
).I've asked members of the core team about this at conferences, but I didn't get a clear response what is to be considered good practice.
My impression is that this recommendation has been abandoned. It would be nice to get an official statement on this though.
For now I've made the pragmatic decision to use the model as the DTO interface implementation. If you feel it is cleaner to use a separate data model, feel free to do so. Both approaches work fine in practice.
If the DTO inteface extends the
Magento\Framework\Api\ExtensibleDataInterface
, the model has to extendMagento\Framework\Model\AbstractExtensibleModel
.If you don't care about the extensibility, the model can simply continue to extend the ORM model base class
Magento\Framework\Model\AbstractModel
.Since the example
HamburgerInterface
extends theExtensibleDataInterface
the hamburger model extends theAbstractExtensibleModel
, as can be seen here:Extracting the property names into constants allows to keep them in one place. They can be used by the getter/setter pair and also by the Setup script that creates the database table. Otherwise there is no benefit in extracting them into constants.
The SearchResultThe
SearchResultsInterface
is the simplest of the three interfaces to implement, since it can inherit all of it's functionality from a framework class.Configure the ObjectManager preferences
Even though the implementations are complete, we still can't use the interfaces as dependencies of other classes, since the Magento Framework object manager does not know what implementations to use. We need to add an
etc/di.xml
configuration for with the preferences.How can the repository be exposed as an API resource?
This part is really simple, it's the reward for going through all the work creating the interfaces, the implementations and wiring them together.
All we need to do is create an
etc/webapi.xml
file.Note that this configuration not only enables the use of the repository as REST endpoints, it also exposes the methods as part of the SOAP API.
In the first example route,
<route method="GET" url="/V1/vinaikopp_hamburgers/:id">
, the placeholder:id
has to match the name of the argument to the mapped method,public function getById($id)
.The two names have to match, for example
/V1/vinaikopp_hamburgers/:hamburgerId
would not work, since the method argument variable name is$id
.For this example I've set the accessability to
<resource ref="anonymous"/>
. This means the resource is exposed publically without any restriction!To make a resource only available to a logged in customer, use
<resource ref="self"/>
. In this case the special wordme
in the resource endpoint URL will be used to populate an argument variable$id
with the ID of the currently logged in customer.Have a look at the Magento Customer
etc/webapi.xml
andCustomerRepositoryInterface
if you need that.Finally, the
<resources>
can also be used to restrict access to a resource to an admin user account. To do this set the<resource>
ref to an identifier defined in anetc/acl.xml
file.For example,
<resource ref="Magento_Customer::manage"/>
would restrict access to any admin account who is privileged to manage customers.An example API query using curl could look like this:
Note: writing this started as an answer to https://github.com/astorm/pestle/issues/195
Check out pestle, buy Commercebug and become a patreon of @alanstorm