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.
