PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Wzorzec Repository – interfejs, implementacja, SearchCriteria, testowanie z mockiem

by Henryk Tews / wtorek, 09 czerwca 2020 / Opublikowano w Wzorce projektowe

Repository to wzorzec, który oddziela logikę biznesową od szczegółów dostępu do danych. Zamiast rozsiewać zapytania SQL lub wywołania ORM po całej aplikacji, masz jedną klasę odpowiedzialną za pobieranie i zapisywanie encji. W Magento 2 Repository jest częścią Service Contracts – to standard, nie opcja. Pokazuję jak zbudować własne Repository od zera i dlaczego warto.

Problem bez Repository

<?php

// Logika biznesowa przemieszana z dostępem do danych
class OrderService
{
    private \PDO $pdo;

    public function getActiveOrders(int $customerId): array
    {
        // Zapytanie SQL bezpośrednio w serwisie - naruszenie SRP
        $stmt = $this->pdo->prepare(
            'SELECT * FROM orders WHERE customer_id = ? AND status = "active"'
        );
        $stmt->execute([$customerId]);
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }

    public function cancelOrder(int $orderId): void
    {
        // Kolejne zapytanie SQL w logice biznesowej
        $this->pdo->prepare('UPDATE orders SET status = "cancelled" WHERE id = ?')
                  ->execute([$orderId]);
    }
}

Testowanie tego kodu wymaga bazy danych. Zmiana źródła danych (np. z MySQL na zewnętrzne API) wymaga modyfikacji logiki biznesowej.

Interfejs Repository – kontrakt

<?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;
}

Implementacja Repository w czystym PHP

<?php

declare(strict_types=1);

namespace Vendor\Module\Model;

use Vendor\Module\Api\OrderRepositoryInterface;
use Vendor\Module\Api\Data\OrderInterface;
use Vendor\Module\Api\Data\OrderInterfaceFactory;
use Vendor\Module\Model\ResourceModel\Order as OrderResource;
use Vendor\Module\Model\ResourceModel\Order\CollectionFactory;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\CouldNotDeleteException;

class OrderRepository implements OrderRepositoryInterface
{
    // Prosty in-memory cache w obrębie requestu
    private array $cache = [];

    public function __construct(
        private OrderResource $resource,
        private OrderInterfaceFactory $orderFactory,
        private CollectionFactory $collectionFactory
    ) {}

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

        /** @var OrderInterface $order */
        $order = $this->orderFactory->create();
        $this->resource->load($order, $id);

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

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

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

        return $collection->getItems();
    }

    public function save(OrderInterface $order): OrderInterface
    {
        try {
            $this->resource->save($order);
            // Odśwież cache po zapisie
            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 z SearchCriteria – lista z filtrowaniem

Pełne Repository Magento 2 obsługuje też getList() z SearchCriteria – to standardowy sposób pobierania kolekcji z filtrowaniem, sortowaniem i paginacją:

<?php

use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SearchResultsInterface;
use Magento\Framework\Api\SearchResultsInterfaceFactory;
use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface;

class OrderRepository implements OrderRepositoryInterface
{
    public function __construct(
        // ... poprzednie zależności
        private SearchResultsInterfaceFactory $searchResultsFactory,
        private CollectionProcessorInterface $collectionProcessor
    ) {}

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

        // CollectionProcessor aplikuje filtry, sortowanie i paginację z SearchCriteria
        $this->collectionProcessor->process($searchCriteria, $collection);

        /** @var SearchResultsInterface $searchResults */
        $searchResults = $this->searchResultsFactory->create();
        $searchResults->setSearchCriteria($searchCriteria);
        $searchResults->setItems($collection->getItems());
        $searchResults->setTotalCount($collection->getSize());

        return $searchResults;
    }
}

Użycie przez SearchCriteriaBuilder:

<?php

// W serwisie lub resolverze GraphQL
$searchCriteria = $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($searchCriteria);

echo 'Znaleziono: ' . $result->getTotalCount() . ' zamówień';
foreach ($result->getItems() as $order) {
    echo $order->getId() . ': ' . $order->getStatus() . PHP_EOL;
}

Testowanie Repository z mockiem

Kluczowa zaleta wzorca – logika biznesowa korzystająca z Repository jest łatwa do testowania bez bazy danych:

<?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);
    }
}

Podsumowanie

Repository to jeden z najważniejszych wzorców przy budowaniu testowalnego kodu w PHP. Oddziela logikę biznesową od szczegółów persistencji, centralizuje dostęp do danych i umożliwia testowanie bez bazy. W Magento 2 jest częścią formalnego kontraktu modułu – implementując własne Repository zgodnie ze standardem platformy, Twój moduł staje się przewidywalny dla innych developerów i narzędzi ekosystemu.

About Henryk Tews

Co możesz przeczytać następne

Adapter i Facade w PHP – wzorce strukturalne
Wzorzec Command – undo, CommandBus, makra, integracja z kolejką Magento 2
Decorator i Proxy w PHP – wzorce strukturalne
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
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 Zawsze aktywne
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.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}