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.
