PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

GraphQL Federation – subgrafy, gateway, Apollo Router, integracja z Magento 2

by Henryk Tews / wtorek, 12 września 2023 / Opublikowano w PHP

GraphQL w Magento 2 pisałem o w lutym 2020. Teraz czas na krok dalej – GraphQL Federation, czyli sposób łączenia wielu niezależnych serwisów GraphQL w jeden spójny supergraf. Gdy architektura mikroserwisów spotyka się z headless commerce, Federation staje się kluczowym narzędziem. Pokazuję architekturę, implementację gateway’a i subgrafów w PHP.

Problem bez Federation

Typowy headless e-commerce z mikroserwisami ma osobne serwisy dla produktów, zamówień, klientów i treści. Każdy ma swoje GraphQL API. Frontend musi odpytywać każdy serwis osobno albo wszystko przez jeden monolityczny backend:

// BEZ Federation - frontend odpytuje kilka API
const [products, orders, user] = await Promise.all([
    fetch('/graphql/catalog', { body: productsQuery }),
    fetch('/graphql/orders',  { body: ordersQuery }),
    fetch('/graphql/crm',     { body: userQuery }),
]);
// Problem: osobne requesty, osobne autoryzacje, brak relacji między danymi
// Z Federation - jeden endpoint, dane z wielu serwisów
const result = await fetch('/graphql', {
    body: `{
        user(id: "1") {
            name
            email
            orders {           # dane z serwisu zamówień
                id
                total
                items {
                    product {  # dane z serwisu katalogowego
                        name
                        price
                        image
                    }
                }
            }
        }
    }`
});
// Jeden request, gateway rozkłada go na subgrafy i scala wyniki

Architektura Federation

Federation składa się z trzech elementów:

  • Subgraph – niezależny serwis GraphQL ze swoim schematem, który deklaruje encje które „posiada” i które „rozszerza”
  • Gateway / Router – zbiera schematy wszystkich subgrafów, buduje supergraf i routuje zapytania
  • Supergraph – wynikowy, scalony schemat widoczny dla frontendu

Subgraf – serwis katalogu produktów (PHP)

<?php

// Schemat subgrafu katalogu - products.graphql
// Kluczowe: @key definiuje encję którą ten serwis posiada
$catalogSchema = '
    extend schema
        @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@external"])

    type Product @key(fields: "id") {
        id: ID!
        sku: String!
        name: String!
        price: Float!
        description: String
        image: String
        inStock: Boolean!
    }

    type Query {
        product(id: ID!): Product
        products(ids: [ID!]): [Product]
        searchProducts(query: String!, limit: Int = 20): [Product]
    }
';

// Resolver dla @key - wymagany dla Federation
// Gdy gateway potrzebuje Product po ID, wywołuje ten resolver
$resolvers = [
    'Product' => [
        '__resolveReference' => function (array $reference) use ($productRepository) {
            // $reference zawiera pola z @key - tutaj "id"
            return $productRepository->getById((int) $reference['id']);
        },
    ],
    'Query' => [
        'product' => function ($root, array $args) use ($productRepository) {
            return $productRepository->getById((int) $args['id']);
        },
        'products' => function ($root, array $args) use ($productRepository) {
            return $productRepository->getByIds($args['ids']);
        },
    ],
];

Subgraf – serwis zamówień (PHP)

<?php

// Schemat subgrafu zamówień
// Kluczowe: rozszerza typ Product z innego subgrafu przez @extends i @external
$ordersSchema = '
    extend schema
        @link(url: "https://specs.apollo.dev/federation/v2.0",
              import: ["@key", "@external", "@extends", "@shareable"])

    # Rozszerzamy encję Product zdefiniowaną w katalogu
    # Dodajemy pole salesCount bez wiedzy serwisu katalogowego
    extend type Product @key(fields: "id") {
        id: ID! @external
        salesCount: Int!          # nowe pole dodane przez serwis zamówień
        lastOrderDate: String
    }

    type OrderItem {
        id: ID!
        product: Product!         # relacja do encji z innego subgrafu
        quantity: Int!
        unitPrice: Float!
        rowTotal: Float!
    }

    type Order @key(fields: "id") {
        id: ID!
        incrementId: String!
        status: String!
        grandTotal: Float!
        items: [OrderItem!]!
        createdAt: String!
    }

    type User @key(fields: "id") {
        id: ID!
        orders: [Order!]!
        totalSpent: Float!
    }

    type Query {
        order(id: ID!): Order
        orderByIncrementId(incrementId: String!): Order
    }
';

$ordersResolvers = [
    'Product' => [
        // Resolver dla pól dodanych przez ten subgraf
        '__resolveReference' => function (array $reference) use ($salesRepository) {
            return ['id' => $reference['id']]; // minimalne dane - resztę doda katalog
        },
        'salesCount' => function (array $product) use ($salesRepository) {
            return $salesRepository->getSalesCount($product['id']);
        },
    ],
    'User' => [
        '__resolveReference' => function (array $reference) use ($orderRepository) {
            return ['id' => $reference['id']];
        },
        'orders' => function (array $user) use ($orderRepository) {
            return $orderRepository->getByCustomerId($user['id']);
        },
        'totalSpent' => function (array $user) use ($orderRepository) {
            return $orderRepository->getTotalSpent($user['id']);
        },
    ],
];

Implementacja Gateway w PHP – Apollo Router lub własny

<?php

declare(strict_types=1);

// Uproszczony Gateway PHP - w produkcji użyj Apollo Router lub Hasura
class GraphQLFederationGateway
{
    private array $subgraphClients = [];

    public function __construct(
        private \GuzzleHttp\Client $httpClient
    ) {}

    public function addSubgraph(string $name, string $url): void
    {
        $this->subgraphClients[$name] = $url;
    }

    public function fetchSubgraphSchema(string $url): string
    {
        $response = $this->httpClient->post($url, [
            'json' => [
                'query' => '{ _service { sdl } }',
            ],
        ]);

        $data = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
        return $data['data']['_service']['sdl'];
    }

    public function buildSupergraph(): array
    {
        $schemas = [];

        foreach ($this->subgraphClients as $name => $url) {
            $schemas[$name] = [
                'url' => $url,
                'sdl' => $this->fetchSubgraphSchema($url),
            ];
        }

        return $schemas;
    }

    public function execute(string $query, array $variables = []): array
    {
        // W produkcji: Apollo Query Planner rozdziela zapytanie
        // na podzapytania do odpowiednich subgrafów
        // Tutaj - uproszczona implementacja
        $queryPlan = $this->planQuery($query);
        $results   = [];

        foreach ($queryPlan as $subgraph => $subquery) {
            $results[$subgraph] = $this->executeOnSubgraph(
                $this->subgraphClients[$subgraph],
                $subquery,
                $variables
            );
        }

        return $this->mergeResults($results);
    }

    private function executeOnSubgraph(string $url, string $query, array $variables): array
    {
        $response = $this->httpClient->post($url, [
            'json' => ['query' => $query, 'variables' => $variables],
        ]);

        return json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
    }

    private function planQuery(string $query): array
    {
        // Uproszczenie - w praktyce Apollo Router robi to automatycznie
        return ['catalog' => $query]; // placeholder
    }

    private function mergeResults(array $results): array
    {
        // Scalanie wyników z różnych subgrafów
        $merged = ['data' => []];

        foreach ($results as $result) {
            if (isset($result['data'])) {
                $merged['data'] = array_merge_recursive($merged['data'], $result['data']);
            }
        }

        return $merged;
    }
}

Apollo Router – produkcyjny gateway

# router.yaml - konfiguracja Apollo Router (Go/Rust binary, nie PHP)
supergraph:
  listen: 0.0.0.0:4000

subgraphs:
  catalog:
    routing_url: http://catalog-service:8080/graphql
  orders:
    routing_url: http://orders-service:8081/graphql
  crm:
    routing_url: http://crm-service:8082/graphql

# CORS dla frontendu
cors:
  allow_any_origin: true
  allow_headers: ["Authorization", "Content-Type"]

# Cache odpowiedzi - TTL per type
cache:
  subgraph:
    all:
      ttl: 60s  # domyślnie 60 sekund
# Uruchomienie z Docker Compose
docker run -p 4000:4000 \
    -v $(pwd)/router.yaml:/dist/config/router.yaml \
    ghcr.io/apollographql/router:v1.x

# Compose supergraphu ze schematów subgrafów (Apollo CLI)
npx @apollo/rover supergraph compose --config supergraph.yaml > supergraph.graphql

Integracja z Magento 2

Magento 2.4+ ma wbudowane GraphQL API. Możesz włączyć je jako subgraf w Federation – Magento staje się serwisem katalogu i zamówień, a dodatkowe mikroserwisy (CRM, PIM, treści) dołączają jako osobne subgrafy:

# supergraph.yaml
federation_version: =2.3.2

subgraphs:
  magento_catalog:
    routing_url: https://shop.example.com/graphql
    schema:
      file: ./schemas/magento.graphql  # wyeksportowany schemat Magento

  pimcore_content:
    routing_url: https://cms.example.com/graphql
    schema:
      file: ./schemas/pimcore.graphql

  custom_loyalty:
    routing_url: https://loyalty.example.com/graphql
    schema:
      file: ./schemas/loyalty.graphql

Podsumowanie

GraphQL Federation to architektura która pozwala skalować API poziomo – każdy zespół i serwis ma własny subgraf, a gateway scala je w spójny interfejs dla frontendu. W kontekście headless Magento 2 Federation pozwala dodawać nowe źródła danych (PIM, CRM, Loyalty) bez modyfikacji samego Magento. Apollo Router jako gotowy binary eliminuje potrzebę pisania gateway’a w PHP – twoja rola to zaprojektowanie subgrafów i ich schematów.

About Henryk Tews

Co możesz przeczytać następne

Blackfire – instalacja w DDEV, profilowanie HTTP i CLI, asercje w CI/CD
AI w pracy PHP developera – Copilot, Claude, Ollama, gdzie pomaga a gdzie zawodzi
PimCore – CMS + PIM + DAM, klasy obiektów, Data Hub GraphQL, integracja z Magento
  • 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}