PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Repository pattern – interface, implementation, SearchCriteria, testing with mock

by Henryk Tews / Tuesday, 09 June 2020 / Published in Wzorce projektowe

Repository is a pattern that separates business logic from the details of data access. Instead of scattering SQL queries or ORM calls throughout the application, you have one class responsible for fetching and saving entities. In Magento 2 Repository is part of Service Contracts – a standard, not an option. I show how to build your own Repository from scratch and why it is worth doing.

The problem without Repository

<?php

// Business logic mixed with data access
class OrderService
{
    public function getActiveOrders(int $customerId): array
    {
        // SQL directly in the service - violates SRP
        $stmt = $this->pdo->prepare(
            'SELECT * FROM orders WHERE customer_id = ? AND status = "active"'
        );
        $stmt->execute([$customerId]);
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }
}

Testing this code requires a real database. Changing the data source requires modifying business logic.

Repository Interface – the contract

<?php

declare(strict_types=1);

namespace Vendor\Module\Api;

use Vendor\Module\Api\Data\OrderInterface;

interface OrderRepositoryInterface
{
    /** @throws \Magento\Framework\Exception\NoSuchEntityException */
    public function getById(int $id): OrderInterface;

    /** @return OrderInterface[] */
    public function getActiveByCustomer(int $customerId): array;

    public function save(OrderInterface $order): OrderInterface;
    public function delete(OrderInterface $order): bool;
    public function deleteById(int $id): bool;
}

Repository implementation

<?php

declare(strict_types=1);

namespace Vendor\Module\Model;

use Vendor\Module\Api\OrderRepositoryInterface;
use Vendor\Module\Api\Data\OrderInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\CouldNotDeleteException;

class OrderRepository implements OrderRepositoryInterface
{
    private array $cache = [];

    public function __construct(
        private \Vendor\Module\Model\ResourceModel\Order $resource,
        private \Vendor\Module\Api\Data\OrderInterfaceFactory $orderFactory,
        private \Vendor\Module\Model\ResourceModel\Order\CollectionFactory $collectionFactory
    ) {}

    public function getById(int $id): OrderInterface
    {
        if (isset($this->cache[$id])) return $this->cache[$id];

        $order = $this->orderFactory->create();
        $this->resource->load($order, $id);

        if (!$order->getId()) {
            throw new NoSuchEntityException(__('Order with ID "%1" does not exist.', $id));
        }

        return $this->cache[$id] = $order;
    }

    public function getActiveByCustomer(int $customerId): array
    {
        return $this->collectionFactory->create()
            ->addFieldToFilter('customer_id', $customerId)
            ->addFieldToFilter('status', 'active')
            ->setOrder('created_at', 'DESC')
            ->getItems();
    }

    public function save(OrderInterface $order): OrderInterface
    {
        try {
            $this->resource->save($order);
            if ($order->getId()) $this->cache[$order->getId()] = $order;
        } catch (\Exception $e) {
            throw new CouldNotSaveException(__('Could not save order: %1', $e->getMessage()), $e);
        }
        return $order;
    }

    public function delete(OrderInterface $order): bool
    {
        try {
            $id = $order->getId();
            $this->resource->delete($order);
            unset($this->cache[$id]);
        } catch (\Exception $e) {
            throw new CouldNotDeleteException(__('Could not delete order: %1', $e->getMessage()), $e);
        }
        return true;
    }

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

Repository with SearchCriteria – paginated filtered lists

<?php

use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SearchResultsInterface;

class OrderRepository implements OrderRepositoryInterface
{
    public function __construct(
        // ... previous dependencies
        private \Magento\Framework\Api\SearchResultsInterfaceFactory $searchResultsFactory,
        private \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface $collectionProcessor
    ) {}

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

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

        return $searchResults;
    }
}

// Usage via SearchCriteriaBuilder
$sc = $this->searchCriteriaBuilder
    ->addFilter('status', 'active')
    ->addFilter('created_at', '2020-01-01', 'gt')
    ->addSortOrder($this->sortOrderBuilder->setField('created_at')->setDescendingDirection()->create())
    ->setPageSize(20)
    ->setCurrentPage(1)
    ->create();

$result = $this->orderRepository->getList($sc);
echo 'Found: ' . $result->getTotalCount() . ' orders';

Testing Repository with a mock

<?php

use PHPUnit\Framework\TestCase;

class OrderServiceTest extends TestCase
{
    public function testCancelOrderChangesStatus(): void
    {
        $order = $this->createMock(OrderInterface::class);
        $order->expects($this->once())->method('setStatus')->with('cancelled');

        $repository = $this->createMock(OrderRepositoryInterface::class);
        $repository->method('getById')->with(42)->willReturn($order);
        $repository->expects($this->once())->method('save')->with($order);

        $service = new OrderService($repository);
        $service->cancelOrder(42);
    }
}

Summary

Repository is one of the most important patterns for building testable PHP code. It separates business logic from persistence details, centralises data access, and enables testing without a database. In Magento 2 it is part of the module’s formal contract – implementing your own Repository according to the platform standard makes your module predictable for other developers and ecosystem tooling.

About Henryk Tews

What you can read next

Interpreter pattern – own discount rule grammar, parser, expression tree
Flyweight pattern – object sharing, instance cache, Magento 2
Memento pattern – undo/redo, price history, DB persistence as audit log

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}