Skip to content

Service Contracts

Ievgen Shakhsuvarov edited this page Jul 16, 2019 · 24 revisions

Table of Contents

MSI Data Interfaces

You are already familiar with Data Interfaces we introduced and manipulate for MSI project. Most of them are presented on the diagram

Here we will describe you Service Interfaces which operate over these Data Interfaces and provide an ability for business logic to make a valuable operation with existing data.

Magento Repositories. Basics

First of all, in Magento 2 for Domain Entities, we usually provide RepositoryInterface which helps to manage particular entity. Usually typical repository provides next methods:

 public function save(\Magento\Module\Api\Data\DataInterface $entityData);
 public function get($entityId);
 public function delete(\Magento\Module\Api\Data\DataInterface $entityData);
 public function deleteById($entityId);
 public function getList(SearchCriteriaInterface $searchCriteria);`

But that's 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), for example, Source Repository (Magento\InventoryApi\Api\SourceRepositoryInterface) does not have 'delete' method, because there is no business operation as Soruce Removal, all we can do is to disable Source if we don't need it anymore. It's very important to notice here that list of the methods in Repository interface could NOT be wider than methods mentioned above, as it's not recommended to add other methods with own semantic to the Repository interface, those methods recommended to 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.

For example, here you can see Repository for Source and SourceItem entities:

Source, SourceItem Repositories and Multiple Save service for SourceItems

/**
 * This is Facade for basic operations with Source
 * There is no delete method. It is related to that Source can't be deleted due to we don't want miss data
 * related to Sources (like as order information). But Source can be disabled
 *
 * Used fully qualified namespaces in annotations for proper work of WebApi request parser
 *
 * @api
 */
interface SourceRepositoryInterface
{
    /**
     * Save Source data
     *
     * @param \Magento\InventoryApi\Api\Data\SourceInterface $source
     * @return int
     * @throws \Magento\Framework\Exception\CouldNotSaveException
     */
    public function save(SourceInterface $source);

    /**
     * Get Source data by given sourceId. If you want to create plugin on get method, also you need to create separate
     * plugin on getList method, because entity loading way is different for these methods
     *
     * @param int $sourceId
     * @return \Magento\InventoryApi\Api\Data\SourceInterface
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     */
    public function get($sourceId);

    /**
     * Load Source data collection by given search criteria
     *
     * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
     * @return \Magento\InventoryApi\Api\Data\SourceSearchResultsInterface
     */
    public function getList(SearchCriteriaInterface $searchCriteria = null);
}
/**
 * This is Facade for basic operations with SourceItem
 *
 * The method save is absent, due to different semantic (save multiple)
 * @see SourceItemsSaveInterface
 *
 * There is no get method because SourceItem identifies by compound identifier (sku and source_id),
 * so need to use getList() method
 *
 * Used fully qualified namespaces in annotations for proper work of WebApi request parser
 *
 * @api
 */
interface SourceItemRepositoryInterface
{
    /**
     * Load Source Item data collection by given search criteria
     *
     * We need to have this method for direct work with Source Items because this object contains
     * additional data like as qty, status (for example can de searchable by additional field)
     *
     * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
     * @return \Magento\InventoryApi\Api\Data\SourceItemSearchResultsInterface
     */
    public function getList(SearchCriteriaInterface $searchCriteria);

    /**
     * Delete Source Item data
     *
     * @param SourceItemInterface $sourceItem
     * @return void
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     * @throws \Magento\Framework\Exception\CouldNotDeleteException
     */
    public function delete(SourceItemInterface $sourceItem);
}

SourceItem is a link model which represents the quantity of particular product (SKU) on particular Source (Source Id). The method save() is absent in SourceItemRepositoryInterface, due to different semantic as we provide an efficient API for Multiple Save operation instead. The operation of saving SourceItem would be heavily used at the time of each Import/Export and synchronization with external ERP and PIM systems take place. Thus, we need to save a lot of SourceItems given from the external system, if we would have the default operation of Single Entity save - that will lead to looping through the whole list of SourceItems being updated and performing this Repository::save operation one by one, which would produce performance degradation as result.

To prevent the possible performance degradation during update/synchronization with external systems Magento\InventoryApi\Api\SourceItemsSaveInterface provided.

/**
 * Service method for source items save multiple
 * Performance efficient API, used for stock synchronization
 *
 * Used fully qualified namespaces in annotations for proper work of WebApi request parser
 *
 * @api
 */
interface SourceItemsSaveInterface
{
    /**
     * Save Multiple Source item data
     *
     * @param \Magento\InventoryApi\Api\Data\SourceItemInterface[] $sourceItems
     * @return void
     * @throws \Magento\Framework\Exception\InputException
     * @throws \Magento\Framework\Exception\CouldNotSaveException
     */
    public function execute(array $sourceItems);
}

The current interface accepts an array of SoruceItems to save. Another reason why we don't provide a single save operation in SourceItemRepository is that we want to keep a single point of Customization for our entities. If we provided both single and multiple save operations, 3rd party developer who would like to extend/customize save operation would have to Pluginize both of these methods to achieve the desired goal. But having only Multiple Save service - Magento provides the only place which should be customized. In this way, we decrease amount of efforts needed for System pluginization and the amount of possible customization mistakes accordingly (when in some scenarios Data Interface would not be handled properly).

Another interesting moment you can find in SourceItemRepository is the absence of get() method. There is no get method because SourceItem identifies by the compound identifier (SKU and source_id), because generally speaking it's a link entity. Thus, we recommend to use getList() method or introduce a dedicated "Sugar" interface which will retrieve SourceItem by SKU and Stock_ID

Stock Repository and Domain services for Source to Stock assignment

Magento\InventoryApi\Api\StockRepositoryInterface has a full typical list of methods

interface StockRepositoryInterface
{
    /**
     * Save Stock data
     *
     * @param \Magento\InventoryApi\Api\Data\StockInterface $stock
     * @return int
     * @throws \Magento\Framework\Exception\CouldNotSaveException
     */
    public function save(StockInterface $stock);
    /**
     * Get Stock data by given stockId. If you want to create plugin on get method, also you need to create separate
     * plugin on getList method, because entity loading way is different for these methods
     *
     * @param int $stockId
     * @return \Magento\InventoryApi\Api\Data\StockInterface
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     */
    public function get($stockId);
    /**
     * Find Stocks by given SearchCriteria
     *
     * @param \Magento\Framework\Api\SearchCriteriaInterface|null $searchCriteria
     * @return \Magento\InventoryApi\Api\Data\StockSearchResultsInterface
     */
    public function getList(SearchCriteriaInterface $searchCriteria = null);
    /**
     * Delete the Stock data by stockId. If stock is not found do nothing
     *
     * @param int $stockId
     * @return void
     * @throws \Magento\Framework\Exception\CouldNotDeleteException
     */
    public function deleteById($stockId);
}

But the most interesting thing here is that for now (*it's possibly would be changed in future when we start implementation of Source Selection Algorithm), we decided not to introduce Data Interface for SourceStockLink and Repository for its management entity which represents a relation between physical sources to virtual Stock. So, there is a possibility both in admin interface as well as using Web API to assign/un-assign sources to particular Stock. The decision was made taking into account all current possible user-scenarios how the code of business logic could work with Stock API. For now, we see next use-cases:

  • Retrieve a list of Sources assigned to particular Stock (this operation needed for Admin UI as well as for Source Selection algorithm in future). Precondition: In this case, we have a Stock entity and need to get a list of Sources assigned. If we would have independent SourceStockLink and SourceStockLinkRepository to manage this linkage, for example, same as we have for Product and Category (CategoryLinkManagement and CategoryLinkRepository) We would end up with two service calls to achieve needed result:
  1. Call for SourceStockLinkRepository::getList() specifying SearchCriteria with Filter by StockID, doing so we will get list of SourceStockLinks. But because SourceStockLink is not a business entity, the real value for business logic represent Sources. Thus, we need to make additional call to SourceRepository to retrieve Source objects.
  2. Call to SourceRepository::getList method specifying SearchCriteria with Filter by SourceIDs[] retrieved from the SearchResult in step 1.
  • Assign/Un-assign sources to Stock In this case, if we would have SourceStockLink and its repository, we need to create needed set of SourceStockLink objects and then save them with the help of SourceStockLinkRepository::save method.

As you can see we provide quite a lot of responsibilities for the business logic. And to make pretty similar operation as to retrieve all the Sources assigned to particular Stock we need to make 2 Service Calls preparing input data (building Search Criteria) and handling response of the 1st Service Call in special way (we get Search Result interface which we need to loop through to retrieve just IDs of sources from the returned items).

But one of the main rules for Good API design: don't make your client do anything you can do for them (this reduces the amount of boilerplate code your client will have)

That's why we decided to introduce dedicated domain services (read more about Domain Driven Design) which implement specific Use-Cases, along with this Client of the API no need to make several calls to achieve needed result. It's enough to make just a single API call per each use-case.

/**
 * Assign Sources to Stock
 *
 * @api
 */
interface AssignSourcesToStockInterface
{
    /**
     * Assign Sources to Stock
     *
     * If one of the Sources or Stock with given id don't exist then exception will be thrown
     *
     * @param int[] $sourceIds
     * @param int $stockId
     * @return void
     * @throws \Magento\Framework\Exception\InputException
     * @throws \Magento\Framework\Exception\CouldNotSaveException
     */
    public function execute(array $sourceIds, int $stockId);
}
/**
 * Get assigned Sources for Stock
 *
 * @api
 */
interface GetAssignedSourcesForStockInterface
{
    /**
     * Get Sources assigned to Stock
     *
     * If Stock with given id doesn't exist then return an empty array
     *
     * @param int $stockId
     * @return \Magento\InventoryApi\Api\Data\SourceInterface[]
     * @throws \Magento\Framework\Exception\InputException
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function execute(int $stockId): array;
}
/**
 * Unassign Source from Stock
 *
 * @api
 */
interface UnassignSourceFromStockInterface
{
    /**
     * Unassign source from stock
     *
     * If Source or Stock with given id doesn't exist then do nothing
     *
     * @param int $sourceId
     * @param int $stockId
     * @return void
     * @throws \Magento\Framework\Exception\InputException
     * @throws \Magento\Framework\Exception\CouldNotDeleteException
     */
    public function execute(int $sourceId, int $stockId);
}

Introducing these Domain services we get rid of the boilerplate code in business logic ( a client of the API), because for now Business Logic needs just to execute one Service Call depending on the given circumstances.

API and SPI segregation

On MSI project we decided to segregate API (Application Programming Interfaces) from SPI (Service Provider Interfaces) for better customization and decreasing coupling of components.

  • Repository now could be considered as an API - Interface for usage (calling) in the business logic
  • Separate class-commands to which Repository proxies initial call (like, Get Save GetList Delete) could be considered as SPI - Interfaces that you should extend and implement to customize current behavior

For example if you look at the implementation of Magento\Inventory\Model\StockRepository you would see that Repository constructor accepts a list of Commands for each of the operation it provides as a part of public interface. And just proxies execution to needed command when one called its public method.

class StockRepository implements StockRepositoryInterface
{
    /**
     * @var SaveInterface
     */
    private $commandSave;

    /**
     * @var GetInterface
     */
    private $commandGet;

    /**
     * @var DeleteByIdInterface
     */
    private $commandDeleteById;

    /**
     * @var GetListInterface
     */
    private $commandGetList;

    /**
     * @param SaveInterface $commandSave
     * @param GetInterface $commandGet
     * @param DeleteByIdInterface $commandDeleteById
     * @param GetListInterface $commandGetList
     */
    public function __construct(
        SaveInterface $commandSave,
        GetInterface $commandGet,
        DeleteByIdInterface $commandDeleteById,
        GetListInterface $commandGetList
    ) {
        $this->commandSave = $commandSave;
        $this->commandGet = $commandGet;
        $this->commandDeleteById = $commandDeleteById;
        $this->commandGetList = $commandGetList;
    }

    /**
     * @inheritdoc
     */
    public function save(StockInterface $stock): int
    {
        return $this->commandSave->execute($stock);
    }

    /**
     * @inheritdoc
     */
    public function get(int $stockId): StockInterface
    {
        return $this->commandGet->execute($stockId);
    }

    /**
     * @inheritdoc
     */
    public function deleteById(int $stockId)
    {
        $this->commandDeleteById->execute($stockId);
    }

    /**
     * @inheritdoc
     */
    public function getList(SearchCriteriaInterface $searchCriteria = null): StockSearchResultsInterface
    {
        return $this->commandGetList->execute($searchCriteria);
    }
}

These Commands represent Module's SPI interfaces and stored under namespace Magento\Inventory\Model\Stock\Command\* Please, pay attention that SPIs are stored in module Inventory, but not in the InventoryAPI.

Here you can see SPI interfaces for Stock Magento\Inventory\Model\Stock\Command\*:

/**
 * Save Stock data command (Service Provider Interface - SPI)
 *
 * Separate command interface to which Repository proxies initial Save call, could be considered as SPI - Interfaces
 * that you should extend and implement to customize current behaviour, but NOT expected to be used (called) in the code
 * of business logic directly
 *
 * @see \Magento\InventoryApi\Api\StockRepositoryInterface
 * @api
 */
interface SaveInterface
{
    /**
     * Save Stock data
     *
     * @param StockInterface $stock
     * @return int
     * @throws CouldNotSaveException
     */
    public function execute(StockInterface $stock);
}

/**
 * Get Stock by stockId command (Service Provider Interface - SPI)
 *
 * Separate command interface to which Repository proxies initial Get call, could be considered as SPI - Interfaces
 * that you should extend and implement to customize current behaviour, but NOT expected to be used (called) in the code
 * of business logic directly
 *
 * @see \Magento\InventoryApi\Api\StockRepositoryInterface
 * @api
 */
interface GetInterface
{
    /**
     * Get Stock data by given stockId
     *
     * @param int $stockId
     * @return StockInterface
     * @throws NoSuchEntityException
     */
    public function execute($stockId);
}

/**
 * Delete Stock by stockId command (Service Provider Interface - SPI)
 *
 * Separate command interface to which Repository proxies initial Delete call, could be considered as SPI - Interfaces
 * that you should extend and implement to customize current behaviour, but NOT expected to be used (called) in the code
 * of business logic directly
 *
 * @see \Magento\InventoryApi\Api\StockRepositoryInterface
 * @api
 */
interface DeleteByIdInterface
{
    /**
     * Delete the Stock data by stockId. If stock is not found do nothing
     *
     * @param int $stockId
     * @return void
     * @throws CouldNotDeleteException
     */
    public function execute($stockId);
}

/**
 * Find Stocks by SearchCriteria command (Service Provider Interface - SPI)
 *
 * Separate command interface to which Repository proxies initial GetList call, could be considered as SPI - Interfaces
 * that you should extend and implement to customize current behaviour, but NOT expected to be used (called) in the code
 * of business logic directly
 *
 * @see \Magento\InventoryApi\Api\StockRepositoryInterface
 * @api
 */
interface GetListInterface
{
    /**
     * Find Stocks by given SearchCriteria
     *
     * @param SearchCriteriaInterface|null $searchCriteria
     * @return StockSearchResultsInterface
     */
    public function execute(SearchCriteriaInterface $searchCriteria = null);
}

Commands' Implementation is pretty similar to what we used to see in the Repository For example, here you can see the listing of Save command for Stock

/**
 * @inheritdoc
 */
class Save implements SaveInterface
{
    /**
     * @var StockResourceModel
     */
    private $stockResource;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @param StockResourceModel $stockResource
     * @param LoggerInterface $logger
     */
    public function __construct(
        StockResourceModel $stockResource,
        LoggerInterface $logger
    ) {
        $this->stockResource = $stockResource;
        $this->logger = $logger;
    }

    /**
     * @inheritdoc
     */
    public function execute(StockInterface $stock)
    {
        try {
            $this->stockResource->save($stock);
            return $stock->getStockId();
        } catch (\Exception $e) {
            $this->logger->error($e->getMessage());
            throw new CouldNotSaveException(__('Could not save Stock'), $e);
        }
    }
}

MSI Documentation:

  1. Technical Vision. Catalog Inventory
  2. Installation Guide
  3. List of Inventory APIs and their legacy analogs
  4. MSI Roadmap
  5. Known Issues in Order Lifecycle
  6. MSI User Guide
  7. DevDocs Documentation
  8. User Stories
  9. User Scenarios:
  10. Technical Designs:
  11. Admin UI
  12. MFTF Extension Tests
  13. Weekly MSI Demos
  14. Tutorials
Clone this wiki locally