Data Interfaces and Service Contracts
An Api/Data interface is the contract other modules depend on, and one di.xml preference binds it to your model.
<preference for="Vendor\Faq\Api\Data\QuestionInterface" type="Vendor\Faq\Model\Question"/>Your model is a class inside your module. Other modules should not depend on it, because then you can never change it. What they depend on instead is an interface in Api/Data that describes the entity as getters and setters.
That interface, together with the repository interface in Api, is your module's service contract: the published surface you promise not to break. Mark both @api. Everything else in the module stays yours to rewrite.
What the interface buys you
Magento reflects on these methods. The REST and SOAP Web API layers build their field types from the @return annotations, so a documented interface becomes a typed API payload with no extra work. GraphQL is a separate layer: its types are declared in the module's etc/schema.graphqls and are not generated from these docblocks. The interface also holds the data keys as constants: QuestionInterface::TITLE is 'title', declared once rather than typed as a string in twenty places.
The preference binds it
A preference is one line of di.xml that names the class to use when something asks for a type. Bind the data interface to your model, and the repository interface to your repository. Nothing can be injected for an unbound interface.
In PHP, QuestionInterface::class and the written string 'Vendor\Faq\Api\Data\QuestionInterface' are the same value, and ::class is checked by your IDE. di.xml has only the written form.
Applying it
Editing di.xml changes merged configuration, which is cached: run bin/magento cache:clean config. In production mode, run bin/magento setup:di:compile as well, because preferences are baked into generated code.
Example
<?php
// app/code/Vendor/Faq/Api/Data/QuestionInterface.php
declare(strict_types=1);
namespace Vendor\Faq\Api\Data;
/**
* FAQ question.
*
* @api
*/
interface QuestionInterface
{
public const QUESTION_ID = 'question_id';
public const TITLE = 'title';
public const ANSWER = 'answer';
public const IS_ACTIVE = 'is_active';
/**
* @return int|null
*/
public function getQuestionId(): ?int;
/**
* @return string
*/
public function getTitle(): string;
/**
* @param string $title
* @return $this
*/
public function setTitle(string $title): self;
/**
* @return string|null
*/
public function getAnswer(): ?string;
/**
* @param string|null $answer
* @return $this
*/
public function setAnswer(?string $answer): self;
/**
* @return bool
*/
public function getIsActive(): bool;
}When to use it
- Publish a custom entity so another module can depend on it without touching your model.
- Expose an entity over REST or GraphQL with correctly typed fields.
- Replace your own model implementation later without breaking integrations.
More examples
The model implementing the interface
The model stays the storage-aware class and the interface stays the contract. Every getter casts, so callers get the type the interface promises instead of whatever MySQL returned as a string. Writing the methods out also stops the magic __call getter from answering a misspelled method name with null.
<?php
// app/code/Vendor/Faq/Model/Question.php
declare(strict_types=1);
namespace Vendor\Faq\Model;
use Magento\Framework\Model\AbstractModel;
use Vendor\Faq\Api\Data\QuestionInterface;
use Vendor\Faq\Model\ResourceModel\Question as QuestionResource;
class Question extends AbstractModel implements QuestionInterface
{
protected function _construct()
{
$this->_init(QuestionResource::class);
}
public function getQuestionId(): ?int
{
$id = $this->getData(self::QUESTION_ID);
return $id === null ? null : (int)$id;
}
public function getTitle(): string
{
return (string)$this->getData(self::TITLE);
}
public function setTitle(string $title): self
{
return $this->setData(self::TITLE, $title);
}
public function getAnswer(): ?string
{
$answer = $this->getData(self::ANSWER);
return $answer === null ? null : (string)$answer;
}
public function setAnswer(?string $answer): self
{
return $this->setData(self::ANSWER, $answer);
}
public function getIsActive(): bool
{
return (bool)$this->getData(self::IS_ACTIVE);
}
}The preferences that make the interfaces usable
Three lines are usually the whole binding for a custom entity. The search results interface can point straight at Magento\Framework\Api\SearchResults, which is concrete and already implements getItems, getTotalCount and getSearchCriteria; core modules often declare their own subclass instead, and both work. After editing this file run bin/magento cache:clean config, plus bin/magento setup:di:compile in production mode. Until the preference exists, injecting QuestionInterface or its generated factory fails.
<?xml version="1.0"?>
<!-- app/code/Vendor/Faq/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\Faq\Api\Data\QuestionInterface"
type="Vendor\Faq\Model\Question"/>
<preference for="Vendor\Faq\Api\Data\QuestionSearchResultsInterface"
type="Magento\Framework\Api\SearchResults"/>
<preference for="Vendor\Faq\Api\QuestionRepositoryInterface"
type="Vendor\Faq\Model\QuestionRepository"/>
</config>The search results interface that getList returns
This interface exists only to narrow the item type. SearchResultsInterface declares getItems() as an array of ExtensibleDataInterface, which tells the API nothing useful; redeclaring it with a specific @return array type is what makes the REST response describe questions. Keep the signatures untyped here to match the parent interface.
<?php
// app/code/Vendor/Faq/Api/Data/QuestionSearchResultsInterface.php
declare(strict_types=1);
namespace Vendor\Faq\Api\Data;
use Magento\Framework\Api\SearchResultsInterface;
/**
* Result of a question search.
*
* @api
*/
interface QuestionSearchResultsInterface extends SearchResultsInterface
{
/**
* @return \Vendor\Faq\Api\Data\QuestionInterface[]
*/
public function getItems();
/**
* @param \Vendor\Faq\Api\Data\QuestionInterface[] $items
* @return $this
*/
public function setItems(array $items);
}
Discussion