API Platform: Verschachtelte Ressourcen und Subresources modellieren
AI generated
SF
{ }
Symfony · API Platform · Subresources · PHP 8.4
Verschachtelte Ressourcen und Subresources in API Platform
wann /orders/1/items wirklich die richtige URL ist

Verschachtelte Ressourcen wie Bestellpositionen unter einer Bestellung wirken auf den ersten Blick intuitiv, führen aber schnell zu unübersichtlichen URL Strukturen und doppelter Autorisierungslogik, wenn sie unüberlegt eingesetzt werden. Dieser Artikel zeigt, wie man Subresources in API Platform sauber über uriTemplate modelliert und wann eine eigenständige Ressource die bessere Wahl ist.

17 Min. Lesezeit uriTemplate · IRI Referenz · Eingebettete Relation API Platform 4 · Symfony 7 · PHP 8.4

1. Warum verschachtelte Ressourcen kein Selbstläufer sind

Sobald eine Domäne aus mehreren zusammenhängenden Entities besteht, etwa Bestellungen und ihre Positionen, liegt die Idee nahe, diese Beziehung auch in der URL abzubilden: /orders/1/items statt einer flachen Liste unter /order-items mit Filterparameter. Solche verschachtelten Ressourcen, in API Platform als Subresources bezeichnet, kommunizieren die fachliche Beziehung direkt in der URL Struktur und sind für viele Konsumenten intuitiv lesbar.

Der Reiz verschachtelter Ressourcen hat aber Grenzen. Jede zusätzliche Verschachtelungsebene erhöht die Komplexität der Routing Konfiguration, der Autorisierungslogik und der URL Struktur insgesamt. Eine URL wie /customers/1/orders/2/items/3/refunds ist zwar technisch möglich, aber weder für Menschen noch für generische API Clients angenehm zu handhaben, und häufig ein Zeichen dafür, dass die Ressourcenmodellierung neu überdacht werden sollte.

API Platform bietet mit dem uriTemplate Argument ein flexibles Werkzeug für verschachtelte Ressourcen, das aber bewusst und nicht reflexartig für jede Eltern Kind Beziehung eingesetzt werden sollte. Die Entscheidung, ob eine Beziehung als Subresource, als eingebettete Relation im Response Body oder als komplett eigenständige Ressource abgebildet wird, ist eine der wichtigsten Design Entscheidungen beim Aufbau einer API.

2. Subresources über uriTemplate definieren

API Platform bildet Subresources nicht über ein eigenes Attribut ab, sondern über das reguläre uriTemplate Argument einer GetCollection Operation, kombiniert mit einem Platzhalter für die ID der Elternressource. Intern übersetzt API Platform diesen Platzhalter automatisch in einen Doctrine Filter, der nur die Kinder der referenzierten Elternressource liefert, ganz ohne manuell geschriebenen Query Code.

Diese verschachtelten Ressourcen erscheinen zusätzlich zur regulären, flachen Collection Route, sofern beide konfiguriert sind. Ein Client kann dann sowohl /order-items?order=1 als auch /orders/1/items nutzen, wobei Letzteres vor allem für Frontend Anwendungen praktisch ist, die ohnehin bereits im Kontext einer konkreten Bestellung navigieren.


<?php

declare(strict_types=1);

namespace App\ApiResource;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use App\Entity\Order;

/**
 * Order item resource, exposed both as a flat collection
 * and as a nested subresource under its parent order.
 */
#[ApiResource(
    operations: [
        new GetCollection(),
        new GetCollection(
            uriTemplate: '/orders/{orderId}/items',
            uriVariables: [
                'orderId' => new \ApiPlatform\Metadata\Link(
                    fromClass: Order::class,
                    toProperty: 'order',
                ),
            ],
        ),
        new Get(),
    ],
)]
final class OrderItem
{
    public int $id;
    public string $productName;
    public int $quantity;
}

3. IRI Referenz gegen eingebettete Relation

Unabhängig von Subresources muss jede Relation zwischen zwei Ressourcen entscheiden, wie sie im JSON Response dargestellt wird. Der Standardfall in API Platform ist die IRI Referenz, eine URL als String, über die der Client bei Bedarf die verlinkte Ressource separat nachladen kann. Das hält jede einzelne Antwort klein und performant, erfordert aber einen zusätzlichen Request, wenn der Client die verlinkten Daten sofort braucht.

Über Serialization Groups lässt sich eine Relation stattdessen vollständig einbetten, sodass die verschachtelten Daten direkt im ersten Response mitgeliefert werden. Diese eingebetteten Relationen sparen zusätzliche Requests, vergrößern aber jede Antwort und können bei tief verschachtelten Objektgraphen schnell zu unübersichtlichen, schwer cachebaren JSON Strukturen führen, wenn sie unkontrolliert für jede Relation aktiviert werden.


<?php

declare(strict_types=1);

namespace App\ApiResource;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use Symfony\Component\Serializer\Attribute\Groups;

/**
 * Order resource with items embedded directly in the response,
 * avoiding an extra request for the client.
 */
#[ApiResource(
    operations: [
        new GetCollection(normalizationContext: ['groups' => ['order:read']]),
    ],
)]
final class Order
{
    #[Groups(['order:read'])]
    public int $id;

    // Embedded relation: full item data instead of just an IRI
    #[Groups(['order:read'])]
    public array $items = [];
}

4. Wie tief sollte Verschachtelung überhaupt gehen

Eine praktische Faustregel: Verschachtelte Ressourcen über mehr als eine Ebene sind selten die richtige Wahl. Statt /customers/1/orders/2/items anzubieten, bewährt sich meist ein flacherer Zugriff, etwa /orders/2/items in Kombination mit einem Filter Parameter auf der Kundenebene, falls dieser überhaupt gebraucht wird. Jede zusätzliche Ebene in der URL bedeutet eine zusätzliche Link Definition, einen zusätzlichen Doctrine Join und eine zusätzliche potenzielle Fehlerquelle bei nicht existierenden Zwischenressourcen.

Ein zweiter Grund gegen tiefe Verschachtelung ist die Bookmark und Cache Fähigkeit von URLs. Eine URL wie /orders/2/items/5 lässt sich bookmarken und in einer zweiten Anwendung direkt referenzieren, ohne den vollständigen Elternpfad kennen zu müssen. Sobald mehrere Elternebenen in die URL codiert sind, wird jede Referenz auf eine tiefer liegende Ressource unnötig lang und an die Existenz aller Zwischenressourcen gekoppelt.

5. Autorisierung entlang der Elternressource vererben

Ein oft übersehener Aspekt bei verschachtelten Ressourcen ist die Autorisierung. Wenn /orders/1/items abgefragt wird, muss geprüft werden, ob der aktuelle Nutzer überhaupt Zugriff auf Bestellung 1 hat, bevor die zugehörigen Positionen zurückgegeben werden. Ohne diese Prüfung könnte ein Nutzer über die Subresource Route Daten einsehen, die über die reguläre Route korrekt geschützt wären, ein klassisches Beispiel für eine Broken Object Level Authorization Lücke.

In API Platform lässt sich das über das security Argument der Subresource Operation lösen, das zusätzlich zur Prüfung auf der Kind Ressource auch die Elternressource über die injizierte URI Variable referenzieren kann. Der Security Ausdruck prüft dann explizit, ob der Nutzer Zugriff auf die referenzierte Elternbestellung hat, und lehnt den Zugriff auf die Subresource andernfalls konsequent ab.

6. Normalization Groups für unterschiedliche Verschachtelungstiefen

Je nachdem, ob eine Ressource als Top Level Endpunkt oder als eingebettete Relation einer anderen Ressource ausgegeben wird, sind oft unterschiedliche Felder relevant. Ein Kunde direkt über /customers/1 abgerufen braucht vollständige Kontaktdaten, während dieselbe Kundenreferenz eingebettet in einer Bestellung meist nur Name und ID benötigt. Normalization Groups lösen dieses Problem, indem verschiedene Serialisierungskontexte für denselben Entity Typ definiert werden.

Diese Kontext abhängige Serialisierung verhindert, dass jede eingebettete Relation automatisch alle Felder der Zielressource mitliefert, was sowohl die Antwortgröße reduziert als auch versehentliches Daten Leaking vermeidet, etwa wenn ein sensibles Feld nur im Top Level Kontext, nicht aber in eingebetteten Relationen erscheinen soll.

7. Wann eine eigenständige Ressource die bessere Wahl ist

Nicht jede Beziehung rechtfertigt eine Subresource. Sobald ein verschachteltes Objekt eigene Filter, eigene Paginierung oder eigene Schreiboperationen jenseits des Elternkontexts braucht, spricht das für eine eigenständige, flache Ressource statt einer echten Subresource. Ein Beispiel: Bestellpositionen, die auch unabhängig von einer konkreten Bestellung durchsucht werden müssen, etwa für eine unternehmensweite Umsatzauswertung nach Produkt, sollten primär als eigenständige /order-items Ressource mit optionalem Filter existieren, die verschachtelte Route bleibt dann ein bequemer Zusatzpfad.

Ein weiteres Signal für eine eigenständige Ressource: Wenn ein verschachteltes Objekt auch ohne seinen aktuellen Elternkontext einen sinnvollen, eigenständigen Lebenszyklus hat, etwa eine Rechnung, die auch nach Löschung der ursprünglichen Bestellung archiviert bleiben muss. Solche Objekte gehören konzeptionell nicht dauerhaft unter die Elternressource, auch wenn sie zum Zeitpunkt der Erstellung aus ihr hervorgehen.

8. Performance bei verschachtelten Collections

Verschachtelte Collections können bei unbedachter Implementierung zu denselben N Plus 1 Problemen führen wie normale Relationen: Wird für jede Bestellung in einer äußeren Liste separat die Subresource der Positionen abgefragt, entstehen unnötig viele Datenbankabfragen. In der Praxis betrifft das vor allem Frontend Anwendungen, die zunächst die Bestellliste laden und danach für jede sichtbare Bestellung eine zusätzliche Subresource Anfrage auslösen, statt die Positionen direkt eingebettet mitzuliefern.

Die Lösung liegt in einer bewussten Entscheidung zwischen eingebetteten Relationen für den Listen Anwendungsfall und separaten Subresource Requests nur dann, wenn tatsächlich Detaildaten einer einzelnen Bestellung benötigt werden. Diese Entscheidung sollte anhand des tatsächlichen Nutzungsmusters des Frontends getroffen werden, nicht anhand einer pauschalen Regel für alle Relationen im Projekt.

9. Subresource gegen eigenständige Ressource im Vergleich

Die folgende Tabelle fasst zusammen, wann eine Subresource sinnvoll ist und wann eine eigenständige, flache Ressource vorzuziehen ist.

Kriterium Subresource Eigenständige Ressource Empfehlung
Existiert nur im Elternkontext Passend Unnötiger Umweg Subresource für strikt abhängige Kinder
Eigene Filter und Paginierung nötig Eingeschränkt möglich Voll flexibel Eigenständige Ressource bei komplexer Suche
Bookmark und direkte Referenz Langer, gekoppelter Pfad Kurze, stabile URL Flache Ressource für Direktzugriff
Autorisierung Muss Elternressource explizit prüfen Eigener, klar abgegrenzter Voter Subresource Security sorgfältig konfigurieren
Lebenszyklus unabhängig vom Elternteil Konzeptionell unpassend Korrekte Abbildung Eigenständige Ressource bei eigenem Lebenszyklus

In der Praxis kombinieren gut modellierte API Platform Projekte beide Ansätze bewusst: echte Subresources für Objekte, die strikt an ihren Elternkontext gebunden sind, und eigenständige, flache Ressourcen überall dort, wo eigene Filter, eigener Lebenszyklus oder direkte Referenzierbarkeit gefragt sind. Diese Entscheidung pro Relation zu treffen, statt eine pauschale Regel für das gesamte Projekt zu verwenden, ist der entscheidende Unterschied zwischen einer wartbaren und einer verschachtelt zugewucherten API Struktur.

Mironsoft

Symfony und API Platform Architektur für anspruchsvolle APIs

Eine saubere Ressourcenstruktur für euer API Platform Projekt?

Wir modellieren Subresources und Relationen so, dass URLs stabil und referenzierbar bleiben, Autorisierung entlang der Elternressource korrekt greift und N Plus 1 Probleme bei verschachtelten Collections gar nicht erst entstehen.

Ressourcen Design

Subresources und eigenständige Ressourcen fachlich sauber trennen

Security Audit

Prüfung auf Broken Object Level Authorization in verschachtelten Routen

Performance Review

N Plus 1 Analyse bei verschachtelten Collections und Relationen

10. Zusammenfassung

Verschachtelte Ressourcen über uriTemplate sind in API Platform ein mächtiges Werkzeug für Objekte, die strikt an ihren Elternkontext gebunden sind, sollten aber nicht reflexartig für jede Beziehung eingesetzt werden. Mehr als eine Verschachtelungsebene, fehlende Autorisierungsprüfung entlang der Elternressource und unkontrolliert eingebettete Relationen sind die häufigsten Fehler bei der praktischen Umsetzung.

Wer bei jeder Relation bewusst zwischen Subresource, eingebetteter Relation und eigenständiger Ressource entscheidet, statt eine einzige Regel für das gesamte Projekt zu verwenden, bekommt eine API Struktur, die sowohl für Menschen lesbar als auch technisch wartbar bleibt. Diese Entscheidung früh im Projekt zu treffen, erspart spätere, schwer rückgängig zu machende Umbauten an der URL Struktur.

Verschachtelte Ressourcen in API Platform: Das Wichtigste auf einen Blick

uriTemplate Subresources

Verschachtelte Collection Routen über einen Link auf die Elternressource, ohne manuellen Query Code.

IRI vs eingebettet

Standardmäßig IRI Referenz für kleine Antworten, eingebettete Relation nur gezielt über Serialization Groups.

Autorisierung

Security Ausdruck der Subresource muss die Elternressource explizit mit prüfen, sonst droht BOLA.

Wann eigenständig

Eigene Filter, eigener Lebenszyklus oder Bookmark Fähigkeit sprechen für eine flache, eigenständige Ressource.

11. FAQ: Verschachtelte Ressourcen in API Platform

1Wie definiere ich eine Subresource?
Über eine GetCollection mit uriTemplate und Platzhalter, kombiniert mit einem Link auf die Elternklasse.
2Subresource neben flacher Collection?
Ja, beide Routen können parallel existieren, flach für Suche, verschachtelt für den Elternkontext.
3IRI vs eingebettet?
IRI ist nur eine URL zum Nachladen, eingebettete Relation liefert die vollständigen Daten direkt mit.
4Wie tief verschachteln?
In der Regel nicht mehr als eine Ebene, mehrstufige Pfade sind schwer wartbar und schlecht bookmarkbar.
5Autorisierung gesondert prüfen?
Ja, das security Argument muss die Elternressource explizit mit prüfen, sonst droht BOLA.
6Serialisierung nach Kontext?
Über unterschiedliche Normalization Groups, eingebettet reduziert, Top Level vollständig.
7Wann eigenständige Ressource?
Bei eigenen Filtern, eigener Paginierung oder unabhängigem Lebenszyklus vom Elternteil.
8N Plus 1 bei verschachtelten Collections?
Ja, wenn pro Element separat eine Subresource Anfrage ausgelöst wird, eingebettete Relationen vermeiden das.
9Doctrine Filter automatisch?
Ja, über das Link Attribut mit fromClass und toProperty, ohne eigenen Query Code.
10Direkt referenzierbar bleiben?
Nur mit zusätzlicher flacher Get Operation, sonst nur über den verschachtelten Pfad erreichbar.