As we continue to work on the services in Magento 2, patterns are emerging. This post describes a few such patterns. These patterns are going to be released on GitHub progressively over the next few weeks as code gets completed.
Disclaimer: The opinions expressed here are my own and do not represent any commitment on behalf of my employer. More importantly, things may change before GA. This sharing is to explain and gather feedback from which the team can learn from and adjust if appropriate.
What is a Service Contract?
What is a Magento 2 Service Contract exactly? To be precise, it is a set of PHP interfaces (and possibly classes) residing under a new Api directory of a module. For example, the service contract declared in the Magento_Customer module has the PHP namespace of Magento\Customer\Api.
There is some ongoing discussion about the structure of the Api directory. Should it be flat or nested? Right now the intent is to go simple. The top level namespace has all the service interfaces that make sense calling directly from other modules (via PHP code) or binding to REST or SOAP endpoints. (To call a service method from another module, the dependency injection framework is used to locate the implementation of the interface.) These interfaces provide access to business logic that can be called. Within the Api namespace there is a single sub-namespace of Data. This directory contains interfaces to access data structures passed to or returned from functions in the Api directory.
Why Do Services Matter?
Before getting into the patterns, I thought I would just repeat a few points about why I see services as being so important to Magento.
- Magento is a modular system. Service contracts define agreements between clients and implementations of services. For a client, a well-defined API it can rely on to be (relatively) stable across upgrades is great. But Magento also wants to allow other implementations to be slotted in as well, and service contracts help here too.
- All service contracts (that obey some simple rules) can be easily exposed as REST or SOAP with no additional PHP coding required. That opens up more external integration possibilities and more creativity in frontend design and technologies. It is also open to all extension developers.
Ok, so let’s look at some of the emerging patterns. I am going to use the Customer module because it is most likely to be pushed to public GitHub first. (It just missed the alpha-101 push.)
Repository interfaces give access to persistent data entities. In the case of the Customer module, persistent data entities include Customer, Address, and Group. Hence there are three interfaces CustomerRepositoryInterface, AddressRepositoryInterface, and GroupRepositoryInterface. Repository interfaces have the following methods:
- save(data entity interface): Creates a new record if no id present, otherwise updates an existing record with the specified id.
- get(id): Performs a database lookup by id and returns a data entity interface (such as CustomerInterface or AddressInterface).
- getList(search criteria): Performs a search for all data entities matching the search criteria and returns a search results interface to give access to the set of matches.
- delete(data entity interface): Deletes the specified entity (the key is in the entity).
- deleteById(id): Deletes the specified entity when you only have the key for the entity.
An interface is defined per data entity so the get() method for example can return exactly the right type.
Management interfaces contain various management functions that are not related to repositories. For example,
- AccountManagementInterface: Contains methods like createAccount(), changePassword(), activate(), and isEmailAvailable().
- AddressManagementInterface: Only has a validate() function to check an address is valid.
If additional patterns emerge, some of these functions may make their way into new patterns. For example, changing a password is never likely to be shared across data entities. Validation on the other hand might, so perhaps a new pattern will emerge to introduce AddressValidationInterface.
Data Entity Interface
The Customer module currently defines 3 data entities: Customer, Address, and Group.
To me data entities are one of the cool side benefits of service contracts. I think of them like as being a part of the “Magento logical schema”. There are normally database tables under these entities, but the database tables could be complicated – for example some attributes may be stored in an EAV table (so one data entity may be represented by a set of MySQL database tables). So data entities reveal a simpler data model than the underlying relational database schema.
For example, the data entity model for the Customer module can be shown as follows:
It just makes it easier to understand the Magento data model being able to talk about data entities at a level higher than MySQL tables.
Further, an idea is to eventually allow different storage technologies for different data collections. For example, use a NoSQL database to replace product tables. There are challenges here still to solve (like how to deal with database transactions that cross database technologies), but separating the conceptual model of Customer from the physical MySQL database schema representation makes this easier to comprehend.
Oh, if you poke around in the Magento\Customer\Api\Data directory you may also notice RegionInterface. Region is not a data entity as it does not have a Repository Interface. The Region class is only used to group related business logic. The data for a Region instance is stored in the Address entity.
If you look at the data entities you may notice there are only methods to read from a data entity instance. So how do you create a data entity in PHP code? The answer is Magento is using the “builder” pattern where you have a class with setter methods to set all the properties, then you call a final create() method to return a new instance for you. If you hunt around the GitHub repo you won’t find the builder code. This is because they are automatically generated for you. For example, there will be a CustomerBuilder class created in the var/generated/Magento/Customer/Api/Data directory. This class will have all the setter methods. (Again, you get a handle to builders via the Magento 2 dependency injection framework.)
So how can you modify an instance of a data entity you got from somewhere? The answer is simple: you can’t! Actually doing so can be dangerous as some sections of code rely on entities not being changed (e.g. in shared caches). Instead, each builder has a populate($entity) method that will clone the attributes out of one entity into a new entity. You can then call the setter methods to change any attributes, then finally create() a new instance.
$this->customerBuilder->populate($customer); $this->customerBuilder->setGroupId(CustomerGroupServiceInterface::NOT_LOGGED_IN_ID); $newCustomer = $this->customerBuilder->create();
There is also a populateWithArray($nameValuePairsArray) for say populating an entity from a HTML form.
Search Results Interface
A Search Results Interface is returned by a getList() call that is passed search criteria. It provides access to the results of a search. An interface needs to be defined per data entity for type hinting purposes. That is, getItems() in CustomerSearchResultsInterface returns an array of CustomerInterface data entities; in GroupSearchResultsInterface it returns an array of GroupInterface data entities.
(Yeah, it would be kinda nice if PHP had generic types like Java or Hack. We are driving for type safety and hinting in APIs in Magento 2, so we are resisting a generic SearchResultsInterface that just returns an array with no hinting of the types of values in the array. In Java or Hack you would instead say something like SearchResults<Customer>. But it has not been a big deal so far.)
Metadata interfaces provide information about what attributes are defined for an entity. This includes custom attributes which I will touch on below.
Outstanding Question – Versioning
Currently the service contract is in the Api directory of a module. That might not be ideal as one of the goals is to make it easier to provide different implementations of the service contract. In that case, you probably don’t want the rest of the module – just the contract.
It also ties the service contract version number to the module version number. Having the contract version number stable across multiple releases may be preferable so extension developers can have a stable version number to depend on.
Pulling out the Api directory however does have negative implications. Should you pull out the presentation layer code as well from the module? I would say “yes” if it depends only on the Api directory (and so could be used with other implementations), and “no” if it is hard coded to work with a specific implementation. For example, store front layout files may depend only the service contract and so be generic, but administration functionality probably would be specific to an implementation of the service contract. So you could end up with three modules for each one existing module – a store front presentation module, the service contract module, and a module for the implementation of the contract.
Outstanding Question – Custom Attributes
A key feature of Magento is its extensibility. Data entity interfaces above defined in PHP are defined with a set of getter methods to access all attributes of the entity. However, there are two additional types of custom attributes which are also accessible via a getCustomAttribute($name) method.
- EAV Attributes can be defined for a local site via the administration interface. These may different per site, so cannot be represented in the PHP data entity interface.
- Extension Attributes can be introduced by extension modules.
For example, an Intergalatic module could be loaded that adds a “planet” attribute to Address. Extension attributes could be exposed by declaring a PHP interface, but for now they are also accessed by name (string). This may change before final release, but it has not been decided how best to achieve this yet. (One approach would be to declare all data entity attributes in XML files, then have a tool to look for all XML files related to a data entity and machine generate the data entity interface based on what a site has locally loaded. There are problems with this approach however. This may be a good community discussion point to get feedback on. For now the getCustomAttribute($name) method is used instead.)
Another Change – No More Copying
There is one other point that is being changed around that you would probably only notice if you dug deep into the existing (alpha101 and earlier) service code. Currently “data” is implemented using classes that hold a copy of the data to pass in to a function or return from it. This is changing to be interfaces where existing classes inside the module implement the interface. This reduces the amount of data copying that goes on.
Service contracts to me are a really important part of enhancing the modularity of Magento. A module can define a service contract to provide a well-defined, stable API for other modules (and third party extensions) to use. They also provide an easy way to expose business logic via REST or SOAP interfaces.
This post was to introduce emerging patterns in the service contracts. It is going to be an important part of Magento, so the team is keen to get them right.
There are also some outstanding issues. Should service contracts be in separate modules to help with versioning? Should extension attributes be exposed via setters and getters like the main module’s entity definition? Opinions always welcome, especially during the developer beta period during Q1 2015.