Repositories

getById, save, delete and getList: the supported way in and out of an entity, and the exceptions a repository must throw.

Syntaxpublic function getById(int $questionId): QuestionInterface; // throws NoSuchEntityException

A repository is the supported entry point to an entity. It takes and returns data interfaces, so the caller needs nothing about your table, your model class or your collection. When another module, a controller, a cron job or the Web API needs your data, it asks the repository.

The methods

getById($id) loads one entity or throws. save($entity) inserts or updates and returns the saved entity, with the new id set. delete($entity) and deleteById($id) remove one. getList(SearchCriteriaInterface $criteria) answers a query and returns a search result.

deleteById($id) is shorthand for delete($this->getById($id)). The two are the same work, and Magento's own repositories write the shorthand as exactly that one line.

Exceptions are part of the contract

A repository reports failure by throwing, never by returning null or false.

ExceptionThrown when
NoSuchEntityExceptionno record has that id
CouldNotSaveExceptionsave failed
CouldNotDeleteExceptiondelete failed
InputExceptionthe argument itself is invalid

All four extend Magento\Framework\Exception\LocalizedException, so the message is translated and safe to show in the admin or in an API response. Catch the driver exception, pass it as the cause, and give a message of your own rather than letting raw SQL text reach the user.

Wiring

Interface in Api, implementation in Model, a preference in di.xml, then bin/magento cache:clean config and, in production mode, bin/magento setup:di:compile.

Example

Example · php
<?php
// app/code/Vendor/Faq/Model/QuestionRepository.php
declare(strict_types=1);

namespace Vendor\Faq\Model;

use Magento\Framework\Exception\CouldNotDeleteException;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\NoSuchEntityException;
use Vendor\Faq\Api\Data\QuestionInterface;
use Vendor\Faq\Api\QuestionRepositoryInterface;
use Vendor\Faq\Model\ResourceModel\Question as QuestionResource;

class QuestionRepository implements QuestionRepositoryInterface
{
    public function __construct(
        private readonly QuestionResource $resource,
        private readonly QuestionFactory $questionFactory
    ) {
    }

    public function getById(int $questionId): QuestionInterface
    {
        $question = $this->questionFactory->create();
        $this->resource->load($question, $questionId);
        if (!$question->getId()) {
            throw NoSuchEntityException::singleField('question_id', $questionId);
        }
        return $question;
    }

    public function save(QuestionInterface $question): QuestionInterface
    {
        try {
            $this->resource->save($question);
        } catch (\Exception $e) {
            throw new CouldNotSaveException(__('The question could not be saved.'), $e);
        }
        return $question;
    }

    public function delete(QuestionInterface $question): bool
    {
        try {
            $this->resource->delete($question);
        } catch (\Exception $e) {
            throw new CouldNotDeleteException(__('The question could not be deleted.'), $e);
        }
        return true;
    }

    public function deleteById(int $questionId): bool
    {
        // Shorthand for the longhand below it: the same two calls.
        return $this->delete($this->getById($questionId));
    }
}

When to use it

  • Load one record by id from a cron job, a controller or an observer.
  • Save an entity from an admin form without writing SQL.
  • Expose create, read, update and delete for a custom entity over REST.

More examples

The repository interface callers depend on

The interface is where the @throws annotations live, because the exceptions are as much a part of the contract as the return types. The same docblocks are what the Web API reads when you expose these methods in webapi.xml, so the fully qualified types in @param and @return matter.

Example · php
<?php
// app/code/Vendor/Faq/Api/QuestionRepositoryInterface.php
declare(strict_types=1);

namespace Vendor\Faq\Api;

use Magento\Framework\Api\SearchCriteriaInterface;
use Vendor\Faq\Api\Data\QuestionInterface;
use Vendor\Faq\Api\Data\QuestionSearchResultsInterface;

/**
 * @api
 */
interface QuestionRepositoryInterface
{
    /**
     * @param int $questionId
     * @return \Vendor\Faq\Api\Data\QuestionInterface
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     */
    public function getById(int $questionId): QuestionInterface;

    /**
     * @param \Vendor\Faq\Api\Data\QuestionInterface $question
     * @return \Vendor\Faq\Api\Data\QuestionInterface
     * @throws \Magento\Framework\Exception\CouldNotSaveException
     */
    public function save(QuestionInterface $question): QuestionInterface;

    /**
     * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
     * @return \Vendor\Faq\Api\Data\QuestionSearchResultsInterface
     */
    public function getList(SearchCriteriaInterface $searchCriteria): QuestionSearchResultsInterface;

    /**
     * @param \Vendor\Faq\Api\Data\QuestionInterface $question
     * @return bool
     * @throws \Magento\Framework\Exception\CouldNotDeleteException
     */
    public function delete(QuestionInterface $question): bool;

    /**
     * @param int $questionId
     * @return bool
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     * @throws \Magento\Framework\Exception\CouldNotDeleteException
     */
    public function deleteById(int $questionId): bool;
}

Validating input before the save

Two different failures, two different exceptions. InputException means the caller sent something wrong and a retry with the same data will fail again; over REST it maps to a 400. CouldNotSaveException means the attempt failed on the way to the database. Validating in the repository rather than in each caller means the admin form, the import and the API all get the same rule.

Example · php
<?php
// app/code/Vendor/Faq/Model/QuestionRepository.php (save, with validation)
declare(strict_types=1);

use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\InputException;
use Vendor\Faq\Api\Data\QuestionInterface;

public function save(QuestionInterface $question): QuestionInterface
{
    if (trim($question->getTitle()) === '') {
        throw InputException::requiredField('title');
    }

    try {
        $this->resource->save($question);
    } catch (\Exception $e) {
        throw new CouldNotSaveException(__('The question could not be saved.'), $e);
    }

    return $question;
}

Calling the repository from a console command

The caller handles the exception rather than checking for null, which is the shape all Magento repository calls take. The command class must also be registered in di.xml under Magento\Framework\Console\CommandList before bin/magento lists it; after that edit run bin/magento cache:clean config.

Example · php
<?php
// app/code/Vendor/Faq/Console/Command/ShowQuestion.php
declare(strict_types=1);

namespace Vendor\Faq\Console\Command;

use Magento\Framework\Exception\NoSuchEntityException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Vendor\Faq\Api\QuestionRepositoryInterface;

class ShowQuestion extends Command
{
    public function __construct(
        private readonly QuestionRepositoryInterface $questionRepository,
        ?string $name = null
    ) {
        parent::__construct($name);
    }

    protected function configure(): void
    {
        $this->setName('vendor:faq:show')->addArgument('id', InputArgument::REQUIRED);
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        try {
            $question = $this->questionRepository->getById((int)$input->getArgument('id'));
        } catch (NoSuchEntityException $e) {
            $output->writeln('<error>' . $e->getMessage() . '</error>');
            return Command::FAILURE;
        }

        $output->writeln($question->getTitle());

        return Command::SUCCESS;
    }
}

// bin/magento vendor:faq:show 4
// How do I return an item?
//
// bin/magento vendor:faq:show 999
// No such entity with question_id = 999

Discussion

  • Be the first to comment on this lesson.