Skip to content
intermediate Phase 9 · DI & Patterns

Repository Pattern

Repository pattern for data access abstraction, CRUD operations, search criteria, and Magento's Repository implementations

45m
0 problems
Topic Progress 0%

What is the Repository Pattern?

Purpose

The Repository pattern mediates between domain and data mapping layers, acting an in-memory collection of domain objects. It abstracts the data store.

Without Repository: Direct Resource Model Access

// Business logic mixed with data access
namespace Vendor\Catalog\Model;

class CategoryService
{
    public function __construct(
        private \Magento\Catalog\Model\ResourceModel\Category $resource,
        private \Magento\Catalog\Model\ResourceModel\Category\Collection $collection
    ) {}

    public function findByName(string $name): ?array
    {
        $collection = $this->collection->addFieldToFilter('name', $name);
        return $collection->getFirstItem()->getData();
    }
}

Problems: Tied to MySQL schema, hard to test, hard to swap data stores.

With Repository Pattern

// Interface: data access contract
namespace Vendor\Catalog\Api;

interface CategoryRepositoryInterface
{
    public function get(int $categoryId): CategoryInterface;
    public function save(CategoryInterface $category): CategoryInterface;
    public function delete(CategoryInterface $category): bool;
    public function deleteById(int $categoryId): bool;
    public function getList(
        \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
    ): \Magento\Framework\Api\SearchResultsInterface;
}

// Implementation: details of data access
namespace Vendor\Catalog\Model\Repository;

class CategoryRepository implements \Vendor\Catalog\Api\CategoryRepositoryInterface
{
    public function __construct(
        private \Magento\Catalog\Model\ResourceModel\Category $resource,
        private \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $collectionFactory,
        private \Magento\Catalog\Api\Data\CategoryInterfaceFactory $objectFactory
    ) {}

    public function get(int $categoryId): \Magento\Catalog\Api\Data\CategoryInterface
    {
        $category = $this->objectFactory->create();
        $this->resource->load($category, $categoryId);

        if (!$category->getId()) {
            throw new \Magento\Framework\Exception\NoSuchEntityException(
                __("Category with id {$categoryId} does not exist")
            );
        }

        return $category;
    }

    public function save(\Magento\Catalog\Api\Data\CategoryInterface $category)
    {
        $this->resource->save($category);
        return $category;
    }
}

Callers depend on the interface, not the MySQL implementation.

Search Criteria API

Magento's Search Criteria

Magento provides a flexible search criteria API for complex queries without writing SQL:

namespace Vendor\Report\Model;

class ActiveProductFinder
{
    public function __construct(
        private \Magento\Catalog\Api\ProductRepositoryInterface $productRepo
    ) {}

    public function findExpensiveActiveProducts(float $minPrice): array
    {
        $searchCriteria = $this->searchCriteriaBuilder
            ->addFilter('status', 1) // Active status
            ->addFilter('price', $minPrice, 'gteq') // Price >= minPrice
            ->addSortOrder('price', 'desc')
            ->setPageSize(20)
            ->create();

        $result = $this->productRepo->getList($searchCriteria);
        return $result->getItems();
    }
}

Building Complex Queries

// Combine multiple filters
$searchCriteria = $this->searchCriteriaBuilder
    ->addFilter('category_id', [10, 20, 30], 'in')
    ->addFilter('price', 100, 'gteq')
    ->addFilter('price', 500, 'lteq')
    ->addFilter('visibility', [2, 4], 'in') // Catalog + Search
    ->addSortOrder('name', 'asc')
    ->setCurrentPage(1)
    ->setPageSize(50)
    ->create();

// Date range filter
$searchCriteria = $this->searchCriteriaBuilder
    ->addFilter('created_at', '2024-01-01', 'gteq')
    ->addFilter('created_at', '2024-12-31', 'lteq')
    ->create();

// Or condition (complex filter)
$filterGroup = $this->filterGroupBuilder
    ->addFilter($this->filterBuilder
        ->setField('name')
        ->setValue('%phone%')
        ->setConditionType('like')
        ->create()
    )
    ->addFilter($this->filterBuilder
        ->setField('sku')
        ->setValue('%phone%')
        ->setConditionType('like')
        ->create()
    )
    ->create();

$searchCriteria = $this->searchCriteriaBuilder
    ->setFilterGroups([$filterGroup])
    ->create();

Custom Repository Implementation

Building a Custom Repository

// 1. Define the data interface
namespace Vendor\Review\Api\Data;

interface ReviewInterface
{
    public function getId(): ?int;
    public function getProductId(): int;
    public function getRating(): int;
    public function getComment(): string;
    public function getCreatedAt(): string;

    public function setId(?int $id): self;
    public function setProductId(int $productId): self;
    public function setRating(int $rating): self;
    public function setComment(string $comment): self;
}

// 2. Define the repository interface
namespace Vendor\Review\Api;

interface ReviewRepositoryInterface
{
    public function getById(int $id): \Vendor\Review\Api\Data\ReviewInterface;
    public function save(\Vendor\Review\Api\Data\ReviewInterface $review): \Vendor\Review\Api\Data\ReviewInterface;
    public function delete(\Vendor\Review\Api\Data\ReviewInterface $review): bool;
    public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria);
}

// 3. Implement
namespace Vendor\Review\Model\Repository;

class ReviewRepository implements \Vendor\Review\Api\ReviewRepositoryInterface
{
    public function __construct(
        private \Vendor\Review\Model\ResourceModel\Review $resource,
        private \Vendor\Review\Model\ResourceModel\Review\CollectionFactory $collectionFactory,
        private \Vendor\Review\Api\Data\ReviewInterfaceFactory $reviewFactory
    ) {}

    public function getById(int $id): \Vendor\Review\Api\Data\ReviewInterface
    {
        $review = $this->reviewFactory->create();
        $this->resource->load($review, $id);

        if (!$review->getId()) {
            throw new \Magento\Framework\Exception\NoSuchEntityException(
                __("Review with id {$id} not found")
            );
        }

        return $review;
    }

    public function save(\Vendor\Review\Api\Data\ReviewInterface $review)
    {
        try {
            $this->resource->save($review);
        } catch (\Exception $e) {
            throw new \Magento\Framework\Exception\CouldNotSaveException(
                __("Could not save review: {$e->getMessage()}")
            );
        }
        return $review;
    }

    public function delete(\Vendor\Review\Api\Data\ReviewInterface $review)
    {
        try {
            $this->resource->delete($review);
        } catch (\Exception $e) {
            throw new \Magento\Framework\Exception\CouldNotDeleteException(
                __("Could not delete review: {$e->getMessage()}")
            );
        }
        return true;
    }

    public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria)
    {
        $collection = $this->collectionFactory->create();
        // Apply search criteria to collection
        $this->searchCriteriaApplier->apply($searchCriteria, $collection);

        $items = $collection->getItems();
        return $this->searchResultFactory->create()
            ->setItems($items)
            ->setTotalCount($collection->getSize())
            ->setSearchCriteria($searchCriteria);
    }
}

Register in di.xml

<config>
    <preference for="Vendor\Review\Api\ReviewRepositoryInterface"
                type="Vendor\Review\Model\Repository\ReviewRepository"/>
</config>

Quiz

1. What is the primary purpose of the Repository pattern?

Question 1 options

2. Magento's Search Criteria API allows you to:

Question 2 options

3. What should a repository return when an entity is not found?

Question 3 options

Flashcards

Question

What does the Repository pattern abstract?

Answer

Data access details (database, file system, API) behind a collection-like interface

Question

What is Magento's Search Criteria?

Answer

A programmatic query builder for filters, sorts, and pagination without SQL

Question

What exception when entity not found?

Answer

NoSuchEntityException

Question

Repository = ? in terms of DDD

Answer

In-memory collection of domain objects

Revision Notes

Key Takeaways

  • 1. Repository abstracts data access behind a collection-like interface
  • 2. Magento repositories follow: interface + resource model + collection + factory
  • 3. Search Criteria API enables complex queries without SQL
  • 4. Repositories throw NoSuchEntityException for missing entities
  • 5. Register repositories via di.xml preferences

Interview Tips

  • Explain the Repository vs Data Mapper distinction
  • Give an example using Search Criteria with multiple filters
  • Discuss when repositories add unnecessary abstraction

Cheat Sheet

Repository Pattern:
  Interface → Api\XxxRepositoryInterface
  Implementation → Model\Repository\XxxRepository
  Uses → ResourceModel + Collection + DataFactory

Search Criteria:
  addFilter(field, value, condition)
  addSortOrder(field, direction)
  setPageSize(n)
  setCurrentPage(n)

Exceptions:
  get() → NoSuchEntityException
  save() → CouldNotSaveException
  delete() → CouldNotDeleteException