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?
2. Magento's Search Criteria API allows you to:
3. What should a repository return when an entity is not found?
Flashcards
Question
What does the Repository pattern abstract?
Click to reveal answer
Answer
Data access details (database, file system, API) behind a collection-like interface
Question
What is Magento's Search Criteria?
Click to reveal answer
Answer
A programmatic query builder for filters, sorts, and pagination without SQL
Question
What exception when entity not found?
Click to reveal answer
Answer
NoSuchEntityException
Question
Repository = ? in terms of DDD
Click to reveal answer
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