API design is a crucial part of software development. APIs directly influence system maintainability and extensibility depending on extraction and extension points introduced to the system and how they are organized in the codebase.
Due to the extensive size of Magento 2, we introduced a layer of Service Contracts (public APIs) in the scope of each Bounded Context (in terms of Domain Driven Design). These public APIs explicitly expose the services provided by each specific business domain and serve as an entry point to the domain, playing a role of Facades to mask complex implementation logic behind the scenes. In Magento 2, public APIs should be placed in separate API modules. All other modules depend on the API module, while implementations may be easily swapped via di.xml
configuration.
For the scope of this article, we use the Multi-Source Inventory (MSI) project. For example, if a module is named Inventory
, its APIs are declared in a module named InventoryApi
.
Service interfaces, which should be exposed as Web APIs (REST/SOAP/GraphQL), are placed under the Api namespace. While all other APIs, including explicit extension points like Chain or Composite implementations, are placed under Model namespace.
Under the Api\Data
namespace Magento stores Data Transfer Object (DTO) interfaces which represent entities operated by services declared on the level above.
All the public APIs are marked with PHP annotation @api
. Magento keeps Semantic Versioning for modules following the assumption that modules do not have to access private implementation of other modules, all the integration supposed to be implemented via public APIs only. It is important for third-party developers to have dependencies just on the code marked with @api
annotation to make their Magento installation being easily upgradeable. As along the product releases Magento follows Backward Compatibility policies for APIs only and thus guarantees that Public APIs would not be affected. Along with that, reserves the right to change all non-@api
-marked instances which are considered as private module’s implementation. In most cases, modifications of non-api code will only trigger PATCH module version bumps. Whereas changes in public code always trigger MINOR or MAJOR version bumps.
Such differentiation should simplify life for both merchants and extension developers. Both parties want things to be as cost-effective as possible. For merchants, this means that upgrades should be as low-touch as possible, while developers want their extensions to be forward-compatible for as long as possible. By specifying dependencies on modules’ public contract only, merchants can be sure that they will not encounter backwards incompatibilities with subsequent patch versions, and that their upgrade path will remain healthy.
A PHP Interface in Magento can be used several ways by the core product and extension developers.
After the code is released, both API and SPI can be evolved in a backwards-compatible way. But both have their specific limitations.
After the API is released, new behavior can be added, but none can be removed. Existing behavior cannot be modified.
Examples of APIs:
After SPI is released, existing behavior can be removed, but none can be added. Existing behavior cannot be modified.
Examples of SPIs:
To prevent over-complicating the development for third-party developers, Magento does not differentiate between APIs and SPIs. As it is still possible that one Magento customization will use a particular interface in an API way, another may provide an implementation for an existing interface (SPI usage). As a result, SPIs are annotated the same way as APIs. Third party developers are responsible to decide how they use interfaces declared in external modules and, depending on those declarations, specify dependency in the module’s composer.json
file.
Example composer.json
:
Taking into account the strict Backward Compatibility policy which Magento has to follow, and that Magento does not distinguish between SPIs and APIs, we have the following prohibited code changes for code marked with @api
:
See DevDocs Backward compatible development to learn more about prohibited code changes for public code.
There is no evolution for @api
interfaces in Backward Compatible way. Any change brought to the interface, regardless of adding or removal behavior, will lead to breaking some clients. The best action to circumvent these circumstances is to make interfaces as atomic as possible. Doing so, we come to the idea of Functional objects (Functors), an interface which consists of the only method.
In this case, if the interface should be modified or removed, it is just marked as @deprecated
and a new version of the interface is introduced, not affecting other services and methods. The more granularly a service is designed, the less impact modifications in this interface would introduce to other services. That’s why good object-oriented programming in the service layer is basically functional programming:
Bringing these concepts to an extreme leads to single-method, immutable services which manipulate with DTOs.
Having single-method services, it is very important to provide proper names to them. As in desirable state, developers should be about to quickly glance at service name to know what this service is responsible for, without opening the listing of the service in an IDE. It’s even better when the responsibility of such self-explanatory interfaces is clear not only to developers but also other stakeholders involved into the project. This Ubiquitous Language in Domain Driven Design.
Sometimes when we are not sure what is a proper name for the entity or service, we ask for feedback from the Community.
Such naming may seem a bit unusual, as there are many recommendations to use nouns naming classes which represent type of objects. This rule is applicable for classes which represent "things/entities". Developers want to name them with nouns, and methods inside these classes represent actions over the entity so that methods usually represented by verbs.
But this approach does not work with Functional Objects that represent "action", so naming them with a verb is more appropriate.
It was discussed to make a more radical shift towards Functional Objects using magic method __invoke
.
The following are Pros and Cons in the approach with __invoke
.
Pros:
__invoke
usage proposal was elimination of unneeded execute methods, which look a bit artificial and redundant:Cons:
$this
:
After completing a short survey with the Community, we decided to use execute method.
https://twitter.com/iminyaylo/status/938122403443552256
The rare exception from the rule of having single-method interfaces are Magento Repositories. Repository interfaces are usually provided to manage domain entities and methods listed inside these interfaces should follow next semantic:
But that is not a strict rule, and the list of the methods could be shorter if based on the business requirements particular entity doesn't have some of the operation(-s). It is important to notice here that list of the methods in the Repository interface could NOT be wider than methods mentioned above, as it is not recommended to add other methods with their own semantic to the Repository interface (those methods are recommended to be put into some dedicated Services).
In Magento 2, Repositories are considered as an implementation of Facade pattern which provides a simplified interface to a larger body of code responsible for Domain Entity management. The main intention is to make API more readable and reduce dependencies of business logic code on the inner workings of a module, since most code uses the facade, thus allowing more flexibility in developing the system.
But internally, Repository still proxies calls to dedicated command-services.
Each Service should follow the CQRS semantic and represent Command or Query, but not both.
More examples of designing Service Contracts for Magento 2 could be found in the scope of Multi-Source Inventory (MSI) project. The project wiki provides information and discussions of the most interesting architectural changes implementing these best practices and Service Contracts.
We recommended reviewing the Magento Service Isolation design document which describes the Modularity approach being introduced to Magento codebase which would help to split the Monolith on independent and isolated set of Services. See DevDocs Service Contracts in the Technical Guidelines for development guidelines and requirement
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.