Add a Magento 2 Service Contract

Magento modules use service contracts, which are a set of PHP interfaces including data interfaces, which preserve data integrity, and service interfaces, which hide business logic details from service requestors such as controllers, web services, and other modules. To add a Magento 2 service contract, first you must create and register a Magento module. This can be seen in action in this post, however before running the setup:upgrade command, proceed with following the below steps.

The first step will involve adding some data into Magento. This will involve creating a database table and populating the table with some data.

The database table is created within the InstallSchema.php‘s install() method. The file resides within the module’s Setup directory.

The example code below shows a example_sample_data table added that contains data_id, data_title, data_description and is_active columns.

// app/code/[Vendor]/[Module]/Setup/InstallSchema.php

<?php
namespace [Vendor]\[Module]\Setup;

use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\DB\Ddl\Table;

class InstallSchema implements InstallSchemaInterface
{

    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        $installer = $setup;
        $installer->startSetup();
        $tableName = $installer->getTable('example_sample_data');

        if (!$installer->tableExists('example_sample_data')) {
            $table = $installer->getConnection()
                ->newTable($tableName)
                ->addColumn(
                    'data_id',
                    Table::TYPE_INTEGER,
                    null,
                    [
                        'identity' => true,
                        'unsigned' => true,
                        'nullable' => false,
                        'primary' => true
                    ],
                    'Data ID'
                )
                ->addColumn(
                    'data_title',
                    Table::TYPE_TEXT,
                    255,
                    ['nullable' => false, 'default' => ''],
                    'Data Title'
                )
                ->addColumn(
                    'data_description',
                    Table::TYPE_TEXT,
                    null,
                    ['nullable' => false, 'default' => ''],
                    'Data Description'
                )
                ->addColumn(
                    'is_active',
                    Table::TYPE_SMALLINT,
                    null,
                    ['unsigned' => true, 'nullable' => false, 'default' => '0'],
                    'Status'
                );
            $installer->getConnection()->createTable($table);
        }
        $installer->endSetup();
    }
}

To add data to the table, create a InstallData.php file also located within the Setup directory

// app/code/[Vendor]/[Module]/Setup/InstallData.php

<?php
namespace [Vendor]\[Module]\Setup;

use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\ModuleContextInterface;

class InstallData implements InstallDataInterface {

    /**
     * Install data
     *
     * @param ModuleDataSetupInterface $setup
     * @param ModuleContextInterface $context
     */
    public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
    {
        if (version_compare($context->getVersion(), '1.0.0', '<')) {
            $data = [
                [
                    'data_title' => 'Hello World!',
                    'data_description' => 'This is the first description.',
                    'is_active' => 1
                ],
                [
                    'data_title' => 'Hello Again!',
                    'data_description' => 'This is the second description.',
                    'is_active' => 1
                ],
                [
                    'data_title' => 'Welcome To The Third Title',
                    'data_description' => 'Here we have a slightly longer description.',
                    'is_active' => 0
                ]
            ];

            foreach ($data as $datum) {
                $setup->getConnection()
                    ->insertForce($setup->getTable('example_sample_data'), $datum);
            }
        }
    }
}

Now if you run the database upgrade command from your Magento root directory:

$ /path/to/php bin/magento setup:upgrade

The table will be added to your database and populated with the above data.

As mentioned, service contracts are a set of PHP interfaced, and by design, Magento suggests these interfaces belong in the module’s Api directory.

The first interface created will actually reside within a Data directory within Api/. This interface will contain the methods you want to expose, usually being able to get and set data within your entity.

As for naming conventions, the interface should be named so it matches the entity added within the module. As this example is adding data_ columns, the interface will be named DataInterface.

// app/code/[Vendor]/[Module]/Api/Data/DataInterface.php

<?php
namespace [Vendor]\[Module]\Api\Data;

interface DataInterface
{
    /**
     * Constants for keys of data array. Identical to the name of the getter in snake case
     */
    const DATA_ID           = 'data_id';
    const DATA_TITLE        = 'data_title';
    const DATA_DESCRIPTION  = 'data_description';
    const IS_ACTIVE         = 'is_active';


    /**
     * Get ID
     *
     * @return int|null
     */
    public function getId();


    /**
     * Set ID
     *
     * @param $id
     * @return DataInterface
     */
    public function setId($id);

    /**
     * Get Data Title
     *
     * @return string
     */
    public function getDataTitle();

    /**
     * Set Data Title
     *
     * @param $title
     * @return mixed
     */
    public function setDataTitle($title);

    /**
     * Get Data Description
     *
     * @return mixed
     */
    public function getDataDescription();


    /**
     * Set Data Description
     *
     * @param $description
     * @return mixed
     */
    public function setDataDescription($description);


    /**
     * Get is active
     *
     * @return bool|int
     */
    public function getIsActive();


    /**
     * Set is active
     *
     * @param $isActive
     * @return DataInterface
     */
    public function setIsActive($isActive);
    
}

Now create a Search Results interface. This interface should implement Magento\Framework\Api\SearchResultsInterface and contain two methods: getItems() and setItems().

// app/code/[Vendor]/[Module]/Api/Data/DataSearchResultsInterface.php

<?php
namespace [Vendor]\[Module]\Api\Data;

use Magento\Framework\Api\SearchResultsInterface;

interface DataSearchResultsInterface extends SearchResultsInterface
{
    /**
     * Get data list.
     *
     * @return \[Vendor]\[Module]\Api\Data\DataInterface[]
     */
    public function getItems();

    /**
     * Set data list.
     *
     * @param \[Vendor]\[Module]\Api\Data\DataInterface[] $items
     * @return $this
     */
    public function setItems(array $items);
}

The nest step is to create the repository interface, which are mainly used as wrappers for the CRUD functionality. Most repositories have the methods getById(), save(), delete() and getList().

Note that the repository interface resides in the Api directory and not Api/Data.

// app/code/[Vendor]/[Module]/Api/DataRepositoryInterface.php

<?php
namespace [Vendor]\[Module]\Api;

use Magento\Framework\Api\SearchCriteriaInterface;
use [Vendor]\[Module]\Api\Data\DataInterface;

interface DataRepositoryInterface
{

    /**
     * @param DataInterface $data
     * @return mixed
     */
    public function save(DataInterface $data);


    /**
     * @param $dataId
     * @return mixed
     */
    public function getById($dataId);

    /**
     * @param SearchCriteriaInterface $searchCriteria
     * @return \[Vendor]\[Module]\Api\Data\DataSearchResultsInterface
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function getList(SearchCriteriaInterface $searchCriteria);

    /**
     * @param DataInterface $data
     * @return mixed
     */
    public function delete(DataInterface $data);

    /**
     * @param $dataId
     * @return mixed
     */
    public function deleteById($dataId);
}

Now that the three repository interfaces have been created, their implementations need to be added. This includes the data models and repository. All of these files will belong in the module’s Model directory.

The model implements [Vendor]\[Module]\Api\Data\DataInterface.

// app/code/[Vendor]/[Module]/Model/Data.php

<?php
namespace [Vendor]\[Module]\Model;

use Magento\Framework\Model\AbstractModel;
use [Vendor]\[Module]\Api\Data\DataInterface;

class Data extends AbstractModel
    implements DataInterface
{

    /**
     * Cache tag
     */
    const CACHE_TAG = 'example_sample_data';

    /**
     * Initialise resource model
     */
    protected function _construct()
    {
        $this->_init('[Vendor]\[Module]\Model\ResourceModel\Data');
    }

    /**
     * Get cache identities
     *
     * @return array
     */
    public function getIdentities()
    {
        return [self::CACHE_TAG . '_' . $this->getId()];
    }

    /**
     * Get title
     *
     * @return string
     */
    public function getDataTitle()
    {
        return $this->getData(DataInterface::DATA_TITLE);
    }

    /**
     * Set title
     *
     * @param $title
     * @return $this
     */
    public function setDataTitle($title)
    {
        return $this->setData(DataInterface::DATA_TITLE, $title);
    }

    /**
     * Get data description
     *
     * @return string
     */
    public function getDataDescription()
    {
        return $this->getData(DataInterface::DATA_TITLE);
    }

    /**
     * Set data description
     *
     * @param $description
     * @return $this
     */
    public function setDataDescription($description)
    {
        return $this->setData(DataInterface::DATA_DESCRIPTION, $description);
    }

    /**
     * Get is active
     *
     * @return bool|int
     */
    public function getIsActive()
    {
        return $this->getData(DataInterface::IS_ACTIVE);
    }

    /**
     * Set is active
     *
     * @param $isActive
     * @return $this
     */
    public function setIsActive($isActive)
    {
        return $this->setData(DataInterface::IS_ACTIVE, $isActive);
    }
}

Within the model’s _construct() method, init() passes in the resource model as a string, so define this next.

// app/code/[Vendor]/[Module]/Model/ResourceModel/Data.php

<?php
namespace [Vendor]\[Module]\Model\ResourceModel;

use Magento\Framework\Model\ResourceModel\Db\Context;
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;

class Data extends AbstractDb
{
    /**
     * Data constructor.
     * @param Context $context
     */
    public function __construct(
        Context $context
    ) {
        parent::__construct($context);
    }

    /**
     * Resource initialisation
     */
    protected function _construct()
    {
        $this->_init('example_sample_data', 'data_id');
    }
}

Now for the collection class.

// app/code/[Vendor]/[Module]/Model/ResourceModel/Data/Collection.php

<?php
namespace [Vendor]\[Module]\Model\ResourceModel\Data;

use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;

class Collection extends AbstractCollection
{
    /**
     * @var string
     */
    protected $_idFieldName = 'data_id';

    /**
     * Collection initialisation
     */
    protected function _construct()
    {
        $this->_init('[Vendor]\[Module]\Model\Data','[Vendor]\[Module]\Model\ResourceModel\Data');
    }
}

Now add the repository. The repository essentially uses the ORM to manipulate the data from the custom entity, such as using the save() and delete() methods.

// app/code/[Vendor]/[Module]/Model/DataRepository.php

<?php
namespace [Vendor]\[Module]\Model;

use Magento\Framework\Api\DataObjectHelper;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\Search\FilterGroup;
use Magento\Framework\Api\SortOrder;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\StateException;
use Magento\Framework\Exception\ValidatorException;
use Magento\Framework\Exception\NoSuchEntityException;
use [Vendor]\[Module]\Api\DataRepositoryInterface;
use [Vendor]\[Module]\Api\Data\DataInterface;
use [Vendor]\[Module]\Api\Data\DataInterfaceFactory;
use [Vendor]\[Module]\Api\Data\DataSearchResultsInterfaceFactory;
use [Vendor]\[Module]\Model\ResourceModel\Data as ResourceData;
use [Vendor]\[Module]\Model\ResourceModel\Data\CollectionFactory as DataCollectionFactory;

class DataRepository implements DataRepositoryInterface
{
    /**
     * @var array
     */
    protected $_instances = [];
    /**
     * @var ResourceData
     */
    protected $_resource;
    /**
     * @var DataCollectionFactory
     */
    protected $_dataCollectionFactory;
    /**
     * @var DataSearchResultsInterfaceFactory
     */
    protected $_searchResultsFactory;
    /**
     * @var DataInterfaceFactory
     */
    protected $_dataInterfaceFactory;
    /**
     * @var DataObjectHelper
     */
    protected $_dataObjectHelper;

    public function __construct(
        ResourceData $resource,
        DataCollectionFactory $dataCollectionFactory,
        DataSearchResultsInterfaceFactory $dataSearchResultsInterfaceFactory,
        DataInterfaceFactory $dataInterfaceFactory,
        DataObjectHelper $dataObjectHelper
    )
    {
        $this->_resource = $resource;
        $this->_dataCollectionFactory = $dataCollectionFactory;
        $this->_searchResultsFactory = $dataSearchResultsInterfaceFactory;
        $this->_dataInterfaceFactory = $dataInterfaceFactory;
        $this->_dataObjectHelper = $dataObjectHelper;
    }

    /**
     * @param DataInterface $data
     * @return DataInterface
     * @throws CouldNotSaveException
     */
    public function save(DataInterface $data)
    {
        try {
            /** @var DataInterface|\Magento\Framework\Model\AbstractModel $data */
            $this->_resource->save($data);
        } catch (\Exception $exception) {
            throw new CouldNotSaveException(__(
                'Could not save the data: %1',
                $exception->getMessage()
            ));
        }
        return $data;
    }

    /**
     * Get data record
     *
     * @param $dataId
     * @return mixed
     * @throws NoSuchEntityException
     */
    public function getById($dataId)
    {
        if (!isset($this->_instances[$dataId])) {
            /** @var \[Vendor]\[Module]\Api\Data\DataInterface|\Magento\Framework\Model\AbstractModel $data */
            $data = $this->_dataInterfaceFactory->create();
            $this->_resource->load($data, $dataId);
            if (!$data->getId()) {
                throw new NoSuchEntityException(__('Requested data doesn\'t exist'));
            }
            $this->_instances[$dataId] = $data;
        }
        return $this->_instances[$dataId];
    }

    /**
     * @param SearchCriteriaInterface $searchCriteria
     * @return \[Vendor]\[Module]\Api\Data\DataSearchResultsInterface
     */
    public function getList(SearchCriteriaInterface $searchCriteria)
    {
        /** @var \[Vendor]\[Module]\Api\Data\DataSearchResultsInterface $searchResults */
        $searchResults = $this->_searchResultsFactory->create();
        $searchResults->setSearchCriteria($searchCriteria);

        /** @var \[Vendor]\[Module]\Model\ResourceModel\Data\Collection $collection */
        $collection = $this->_dataCollectionFactory->create();

        //Add filters from root filter group to the collection
        /** @var FilterGroup $group */
        foreach ($searchCriteria->getFilterGroups() as $group) {
            $this->addFilterGroupToCollection($group, $collection);
        }
        $sortOrders = $searchCriteria->getSortOrders();
        /** @var SortOrder $sortOrder */
        if ($sortOrders) {
            foreach ($searchCriteria->getSortOrders() as $sortOrder) {
                $field = $sortOrder->getField();
                $collection->addOrder(
                    $field,
                    ($sortOrder->getDirection() == SortOrder::SORT_ASC) ? 'ASC' : 'DESC'
                );
            }
        } else {
            $field = 'data_id';
            $collection->addOrder($field, 'ASC');
        }
        $collection->setCurPage($searchCriteria->getCurrentPage());
        $collection->setPageSize($searchCriteria->getPageSize());

        $data = [];
        foreach ($collection as $datum) {
            $dataDataObject = $this->_dataInterfaceFactory->create();
            $this->_dataObjectHelper->populateWithArray($dataDataObject, $datum->getData(), DataInterface::class);
            $data[] = $dataDataObject;
        }
        $searchResults->setTotalCount($collection->getSize());
        return $searchResults->setItems($data);
    }

    /**
     * @param DataInterface $data
     * @return bool
     * @throws CouldNotSaveException
     * @throws StateException
     */
    public function delete(DataInterface $data)
    {
        /** @var \[Vendor]\[Module]\Api\Data\DataInterface|\Magento\Framework\Model\AbstractModel $data */
        $id = $data->getId();
        try {
            unset($this->_instances[$id]);
            $this->_resource->delete($data);
        } catch (ValidatorException $e) {
            throw new CouldNotSaveException(__($e->getMessage()));
        } catch (\Exception $e) {
            throw new StateException(
                __('Unable to remove data %1', $id)
            );
        }
        unset($this->_instances[$id]);
        return true;
    }

    /**
     * @param $dataId
     * @return bool
     */
    public function deleteById($dataId)
    {
        $data = $this->getById($dataId);
        return $this->delete($data);
    }
}

Lastly, add a di.xml file to your module’s etc directory that tells Magento what implementations to use.

// app/code/[Vendor]/[Module]/etc/di.xml

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="[Vendor]\[Module]\Api\DataRepositoryInterface" type="[Vendor]\[Module]\Model\DataRepository" />
    <preference for="[Vendor]\[Module]\Api\Data\DataInterface" type="[Vendor]\[Module]\Model\Data" />
</config>

And that’s it! Now you can use the API repositories for your custom entity rather than having to use the deprecated load() and save() model methods.

Remember to clear the cache, and regenerate the contents of var/generation.

For example, you can manipulate the data in a controller by importing the classes using the use keyword. To find out more about adding controllers in Magento 2, click here.

Some code examples can be seen below within the controller’s execute() method that load an existing entity, and save new data to the database table.

// app/code/[Vendor]/[Module]/Controller/Index/Index.php

<?php
namespace [Vendor]\[Module]\Controller\Index;

use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\Api\DataObjectHelper;
use [Vendor]\[Module]\Api\DataRepositoryInterface;
use [Vendor]\[Module]\Api\Data\DataInterface;
use [Vendor]\[Module]\Api\Data\DataInterfaceFactory;

class Index extends Action
{
    /**
     * @var DataObjectHelper
     */
    protected $_dataObjectHelper;

    /**
     * @var DataRepositoryInterface
     */
    protected $_dataRepository;

    /**
     * @var DataInterfaceFactory
     */
    protected $_dataFactory;

    /**
     * Index constructor.
     * @param Context $context
     * @param DataObjectHelper $dataObjectHelper
     * @param DataRepositoryInterface $dataRepository
     * @param DataInterfaceFactory $dataInterfaceFactory
     */
    public function __construct(
        Context $context,
        DataObjectHelper $dataObjectHelper,
        DataRepositoryInterface $dataRepository,
        DataInterfaceFactory $dataInterfaceFactory
    )
    {
        parent::__construct($context);
        $this->_dataObjectHelper = $dataObjectHelper;
        $this->_dataRepository = $dataRepository;
        $this->_dataFactory = $dataInterfaceFactory;
    }

    public function execute()
    {
        // Load a specific entity with an ID of 1
        $entity = $this->_dataRepository->getById(1);

        // View the data from the entity
        print_r($entity->getData());

        // Save some data
        $model = $this->_dataFactory->create();
        $data = [
            'data_title' => 'A short story',
            'data_description' => 'The quick brown fox jumped over the lazy dog',
            'is_active' => true
        ];

        $this->_dataObjectHelper->populateWithArray($model, $data, DataInterface::class);
        $this->_dataRepository->save($model);
    }
}

Note: This article is based on Magento CE version 2.1.7.