PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Extension Attributes – pełna implementacja z batch loadingiem, REST API, testy

by Henryk Tews / wtorek, 18 listopada 2025 / Opublikowano w Magento 2

Extension Attributes to jeden z najbardziej eleganckich mechanizmów Magento 2 który pozwala dodawać pola do istniejących encji (produkty, zamówienia, klienci) bez modyfikowania ich tabel ani klas. Zamiast tego deklarujesz atrybuty w XML, implementujesz plugin do ładowania i zapisywania danych, i Magento automatycznie dołącza je do odpowiedzi REST API i GraphQL. Pokazuję kompletną implementację krok po kroku.

Extension Attributes vs Custom Attributes vs EAV

Mechanizm Gdzie dane Kiedy używać
EAV Attribute Tabele _varchar/_int/_decimal Atrybuty produktów widoczne w adminie, filtrowanie w layered navigation
Custom Attribute (extension_attributes) Własna tabela (join) Dane techniczne lub B2B, nie wymagają UI w adminie, powiązane z encją 1:1
getData/setData (magic) Pamięć obiektu, nie persystuje Tymczasowe dane w scope jednego requestu

Deklaracja Extension Attribute

<!-- etc/extension_attributes.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">

    <!-- Rozszerz Order o dane dostawy z zewnętrznego systemu -->
    <extension_attributes for="Magento\Sales\Api\Data\OrderInterface">
        <attribute code="vendor_delivery_note" type="string"/>
        <attribute code="vendor_erp_order_id"  type="string"/>
        <attribute code="vendor_warehouse_id"  type="int"/>
    </extension_attributes>

    <!-- Rozszerz Product o dane z PIM -->
    <extension_attributes for="Magento\Catalog\Api\Data\ProductInterface">
        <attribute code="vendor_pim_id"          type="string"/>
        <attribute code="vendor_family_code"     type="string"/>
        <attribute code="vendor_completeness"    type="float"/>
        <!-- Zagnieżdżony obiekt - własny interfejs danych -->
        <attribute code="vendor_stock_info"
                   type="Vendor\Module\Api\Data\StockInfoInterface"/>
    </extension_attributes>

    <!-- Rozszerz Customer -->
    <extension_attributes for="Magento\Customer\Api\Data\CustomerInterface">
        <attribute code="vendor_company_code" type="string"/>
        <attribute code="vendor_credit_limit" type="float"/>
    </extension_attributes>

</config>

Plugin do ładowania i zapisywania danych

<?php

declare(strict_types=1);

namespace Vendor\Module\Plugin;

use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\Data\OrderExtensionFactory;
use Magento\Sales\Api\OrderRepositoryInterface;

class OrderExtensionAttributesPlugin
{
    public function __construct(
        private OrderExtensionFactory $orderExtensionFactory,
        private \Vendor\Module\Model\ResourceModel\OrderExtData $resourceModel
    ) {}

    // Ładuj dane po pobraniu zamówienia
    public function afterGet(
        OrderRepositoryInterface $subject,
        OrderInterface $order
    ): OrderInterface {
        $this->loadExtensionAttributes($order);
        return $order;
    }

    // Ładuj dane dla listy zamówień
    public function afterGetList(
        OrderRepositoryInterface $subject,
        \Magento\Sales\Api\Data\OrderSearchResultInterface $searchResult
    ): \Magento\Sales\Api\Data\OrderSearchResultInterface {
        // Pobierz wszystkie ID zamówień naraz - unikamy N+1
        $orderIds = array_map(
            fn($o) => (int) $o->getEntityId(),
            $searchResult->getItems()
        );

        $allExtData = $this->resourceModel->getByOrderIds($orderIds);

        foreach ($searchResult->getItems() as $order) {
            $data = $allExtData[(int) $order->getEntityId()] ?? [];
            $this->applyExtensionAttributes($order, $data);
        }

        return $searchResult;
    }

    // Zapisz dane razem z zamówieniem
    public function afterSave(
        OrderRepositoryInterface $subject,
        OrderInterface $order
    ): OrderInterface {
        $extAttributes = $order->getExtensionAttributes();

        if ($extAttributes === null) {
            return $order;
        }

        $this->resourceModel->saveForOrder((int) $order->getEntityId(), [
            'delivery_note' => $extAttributes->getVendorDeliveryNote(),
            'erp_order_id'  => $extAttributes->getVendorErpOrderId(),
            'warehouse_id'  => $extAttributes->getVendorWarehouseId(),
        ]);

        return $order;
    }

    private function loadExtensionAttributes(OrderInterface $order): void
    {
        $data = $this->resourceModel->getByOrderId((int) $order->getEntityId());
        $this->applyExtensionAttributes($order, $data);
    }

    private function applyExtensionAttributes(OrderInterface $order, array $data): void
    {
        $extAttributes = $order->getExtensionAttributes()
            ?? $this->orderExtensionFactory->create();

        $extAttributes->setVendorDeliveryNote($data['delivery_note'] ?? null);
        $extAttributes->setVendorErpOrderId($data['erp_order_id'] ?? null);
        $extAttributes->setVendorWarehouseId(
            isset($data['warehouse_id']) ? (int) $data['warehouse_id'] : null
        );

        $order->setExtensionAttributes($extAttributes);
    }
}

Resource Model dla własnej tabeli

<?php

declare(strict_types=1);

namespace Vendor\Module\Model\ResourceModel;

use Magento\Framework\App\ResourceConnection;

class OrderExtData
{
    private const TABLE = 'vendor_order_ext_data';

    public function __construct(
        private ResourceConnection $resourceConnection
    ) {}

    public function getByOrderId(int $orderId): array
    {
        $select = $this->getConnection()->select()
            ->from($this->getTable())
            ->where('order_id = ?', $orderId);

        return $this->getConnection()->fetchRow($select) ?: [];
    }

    // Batch load – unikamy N+1 przy getList()
    public function getByOrderIds(array $orderIds): array
    {
        if (empty($orderIds)) {
            return [];
        }

        $select = $this->getConnection()->select()
            ->from($this->getTable())
            ->where('order_id IN (?)', $orderIds);

        $rows   = $this->getConnection()->fetchAll($select);
        $result = [];

        foreach ($rows as $row) {
            $result[(int) $row['order_id']] = $row;
        }

        return $result;
    }

    public function saveForOrder(int $orderId, array $data): void
    {
        $data['order_id'] = $orderId;

        $this->getConnection()->insertOnDuplicate(
            $this->getTable(),
            $data,
            array_keys(array_diff_key($data, ['order_id' => null]))
        );
    }

    private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface
    {
        return $this->resourceConnection->getConnection();
    }

    private function getTable(): string
    {
        return $this->resourceConnection->getTableName(self::TABLE);
    }
}

Rejestracja pluginu w di.xml

<!-- etc/di.xml -->
<?xml version="1.0"?>
<config>
    <type name="Magento\Sales\Api\OrderRepositoryInterface">
        <plugin name="vendor_module_order_extension_attributes"
                type="Vendor\Module\Plugin\OrderExtensionAttributesPlugin"
                sortOrder="10"/>
    </type>
</config>

Dostęp do Extension Attributes w REST API i GraphQL

# REST API - extension_attributes automatycznie w odpowiedzi
curl -X GET \
    https://shop.example.com/rest/V1/orders/1042 \
    -H 'Authorization: Bearer ADMIN_TOKEN'

# Odpowiedź zawiera:
# {
#   "entity_id": 1042,
#   "status": "processing",
#   "extension_attributes": {
#     "vendor_delivery_note": "Dzwonek nie działa - zostawić u sąsiada",
#     "vendor_erp_order_id": "ERP-20251118-001",
#     "vendor_warehouse_id": 3
#   }
# }

# Zapis przez REST API
curl -X PUT \
    https://shop.example.com/rest/V1/orders/1042 \
    -H 'Authorization: Bearer ADMIN_TOKEN' \
    -H 'Content-Type: application/json' \
    -d '{
        "entity": {
            "entity_id": 1042,
            "extension_attributes": {
                "vendor_erp_order_id": "ERP-20251118-001",
                "vendor_warehouse_id": 3
            }
        }
    }'

Extension Attributes w testach integracyjnych

<?php

use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;

class OrderExtensionAttributesTest extends TestCase
{
    private \Magento\Sales\Api\OrderRepositoryInterface $orderRepository;

    protected function setUp(): void
    {
        $this->orderRepository = Bootstrap::getObjectManager()
            ->get(\Magento\Sales\Api\OrderRepositoryInterface::class);
    }

    public function testExtensionAttributesPersist(): void
    {
        $order = $this->orderRepository->get(1);

        $extAttr = $order->getExtensionAttributes();
        $extAttr->setVendorDeliveryNote('Test note');
        $extAttr->setVendorErpOrderId('ERP-TEST-001');
        $order->setExtensionAttributes($extAttr);

        $this->orderRepository->save($order);

        // Przeładuj świeże z bazy
        $loaded = $this->orderRepository->get(1);

        $this->assertEquals('Test note', $loaded->getExtensionAttributes()->getVendorDeliveryNote());
        $this->assertEquals('ERP-TEST-001', $loaded->getExtensionAttributes()->getVendorErpOrderId());
    }
}

Podsumowanie

Extension Attributes to najczystszy sposób rozszerzania modeli Magento 2 bez ingerencji w rdzeń. Deklaracja w XML, plugin do ładowania/zapisywania, własna tabela z batch loadingiem żeby uniknąć N+1 – to pełny zestaw. Kluczowa zasada: zawsze implementuj batch loading w afterGetList() – bez tego każda lista zamówień czy produktów generuje N dodatkowych zapytań do bazy. REST API i GraphQL automatycznie dostają nowe pola bez żadnej dodatkowej konfiguracji.

About Henryk Tews

Co możesz przeczytać następne

Hyvä zaawansowane wzorce – Alpine.js Store, eventy, lazy loading, reużywalne komponenty
Własny indekser – mview.xml, flat table, full/partial reindeksacja, triggerowane przez cron
MSI – Source, Stock, SSA, własny algorytm, migracja ze starego API
  • 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}