Skip to content
intermediate Phase 25 · Module Backend

Creating Repositories - Service Contracts

Guide to Magento 2 repositories: RepositoryInterface, SearchResults, SearchCriteria interface, and implementing the service contract pattern

45m
0 problems
Topic Progress 0%

Repository Interface

Repository Pattern

Repositories provide a clean API for data access. They hide database details and expose a contract that can be consumed via API or internal code.

Standard Repository Interface

<?php
namespace Amazon\Prep\Api;

use Amazon\Prep\Api\Data\WarrantyInterface;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SearchResultsInterface;

class WarrantyRepositoryInterface
{
    public function save(WarrantyInterface $warranty): WarrantyInterface;
    public function get(int $id): WarrantyInterface;
    public function delete(WarrantyInterface $warranty): bool;
    public function deleteById(int $id): bool;
    public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface;
}

Data Interface

<?php
namespace Amazon\Prep\Api\Data;

interface WarrantyInterface
{
    const ID = 'warranty_id';
    const NAME = 'name';
    const DURATION = 'duration';
    const STATUS = 'status';
    const CREATED_AT = 'created_at';
    const UPDATED_AT = 'updated_at';

    public function getId(): ?int;
    public function getName(): ?string;
    public function getDuration(): ?int;
    public function getStatus(): ?string;
    public function getCreatedAt(): ?string;
    public function getUpdatedAt(): ?string;

    public function setId(?int $id): WarrantyInterface;
    public function setName(?string $name): WarrantyInterface;
    public function setDuration(?int $duration): WarrantyInterface;
    public function setStatus(?string $status): WarrantyInterface;
    public function setCreatedAt(?string $createdAt): WarrantyInterface;
    public function setUpdatedAt(?string $updatedAt): WarrantyInterface;
}

di.xml Binding

<preference for="Amazon\Prep\Api\WarrantyRepositoryInterface"
            type="Amazon\Prep\Model\WarrantyRepository"/>

Repository Implementation

Complete Repository Class

<?php
namespace Amazon\Prep\Model;

use Amazon\Prep\Api\WarrantyRepositoryInterface;
use Amazon\Prep\Api\Data\WarrantyInterface;
use Amazon\Prep\Model\WarrantyFactory;
use Amazon\Prep\Model\ResourceModel\Warranty as WarrantyResource;
use Amazon\Prep\Model\ResourceModel\Warranty\CollectionFactory;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SearchResultsInterfaceFactory;
use Magento\Framework\Exception\CouldNotDeleteException;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\NoSuchEntityException;

class WarrantyRepository implements WarrantyRepositoryInterface
{
    public function __construct(
        private WarrantyFactory $warrantyFactory,
        private WarrantyResource $resource,
        private CollectionFactory $collectionFactory,
        private SearchResultsInterfaceFactory $searchResultsFactory
    ) {}

    public function save(WarrantyInterface $warranty): WarrantyInterface
    {
        try {
            $this->resource->save($warranty);
        } catch (\Exception $e) {
            throw new CouldNotSaveException(
                __('Could not save warranty: %1', $e->getMessage()),
                $e
            );
        }
        return $warranty;
    }

    public function get(int $id): WarrantyInterface
    {
        $warranty = $this->warrantyFactory->create();
        $this->resource->load($warranty, $id);
        if (!$warranty->getId()) {
            throw new NoSuchEntityException(
                __('Warranty with ID %1 not found', $id)
            );
        }
        return $warranty;
    }

    public function delete(WarrantyInterface $warranty): bool
    {
        try {
            $this->resource->delete($warranty);
        } catch (\Exception $e) {
            throw new CouldNotDeleteException(
                __('Could not delete warranty: %1', $e->getMessage()),
                $e
            );
        }
        return true;
    }

    public function deleteById(int $id): bool
    {
        return $this->delete($this->get($id));
    }

    public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface
    {
        $collection = $this->collectionFactory->create();

        $this->addSearchCriteriaToCollection($searchCriteria, $collection);

        $searchResults = $this->searchResultsFactory->create();
        $searchResults->setSearchCriteria($searchCriteria);
        $searchResults->setItems($collection->getItems());
        $searchResults->setTotalCount($collection->getSize());

        return $searchResults;
    }

    private function addSearchCriteriaToCollection(
        SearchCriteriaInterface $searchCriteria,
        \Amazon\Prep\Model\ResourceModel\Warranty\Collection $collection
    ): void {
        foreach ($searchCriteria->getFilterGroups() as $filterGroup) {
            $conditions = [];
            foreach ($filterGroup->getFilters() as $filter) {
                $conditions[] = [
                    'attribute' => $filter->getField(),
                    $filter->getConditionType() => $filter->getValue(),
                ];
            }
            if (!empty($conditions)) {
                $collection->addFieldToFilter($conditions);
            }
        }

        if ($searchCriteria->getSortOrders()) {
            foreach ($searchCriteria->getSortOrders() as $sortOrder) {
                $collection->addOrder(
                    $sortOrder->getField(),
                    [$sortOrder->getDirection()]
                );
            }
        }

        $collection->setPageSize($searchCriteria->getPageSize() ?: 20);
        $collection->setCurPage($searchCriteria->getCurrentPage() ?: 1);
    }
}

SearchCriteria

SearchCriteria Overview

SearchCriteria is Magento's standard way to build queries. It's used by repositories and the API.

Creating SearchCriteria

use Magento\Framework\Api\SearchCriteriaBuilder;

// Using the builder
$searchCriteria = $this->searchCriteriaBuilder
    ->addFilter('status', 'active')
    ->addFilter('duration', 12, 'gteq')
    ->setSortOrders([
        $this->sortOrderBuilder
            ->setField('created_at')
            ->setDirection('DESC')
            ->create()
    ])
    ->setPageSize(10)
    ->setCurrentPage(1)
    ->create();

$warranties = $this->warrantyRepository->getList($searchCriteria);

Filter Conditions

Condition Meaning
eq Equal
neq Not equal
gteq Greater than or equal
lteq Less than or equal
gt Greater than
lt Less than
like SQL LIKE
in In array
nin Not in array
null Is null
nnull Is not null
to Less than or equal (date)
from Greater than or equal (date)

SearchResults

$searchResults = $this->warrantyRepository->getList($searchCriteria);

// Access results
$items = $searchResults->getItems();      // Array of WarrantyInterface
$totalCount = $searchResults->getTotalCount(); // Total matching records

// Access individual warranty
foreach ($items as $warranty) {
    echo $warranty->getName();
    echo $warranty->getDuration();
}

API Usage

SearchCriteria works with REST API:

GET /rest/V1/warranty?
    searchCriteria[filterGroups][0][filters][0][field]=status&
    searchCriteria[filterGroups][0][filters][0][value]=active&
    searchCriteria[sortOrders][0][field]=created_at&
    searchCriteria[sortOrders][0][direction]=DESC&
    searchCriteria[pageSize]=10&
    searchCriteria[currentPage]=1

Repository Best Practices

Custom Repository Methods

Add domain-specific methods:

class WarrantyRepository implements WarrantyRepositoryInterface
{
    public function getBySku(string $sku): WarrantyInterface
    {
        $collection = $this->collectionFactory->create();
        $collection->addFieldToFilter('sku', $sku)->setPageSize(1);

        $items = $collection->getItems();
        if (empty($items)) {
            throw new NoSuchEntityException(
                __('Warranty with SKU %1 not found', $sku)
            );
        }
        return reset($items);
    }

    public function getActiveWarranties(): array
    {
        $collection = $this->collectionFactory->create();
        $collection->addFieldToFilter('status', 'active');
        return $collection->getItems();
    }
}

Error Handling

use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\CouldNotDeleteException;
use Magento\Framework\Exception\NoSuchEntityException;

// In repository methods
public function save(WarrantyInterface $warranty): WarrantyInterface
{
    try {
        $this->validate($warranty);
        $this->resource->save($warranty);
    } catch (\Exception $e) {
        throw new CouldNotSaveException(
            __('Could not save warranty: %1', $e->getMessage()),
            $e
        );
    }
    return $warranty;
}

Testing Repositories

use PHPUnit\Framework\TestCase;

class WarrantyRepositoryTest extends TestCase
{
    public function testGetReturnsWarranty(): void
    {
        $warranty = $this->createMock(WarrantyInterface::class);
        $warranty->method('getId')->willReturn(1);

        $factory = $this->createMock(WarrantyFactory::class);
        $factory->method('create')->willReturn($warranty);

        $resource = $this->createMock(WarrantyResource::class);
        $resource->method('load')->willReturnCallback(
            fn($model, $id) => $model->setId($id)
        );

        $repo = new WarrantyRepository(
            $factory,
            $resource,
            $this->createMock(CollectionFactory::class),
            $this->createMock(SearchResultsInterfaceFactory::class)
        );

        $result = $repo->get(1);
        $this->assertEquals(1, $result->getId());
    }
}

Quiz

1. What does a repository provide?

Question 1 options

2. What does SearchCriteria control?

Question 2 options

3. What exception should be thrown when a record is not found?

Question 3 options

Flashcards

Question

What does a repository implement?

Answer

RepositoryInterface with save(), get(), delete(), getList() methods

Question

What is SearchCriteria?

Answer

A standard query builder for filtering, sorting, and pagination

Question

What does getList() return?

Answer

SearchResultsInterface with items and total count

Question

What exception for missing records?

Answer

NoSuchEntityException

Question

How do you bind interface to implementation?

Answer

di.xml preference: <preference for="Interface" type="Implementation"/>

Revision Notes

Key Takeaways

  • 1. Repositories implement RepositoryInterface for CRUD operations
  • 2. SearchCriteria provides standard filtering, sorting, pagination
  • 3. SearchResults returns items with total count
  • 4. Use standard exceptions: CouldNotSaveException, NoSuchEntityException
  • 5. Repositories are bound to interfaces via di.xml preferences

Interview Tips

  • Explain the repository pattern and why it exists
  • Know how to build SearchCriteria with filters and sorting
  • Be ready to implement a complete repository class
  • Discuss how repositories enable API access

Cheat Sheet

Repository interface:
  save(WarrantyInterface $warranty)
  get(int $id)
  delete(WarrantyInterface $warranty)
  getList(SearchCriteriaInterface $criteria)

SearchCriteria:
  ->addFilter('field', 'value', 'condition')
  ->setSortOrders([...])
  ->setPageSize(10)
  ->create()

Exceptions:
  CouldNotSaveException, NoSuchEntityException