Before you start, please check the first part: Database in Magento 2: Models, Resource Models, and Collections.
There are several important concepts that have been incorporated into Magento 2, such as plugins, dependency injection, and service contracts. These demonstrate that, overall, the changes in going from Magento 1 to Magento 2 are focused more on the way you do things — to be more flexible and efficient — rather than introducing new features.
Service contracts fulfill a number of important functions, such as:
- Improving the upgrade process
- Formalizing the customization process
- Decoupling modules
Basically, service contracts are a set of interfaces that are available for modules to declare standard APIs. A service layer is used for making customizations without having to delve deeper into the product core. They also help with module interoperability. This is possible because the implementation of an interface might change, but the signature will not.
Service contracts are also designed to make the customization process more formal and straightforward, helping to minimize situations where you have to hunt for classes and haphazardly make changes that might fulfill one function but break others. Magento 2 implements the “development based on interface” concept, in which a developer relies only on the public methods declared in an interface, not in the implementation.
In Magento 2:
- Modules only communicate through the API.
- One module is not aware of the internals of another, so the implementation can change.
- Modules can be disabled or deployed on separate servers.
Operations can be divided into two groups: data and operational. The data API provides access to a module’s entity data. It can be found in the folder _MODULE_NAME_/Api/Data.
The operational API not only provides data but also drives the actual operations used on that data. These APIs allow business operations to function properly between modules. This API usually includes the public methods of Magento 1 models and helpers.
The data API only exposes CRUD methods, while the operational API actually does something. The operational APIs for a module can be found in the folder MODULE_NAME_/Api (except for data, which is located in a subfolder).
In Magento 1, when you wanted to customize a module, you had to read the core code. You had to understand how it worked, in order to be able to create the required change.
In Magento 2, you can now customize a module using an API interface that communicates with the model, without interacting directly with the core, a much safer approach. What are some of the benefits of using a services approach?
- It provides comprehensive internal documentation that allows you to make customizations without having to go into the core.
- Following this approach helps to minimize conflicts between modules.
- Magento upgrades are much safer to execute without anything breaking.
- Clear extension points make customizations easier.
There are also some drawbacks to using a services approach, of which you should be aware.
- Services will often either be too simple or too complex, too broad or too granular. It will be more difficult to perform low-level, refined customizations.
- The approach may work differently with different implementations.
- It may be more difficult to debug an application using this approach.
In this diagram, we have separated the operational API into more detailed components, specifically the business logic API and repositories. The data API and the framework API remain their own logical units. Repositories provide the equivalent of service-level collections, while the business logic API provides the actual business operations. The framework API provides interfaces, implementations, and classes for various parts.
Here is an example of a business logic API, for the Magento catalog.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<strong>namespace </strong>Magento\Catalog\Api; /** * @api */ interface ProductTypeListInterface { /** * Retrieve available product types * * @return \Magento\Catalog\Api\Data\ProductTypeInterface[] */ public function getProductTypes(); } |
The code below provides an example of implementing the business logic API, using the \Magento\Customer\Api\AccountManagementInterface
1 2 3 4 5 6 7 8 9 |
** * Authenticate a customer by username and password * * @param string $email * @param string $password * @return \Magento\Customer\Api\Data\CustomerInterface * @throws \Magento\Framework\Exception\LocalizedException */ public function authenticate($email, $password); |
To see an example for a concrete implementation, search for the public function \Magento\Customer\Model\AccountManagement::authenticate() in the app installation.
An example of a repository interface and its implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//INTERFACE namespace Magento\Catalog\Api; /** * @api */ interface ProductRepositoryInterface { … namespace Magento\Catalog\Model; //IMPLEMENTATION class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterface { ... |
One important part of the operational API is repositories. Typically, a repository is an interface that provides access to a set of objects using the getList() method. It has no obligation to extend something from Framework.
Magento Webdesign
Take your online store to the next level with BelVG Magento Webdesign
Visit the pageHowever, repository getList() methods typically receive an instance of the SearchCriteriaInterface, defined as part of the framework. The framework also supplies an implementation for that interface, namely Magento\Framework\Api\SearchCriteria.
The data API implementation may extend AbstractExtensibleObject, but it is also possible for data API implementations to extend other classes or nothing at all. Business logic API implementations usually extend nothing. Repositories usually extend nothing but expect a SearchCriteriaInterface implementation as the parameter to their getList() method.
AbstractSimpleObject is a class that is similar in concept to the Varien object in Magento 1. It also has a data property, but it does not provide magic getters and setters, it only provides protected _setData() and _get() methods, as you’ll see in the following code example. This class is used to extend if some data needs to be configurable via di.xml. The object factory will inject the $data array as a constructor argument.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
namespace Magento\Framework\Api; abstract class AbstractSimpleObject { protected $_data; public function __construct(array $data = []) { $this->_data = $data; } protected function _get($key) { return isset($this->_data[$key]) ? $this->_data[$key] : null; } public function setData($key, $value) { $this->_data[$key] = $value; return $this; } public function __toArray() { ... } } |
AbstractSimpleObject provides _get(), which returns an item from the data array, or null if the requested key doesn’t exist. The important takeaway point is that this object does not provide public getters and setters. They need to be implemented as required by the concrete implementation extending AbstractSimpleObject.
You extend the AbstractSimpleObjectBuilder, optionally adding methods to specify the data that is required to instantiate the simple object. Then you create an instance of your Builder and send the data to Builder. The Builder takes data and injects it into the SimpleObject when create() is called. This process reflects not having public methods.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
namespace Magento\Framework\Api; /** * Base Builder interface for simple data Objects */ interface SimpleBuilderInterface { /** * Builds the Data Object * * @return AbstractSimpleObject */ public function create(); /** * Return data Object data. * * @return array */ public function getData(); } |
SimpleBuilderInterface has two methods, create() and getData(). These are implemented mainly by the AbstractSimpleObjectBuilder.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
namespace Magento\Framework\Api; abstract class AbstractSimpleObjectBuilder implements SimpleBuilderInterface { protected $data; protected $objectFactory; public function __construct(ObjectFactory $objectFactory) { $this->data = []; $this->objectFactory = $objectFactory; } public function create() { $dataObjectType = $this->_getDataObjectType(); $dataObject = $this->objectFactory->create($dataObjectType, ['data' => $this->data]); $this->data = []; return $dataObject; } protected function _set($key, $value) { $this->data[$key] = $value; return $this; } protected function _getDataObjectType() { $currentClass = get_class($this); $builderSuffix = 'Builder'; $dataObjectType = substr($currentClass, 0, -strlen($builderSuffix)); return $dataObjectType; } public function getData() { return $this->data; } } |
Within the AbstractSimpleObjectBuilder::create() method, the data array is passed to the object factory to be injected into the $dataObject during instantiation. You can see from the code of the method _getDataObjectType() that the simple object type is the class name of the builder without the word “builder” attached at the end.
Partner With Us
Let's discuss how to grow your business. Get a Free Quote.Repositories provide access to data sources, acting as a type of intermediary.
The advantage to using this design pattern and its service API is that your application can function independently of the number of data sources and how they are connected to the app. This allows for easier upgrades, and since repositories deal with data objects and not models, the structure is compatible with any Object Relational Mapping system. So, for example, if you were to expand the product line of a store, in theory, you could add data sources without having to modify the application itself. Only the repository would have to know about the new sources.
Repository | Collection |
As Service, will remain unchanged with new releases | Might be changed, or totally replaced with different mechanism in new releases |
Deals with Data Objects | Returns, a list of Magento Models |
Provides high-level access to the data | Provides low-level access to the data |
Support SearchCriteria mechanism for filtering and sorting | Provides the own interface for most of the database operations. Highly customizable. |
Does not provide low-level access to the database | Provides an access to the select object, gives an ability to create custom queries |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
namespace Magento\Customer\Api; /** * Customer CRUD interface. * @api */ interface CustomerRepositoryInterface { public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $passwordHash = null); public function get($email, $websiteId = null); public function getById($customerId); public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria); public function delete(\Magento\Customer\Api\Data\CustomerInterface $customer); public function deleteById($customerId); } |
Here is a code example of a repository — the CustomerRepositoryInterface. As you can see, the customer repository interface is composed of a number of public methods which all deal with the persistence layer. The parameters are Magento\Customer\Model\Data\Customer instances, not to be confused with regular customer models, Magento\Customer\Model\Customer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
namespace Magento\Customer\Model\ResourceModel; class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInterface { … public function get($email, $websiteId = null) { $customerModel = $this->customerRegistry->retrieveByEmail($email, $websiteId); return $customerModel->getDataModel(); } public function getById($customerId) { $customerModel = $this->customerRegistry->retrieve($customerId); return $customerModel->getDataModel(); } … } |
In looking at the get() function, you will notice the customer registry ($this->customerRegistry) object.
In contrast to one large registry in Magento 1, Magento 2 has a number of smaller registries. If you call get() with the same arguments two times, it will return the same instance. This is an example of the identity map design pattern.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
namespace Magento\Customer\Model; class CustomerRegistry { … public function retrieve($customerId) { if (isset($this->customerRegistryById[$customerId])) { return $this->customerRegistryById[$customerId]; } /** @var Customer $customer */ $customer = $this->customerFactory->create()->load($customerId); if (!$customer->getId()) { // customer does not exist throw NoSuchEntityException::singleField('customerId', $customerId); } else { $emailKey = $this->getEmailKey($customer->getEmail(), $customer->getWebsiteId()); $this->customerRegistryById[$customerId] = $customer; $this->customerRegistryByEmail[$emailKey] = $customer; return $customer; } } |
Here you see that the registry uses the $customerId to check if the requested customer model already was loaded. If not, the injected customerFactory is used to create a customer model instance, which then is loaded. The fully loaded model is then placed in the registry properties customerRegistryById and customerRegistryByEmail. On subsequent calls, the customer already will be known to the registry and will be returned directly.
SearchCriteria is the parameter in a repository’s getList() method, which defines filters, sorting, and paging.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
namespace Magento\Customer\Model; class GroupManagement implements \Magento\Customer\Api\GroupManagementInterface { … public function getLoggedInGroups() { $notLoggedInFilter[] = $this->filterBuilder ->setField(GroupInterface::ID) ->setConditionType('neq') ->setValue(self::NOT_LOGGED_IN_ID) ->create(); $groupAll[] = $this->filterBuilder ->setField(GroupInterface::ID) ->setConditionType('neq') ->setValue(self::CUST_GROUP_ALL) ->create(); $searchCriteria = $this->searchCriteriaBuilder ->addFilters($notLoggedInFilter) ->addFilters($groupAll) ->create(); return $this->groupRepository->getList($searchCriteria)->getItems(); } … } |
Looking at the code above, you can see how the search criteria instance is created, using SearchCriteriaBuilder. First, all filters are added to the builder, and then the SearchCriteria object is instantiated using the builder’s create() method.
SearchCriteria implements the SearchCriteriaInterface and extends the class AbstractSimpleObject. It uses the set of filters contained within Search\FilterGroup, which in turn wraps Filter instances. SearchCriteriaBuilder, extending AbstractSimpleObject, is used to create the SearchCriteria object.
1 2 3 4 5 6 7 8 9 10 11 12 |
namespace Magento\Framework\Api; interface SearchCriteriaInterface { public function getFilterGroups(); public function setFilterGroups(array $filterGroups = null); public function getSortOrders(); public function setSortOrders(array $sortOrders = null); public function getPageSize(); public function setPageSize($pageSize); public function getCurrentPage(); public function setCurrentPage($currentPage); } |
Here is a list of the public methods available within the SearchCriteriaInterface. Please note that usually the manipulators for the SearchCriteria are not used. According to best practices, all properties should be set on the SearchCriteriaBuilder, which in turn injects the complete $data array during instantiation. Once the SearchCriteria has been instantiated in this way, only getters should be used on it.
The Filter class extends AbstractSimpleObject and adds the methods get/setField(), get/setValue(), and get/setConditionType(). Just as with SearchCriteria, even though the class exposes setters, it will usually be constructed using the FilterBuilder, which injects all values during instantiation.
SearchResult:
- Implement methods listed in SearchResultInterface
- Is implemented by Magento\Framework\Api\SearchResult
- Extends AbsractSimpleObject
As its name implies, SearchResults is an object that represents search results. It is the base interface returned from repository getList() methods.
1 2 3 4 5 6 7 8 9 10 |
namespace Magento\Framework\Api; interface SearchResultsInterface { public function getItems(); public function setItems(array $items); public function getSearchCriteria(); public function setSearchCriteria(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria); public function getTotalCount(); public function setTotalCount($totalCount); } |
This code example lists the public methods of the SearchResultsInterface.
Business logic:
- Implements business features
- Does not connect to any generic framework
This overview diagram illustrates how the elements that compose the theoretical API relate to a module. The business logic API contains all the logic not contained within repositories or the data API, and is responsible basically for all the actions a module can take. In the business logic part of a module, you have unique features such as sending an email, selecting a product, and placing an order.
Data API
Data objects are essentially a set of fields defined by an interface that allow us to describe the type of data to use. The data API defines the getters and setters for each of these fields. Secondly, we need some flexibility, to define new attributes, provide information about relations, and so on. The data API is designed to meet this need by allowing the assignment of custom attributes. This may seem somewhat of a contradiction: We want the API to offer flexibility, yet we also want it to provide a well-defined structure for interacting with services like SOAP. We will examine both aspects in this module.
Customer Module Implementation | Catalog Module Implementation |
Data interfaces implemented in separate classes | Data interfaces implemented by models |
All service-layer works with data interfaces | Service-layer only works with models |
When customizing developer should care about both – models and data models | Developer should only care about models |
How do you implement the data API? We already know that there is a data API folder containing interfaces that describe the data objects used by the service API. There are two possible ways to implement the interfaces. One is to create dedicated data objects — objects that solely contain data and have no behavior beyond getters and setters. This is outlined in the customer module column of the table.
The second approach is to operate with regular models that additionally implement the getters/setters defined in the data API interface, besides containing their business logic. This is outlined in the catalog module column of the table. The recommended way to work with Magento 2 is to have dedicated data objects that implement data API interfaces. Basically, both approaches operate with data API interfaces. In the first (customer) case, if you call the customer module API, it will return instances of classes that only implement the data interface. Alternatively, if you call the catalog module API, it will return instances of regular models that also implement the data API interface.
So, in general, you should rely only on the interface methods, as the implementation may vary or be changed in future releases. Look at the methods within the interface, and restrict yourself to using only those. Do not rely on, for example, a model save() method, as that may change in future releases and break your system. This is why Magento 2 introduced service layers and API interfaces, to make upgrades safer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
namespace Magento\Customer\Model\ResourceModel; class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInterface { … public function getList(SearchCriteriaInterface $searchCriteria) { … $collection->setCurPage($searchCriteria->getCurrentPage()); $collection->setPageSize($searchCriteria->getPageSize()); $customers = []; /** @var \Magento\Customer\Model\Customer $customerModel */ foreach ($collection as $customerModel) { $customers[] = $customerModel->getDataModel(); } $searchResults->setItems($customers); return $searchResults; } … } |
The CustomerRepository is an example of the implementation of data objects.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
namespace Magento\Catalog\Model; class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterface { … public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) { … $collection->load(); $searchResult = $this->searchResultsFactory->create(); $searchResult->setSearchCriteria($searchCriteria); $searchResult->setItems($collection->getItems()); $searchResult->setTotalCount($collection->getSize()); return $searchResult; } … } |
In the ProductRepository example, you see that the service layer API directly returns the product models. Both approaches generally use $searchResult interface.
Magento Custom Development
Take your online store to the next level with BelVG Magento Custom Development
Visit the pageExtensible Object
Unlike API services, which can be arbitrary, the data API situation is different because the metadata of the data is the same; so, its representation is the same. Every interface extends a special interface, ExtensibleDataInterface. The concrete implementation implements the interface but most also extend AbstractExtensibleObject. This object has a couple of very important methods: get/setExtensionAttributes(). These methods return the extension object, which is used to customize objects. Your data goes into the extension object. The AbstractExtensibleObject implementation includes sets of attributes (standard, custom, and extension) and extends the AbstractExtensibleObject.
This object extends ExtensibleDataInterface using two factories: ExtensionAttributeFactory and AttributeValueFactory. AbstractExtensibleObject also implements CustomAttributeDataInterface, which in turn extends ExtensibleDataInterface.
1 2 3 4 5 6 7 8 |
namespace Magento\Framework\Api; interface ExtensibleDataInterface { /** * Key for extension attributes object */ const EXTENSION_ATTRIBUTES_KEY = 'extension_attributes'; } |
The ExtensibleDataInterface only contains a constant used as the data array key for an extension_attributes object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
namespace Magento\Framework\Api; interface CustomAttributesDataInterface extends ExtensibleDataInterface { const CUSTOM_ATTRIBUTES = 'custom_attributes'; /** * Get an attribute value. * * @param string $attributeCode * @return \Magento\Framework\Api\AttributeInterface|null */ public function getCustomAttribute($attributeCode); public function setCustomAttribute($attributeCode, $attributeValue); public function getCustomAttributes(); public function setCustomAttributes(array $attributes); } |
The CustomDataInterface also contains a key. This one is for custom attributes.
This diagram provides more detail on the ExtensibleObject diagram, three slides earlier.
AbstractExtensibleObject will take a parameter of ExtensionAttributesFactory or AttributeValueFactory via dependency injection. When you create an implementation of that interface, it extends its own extensible object with its own custom logic. If you want to have some objects in your method, you would have to use or alter a constructor. This all gets a little more complex when you add an interface to the model.
So instead, specify the custom attributes using the getters and setters defined in the AbstractExtensibleObject. You set custom attributes using the AttributeValueFactory(), which provides an AttributeInterface. Then, with this interface, you can set the code and the values you want. Every EAV interface implementation will have its own metadata object. The metadata object is an analog of the EAV config class in Magento 1, which provided information on attributes, classes, entities, and more.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
namespace Magento\Framework\Api; use \Magento\Framework\Api\AttributeValueFactory; abstract class AbstractExtensibleObject extends AbstractSimpleObject implements CustomAttributesDataInterface { … protected function _getExtensionAttributes() { return $this->_get(self::EXTENSION_ATTRIBUTES_KEY); } protected function _setExtensionAttributes(\Magento\Framework\Api\ExtensionAttributesInterface $extensionAttributes) { $this->_data[self::EXTENSION_ATTRIBUTES_KEY] = $extensionAttributes; return $this; } ... } |
The code here demonstrates how to set an extension attributes object.