Magento 2 Service Contract Patterns

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

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

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:

Customer Data Entities

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.

Builders

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

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.

Conclusions

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.

9 comments

  1. Nice job on decoupling the code. I would really love to see if business logic of component uses interface. I this would be perfect to have a separate module that provides implementation guidelines, not the actual implementation. So we could just take off module with implementation and replace with our own, that will implement all the required interfaces.

    As for customizations, I would prefer have static interface, that is not auto-generated. It would be stopper for implementing a functionality without extending from original class. Let’s say if I choose another storage for retrieving of customer data in Magento, instead of using Magento style, than I am in trouble, because interface is modified on one of instances. Interfaces always should be static, so I really prefer to have getCustomAttribute() method instead 🙂

    1. On first point are you saying you would prefer flexibility of having more modules – you don’t care there could be lots more modules as a result?

      On the second point one idea was to require implementations to use getCustomAttribute() to expose attributes, but profile a proxy class that had all the methods. We are going to start with just getCustomAttribute() but would love to get hands on feedback when people try it out. This is partly what the dev beta period is for q1 next year. Get feedback.

      Thanks for the comment!

  2. Josh Di Fabio · · Reply

    Thanks for taking the time to write this; I really like where Magento 2 is going.

    If the aim of the service layer is to separate API from implementation then wouldn’t it make sense for the API interfaces to be completely independent of any implementation (i.e. the service contracts should reside in their own Composer packages)? If I wanted to use a custom implementation of the sales module, why should I pull in Magento’s considerable vanilla implementation when all I need from that package are the API interfaces? It seems like it should become commonplace to require only the API interfaces of a module.

    I also think versioning will become messy if implementations and API interfaces live together, which you have already hinted at.

    Thanks for being so open about what Magento are doing and what your plans are; it all looks very encouraging!

  3. Thanks for the post Alan, this is looking nice.
    I really like explicit interfaces, but I also think Ivan has a point.

    How about keeping the getCustomAttribute() in the “static” ExtensibleDataInterface and generate an additional ExtendedCustomerInterface interface with explicit getters? The implementation for it should probably also be generated as a trait.
    That way any third-party implementation could easily implement the ExtendedCustomerInterface use the trait and, or choose not to and just implement the regular CustomerInterface.

    Regarding keeping the interfaces in the modules that implement them, I think that would be okay for now, but once they stabilize I would also prefer them to be in a separate package.
    It should be possible to make the choice to move the interfaces into their own module later on thanks to composer.

    How about keeping the Interfaces in the modules that use them instead of in the modules that implement them?
    As a third party module developer, the only reason for me to implement a given interface would be so my module can work with that client client, so I need that code anyway.
    If this is still too much coupling or you don’t want to split the interfaces along the client use cases, then having them in their own module probably will be better in the long run.

    1. Thanks for the feedback. The getCustomAttribute($name) question I think needs to stew for a while. Would love some people to try things and see how it works. Right now we are getting more services defined, so will start there and keep it simple. Lots of possibilities – I would like to get some real experience to base final decision on here.

      Regarding the idea to put interfaces in client modules, this is effectively what we have been calling an SPI pattern. It seems to be a question of whether you expect to have multiple clients or multiple implementations. The argument for putting the group of API interfaces in its own module is to support multiple clients (e.g. lots of modules call the “Customer” module) and muliple (alternative) implementations easily. Putting it in one client feels strange for other clients; in one module implementation feels strange for other module implementations.

    2. Thanks for the comments!

      On the last paragraph, Sometimes we would have interfaces in the client – we were referring to that as SPI (or maybe adaptor) as if there is only one client, then the reason for having the interface is to swap in different implementations. Service contracts conceptually are intended for when multiple client modules and possibly multiple implementations exist.

  4. I suppose that Builders are the same as Factories in the latest magento version. Right?

    1. We dropped builders because in practice they proved too restrictive / too much object copying. But yes, you use a factory now instead of a builder

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.