Bezpośrednie użycie modeli Magento 2 w swoim kodzie to kusząca droga na skróty. Model jest pod ręką, ma metodę której szukasz, działa. Problem pojawia się przy aktualizacji Magento, gdy wewnętrzna implementacja modelu się zmienia. Service Contracts to odpowiedź Magento na ten problem – stabilne, publiczne API oddzielone od implementacji.
Czym są Service Contracts?
Service Contract to zestaw interfejsów PHP definiujących publiczne API modułu. Magento dzieli je na dwa typy:
- Data Interfaces – opisują strukturę danych (np.
ProductInterface,OrderInterface) - Service Interfaces – opisują operacje biznesowe (np.
ProductRepositoryInterface,OrderManagementInterface)
Interfejsy oznaczone adnotacją @api są objęte gwarancją wstecznej kompatybilności przez Magento. Implementacja może się zmienić – kontrakt pozostaje stabilny.
Model vs Repository – konkretny przykład
Ładowanie produktu bez Service Contracts:
<?php
// Sposób "na skróty" – bezpośrednie użycie modelu
class MyClass
{
private \Magento\Catalog\Model\ProductFactory $productFactory;
public function __construct(
\Magento\Catalog\Model\ProductFactory $productFactory
) {
$this->productFactory = $productFactory;
}
public function getProduct(int $id): \Magento\Catalog\Model\Product
{
$product = $this->productFactory->create();
$product->load($id); // metoda load() jest deprecated od Magento 2.1!
return $product;
}
}
Ten sam efekt przez Service Contract:
<?php
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Api\Data\ProductInterface;
class MyClass
{
private ProductRepositoryInterface $productRepository;
public function __construct(
ProductRepositoryInterface $productRepository
) {
$this->productRepository = $productRepository;
}
public function getProduct(int $id): ProductInterface
{
// Rzuca NoSuchEntityException jeśli produkt nie istnieje
return $this->productRepository->getById($id);
}
}
Różnice są istotne: type hinty opierają się na interfejsach (nie konkretnych klasach), metoda getById() jest częścią stabilnego API, a wyjątek NoSuchEntityException jest jawnie zadeklarowany w kontrakcie.
Data Interfaces – praca z danymi przez gettery i settery
ProductInterface definiuje zestaw getterów i setterów dla standardowych atrybutów produktu:
<?php
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
class ProductService
{
public function __construct(
private ProductRepositoryInterface $productRepository,
private SearchCriteriaBuilder $searchCriteriaBuilder
) {}
public function getActiveProducts(): array
{
$searchCriteria = $this->searchCriteriaBuilder
->addFilter('status', 1)
->addFilter('visibility', [2, 3, 4], 'in')
->setPageSize(20)
->create();
$result = $this->productRepository->getList($searchCriteria);
return array_map(
fn(ProductInterface $p) => [
'sku' => $p->getSku(),
'name' => $p->getName(),
'price' => $p->getPrice(),
],
$result->getItems()
);
}
}
Własne Service Contracts w module
Pisząc własny moduł, warto zdefiniować Service Contracts dla jego głównych encji. Struktura katalogów:
Vendor/Module/
Api/
Data/
SubscriptionInterface.php <- Data Interface
SubscriptionRepositoryInterface.php <- Service Interface
Model/
Subscription.php <- implementacja modelu
SubscriptionRepository.php <- implementacja repository
Przykład Data Interface:
<?php
namespace Vendor\Module\Api\Data;
/**
* @api
*/
interface SubscriptionInterface
{
public const FIELD_ID = 'subscription_id';
public const FIELD_EMAIL = 'email';
public const FIELD_CREATED_AT = 'created_at';
public function getId(): ?int;
public function getEmail(): string;
public function setEmail(string $email): self;
public function getCreatedAt(): string;
}
Rejestracja implementacji przez preference w di.xml:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="Vendor\Module\Api\Data\SubscriptionInterface"
type="Vendor\Module\Model\Subscription"/>
<preference for="Vendor\Module\Api\SubscriptionRepositoryInterface"
type="Vendor\Module\Model\SubscriptionRepository"/>
</config>
Zwróć uwagę, że tu preference jest uzasadnione - wiążemy interfejs z implementacją. To właśnie ten przypadek, o którym pisałem we wpisie o Plugin vs Preference.
Podsumowanie
Service Contracts to nie biurokratyczny narzut, ale realna ochrona przed problemami przy aktualizacjach Magento. Kod oparty na interfejsach @api jest bardziej odporny na zmiany wewnętrzne platformy i łatwiejszy do testowania – możesz podmienić implementację repozytorium na mocka bez zmiany kodu korzystającego z kontraktu.
