ohne Drift zwischen Docs und Implementierung
Dokumentation, die vom Code abweicht, ist schlimmer als gar keine Dokumentation. Wer OpenAPI-Schemas, Validierungsregeln und Beispielwerte in Symfony nicht strukturiert pflegt, baut technische Schulden auf, die sich bei jedem Breaking Change entladen. Dieser Artikel zeigt, wie man Schemas, Beispiele und Validierung dauerhaft synchron hält.
Inhaltsverzeichnis
- 1. Das Drift-Problem: Wenn Docs und Code auseinanderlaufen
- 2. Nelmio API Doc als Grundlage in Symfony
- 3. PHP-Attribute statt YAML: Schemas direkt am Code
- 4. Schema-Vererbung: allOf, oneOf und discriminator
- 5. Reusable Examples und example-Felder richtig einsetzen
- 6. Symfony Validator und OpenAPI-Constraints synchron halten
- 7. Components-Sektion: Schemas zentral verwalten
- 8. Contract-Tests: Schemas automatisch gegen Implementierung prüfen
- 9. Ansätze im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Das Drift-Problem: Wenn Docs und Code auseinanderlaufen
Schema-Drift ist das häufigste Problem in API-Projekten, die über mehrere Monate oder Teams wachsen. Ein Entwickler fügt einem Endpunkt ein neues Pflichtfeld hinzu, aktualisiert den Symfony-Validator, vergisst aber das OpenAPI-Schema. Ein anderer Entwickler verlässt sich auf die Dokumentation, baut einen Client, der das neue Feld nicht sendet – und erhält erst zur Laufzeit einen Validierungsfehler. Die Ursache ist strukturell: Wenn Schemas und Code in verschiedenen Artefakten leben, divergieren sie unter Entwicklungsdruck unweigerlich.
Die Lösung liegt nicht in Disziplin, sondern in Werkzeugen, die Divergenz unmöglich machen oder zumindest sofort sichtbar machen. In Symfony gibt es dafür drei Hebel: Erstens Schemas direkt am PHP-Code definieren, sodass eine Codeänderung die Dokumentation automatisch mitändert. Zweitens Validierungsconstraints aus denselben Quellen ableiten wie Schema-Definitionen. Drittens automatisierte Contract-Tests, die bei jedem Push prüfen, ob der tatsächliche API-Response dem deklarierten Schema entspricht. Dieser Artikel durchläuft alle drei Ansätze mit konkreten Symfony-Beispielen.
2. Nelmio API Doc als Grundlage in Symfony
Das Bundle nelmio/api-doc-bundle ist der Standard für OpenAPI-Dokumentation in Symfony-Projekten. Es liest PHP-Attribute, Doctrine-Entities, Symfony-Formulare und PHPDoc-Annotationen aus und generiert daraus eine vollständige openapi.json zur Laufzeit. Das Entscheidende: Die generierte Spezifikation ist niemals älter als der Code, weil sie bei jedem Request neu berechnet wird – es gibt keine separate Datei, die manuell aktuell gehalten werden müsste.
Die Installation und Grundkonfiguration ist minimal. Nach composer require nelmio/api-doc-bundle legt man in config/packages/nelmio_api_doc.yaml fest, welche Route-Prefixe dokumentiert werden sollen, welche Authentifizierungsmechanismen existieren und welche globalen Schema-Definitionen eingebunden werden. Die Route /api/doc.json liefert die maschinenlesbare Spezifikation, /api/doc die Swagger-UI. Für CI-Pipelines exportiert man die Spezifikation einmalig per bin/console nelmio:apidoc:dump --format=json > openapi.json und versioniert das Ergebnis neben dem Code.
# Install Nelmio API Doc Bundle
composer require nelmio/api-doc-bundle
# config/packages/nelmio_api_doc.yaml
nelmio_api_doc:
documentation:
info:
title: "Mironsoft API"
description: "REST API für Mironsoft-Dienste"
version: "1.0.0"
servers:
- url: "https://api.mironsoft.de/v1"
description: "Production"
- url: "http://localhost:8080/v1"
description: "Development"
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []
areas:
path_patterns:
- ^/api(?!/doc$)
# Export spec for CI
bin/console nelmio:apidoc:dump --format=json > openapi.json
Ein häufig übersehenes Feature von Nelmio ist der Area-Mechanismus. Über areas lassen sich mehrere getrennte API-Dokumentationen erzeugen – eine öffentliche für externe Konsumenten und eine interne für Admin-Endpunkte. Jede Area hat eigene Authentifizierungsschemata, eigene Server-URLs und kann über separate Routen erreichbar sein. Das verhindert, dass interne Endpunkte mit sensiblen Parametern in der öffentlichen Dokumentation auftauchen.
3. PHP-Attribute statt YAML: Schemas direkt am Code
Die OpenApi\Attributes-Klassen aus dem Paket zircote/swagger-php ermöglichen es, OpenAPI-Schemas direkt als PHP-Attribute an Controller-Methoden, DTOs und Model-Klassen zu schreiben. Nelmio liest diese Attribute automatisch aus. Der entscheidende Vorteil gegenüber separaten YAML-Dateien: Wenn ein Entwickler die Methode umbenennt, Parameter hinzufügt oder den Response-Typ ändert, sieht er die zugehörige Dokumentation direkt daneben – und die Wahrscheinlichkeit steigt, dass er sie mit aktualisiert.
Das Attribute-System unterstützt den vollen OpenAPI-3.1-Umfang: #[OA\RequestBody], #[OA\Response], #[OA\Parameter], #[OA\Property] für Schema-Felder, #[OA\Schema] für wiederverwendbare Definitionen. In DTOs annotiert man jede Property direkt, was Typ, Format, Beschreibung und Beispielwerte zusammen mit dem PHP-Typdeklaration hält. Wenn der PHP-Typ von string auf int geändert wird, fällt der Widerspruch zum #[OA\Property(type: "string")] sofort auf.
// src/Dto/ProductCreateDto.php
<?php
declare(strict_types=1);
namespace App\Dto;
use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;
#[OA\Schema(
schema: "ProductCreateRequest",
required: ["name", "price", "categoryId"],
description: "Payload to create a new product"
)]
final class ProductCreateDto
{
public function __construct(
#[OA\Property(description: "Product name", example: "Wireless Headphones", maxLength: 255)]
#[Assert\NotBlank]
#[Assert\Length(max: 255)]
public readonly string $name,
#[OA\Property(description: "Price in Euro cents", example: 4999, minimum: 1)]
#[Assert\Positive]
public readonly int $price,
#[OA\Property(description: "UUID of the parent category", format: "uuid")]
#[Assert\Uuid]
public readonly string $categoryId,
#[OA\Property(description: "Optional product description", nullable: true)]
#[Assert\Length(max: 2000)]
public readonly ?string $description = null,
) {}
}
4. Schema-Vererbung: allOf, oneOf und discriminator
Reale APIs haben selten flache, homogene Schemas. Bestellungen können verschiedene Zahlungsarten haben, Benachrichtigungen können E-Mail, SMS oder Push sein, Produkte können physische oder digitale Varianten sein. OpenAPI 3.x bietet dafür allOf, oneOf und anyOf mit optionalem discriminator. In Symfony-Projekten bildet man diese Polymorphie durch abstrakte Basis-DTOs und konkrete Ableitungen ab, die jeweils ihr eigenes #[OA\Schema]-Attribut tragen.
Der discriminator teilt OpenAPI-Clients mit, anhand welches Felds der konkrete Subtyp unterschieden wird. Das Schema der Basisklasse deklariert das Diskriminatorfeld als required, die Kindklassen erweitern per allOf. Nelmio kann diese Hierarchie automatisch ableiten, wenn man die Vererbung durch PHP-Interfaces oder abstrakte Klassen ausdrückt und die Attribute korrekt setzt. Ohne discriminator müssen Code-Generatoren raten, welchen Subtyp sie instanziieren sollen – was zu schwer debugbaren Fehlern in generierten Clients führt.
5. Reusable Examples und example-Felder richtig einsetzen
OpenAPI unterscheidet zwischen example (ein einzelner Wert direkt am Schema oder Parameter) und examples (eine benannte Map von Beispielobjekten in der Components-Sektion). Die examples-Map ist für Situationen gedacht, in denen ein Endpunkt mehrere repräsentative Szenarien hat: einen Erfolgsfall, einen Validierungsfehler, einen Berechtigungsfehler. Jedes Beispiel hat einen summary-Text und einen value. Swagger-UI und Redoc zeigen diese Beispiele als auswählbare Varianten an, was die Dokumentation für API-Konsumenten erheblich nützlicher macht.
In Symfony-Projekten lagert man häufig verwendete Beispiele in die Components-Sektion aus und referenziert sie mit $ref: '#/components/examples/ProductCreated'. Das verhindert Duplikate und stellt sicher, dass ein geänderter Beispielwert überall aktualisiert wird. Besonders wichtig: Beispielwerte müssen dem Schema entsprechen. Ein Beispiel, das ein Pflichtfeld weglässt oder einen falschen Typ verwendet, wird von manchen Validierungstools als Fehler markiert. Die Kombination aus Schema-Validierung und Beispiel-Konsistenz ist ein häufig unterschätzter Qualitätsindikator für API-Dokumentation.
# openapi/examples.yaml — Reusable examples in components section
components:
examples:
ProductCreated:
summary: "Successful product creation"
value:
id: "01906c3f-a8b2-7e4d-9f1a-3c2d4e5f6a7b"
name: "Wireless Headphones"
price: 4999
categoryId: "01906c3f-1111-7e4d-9f1a-aabbccddeeff"
description: "Over-ear with active noise cancellation"
createdAt: "2026-05-09T14:30:00Z"
ValidationError:
summary: "Validation failed — missing required field"
value:
type: "https://mironsoft.de/errors/validation"
title: "Validation Failed"
status: 422
violations:
- field: "name"
message: "This value should not be blank."
- field: "price"
message: "This value should be positive."
schemas:
ProblemDetail:
type: object
required: [type, title, status]
properties:
type:
type: string
format: uri
title:
type: string
status:
type: integer
violations:
type: array
items:
$ref: '#/components/schemas/ConstraintViolation'
6. Symfony Validator und OpenAPI-Constraints synchron halten
Der häufigste Drift-Punkt in Symfony-APIs liegt zwischen Validierungsconstraints und OpenAPI-Schema-Eigenschaften. Ein Feld wird im Symfony-Validator als #[Assert\Length(max: 255)] markiert, im OpenAPI-Schema aber ohne maxLength: 255 deklariert. Clients, die die Dokumentation lesen, haben keine Möglichkeit zu wissen, dass es ein Längenlimit gibt – bis sie beim Senden langer Werte einen 422-Fehler erhalten. Das ist schlechtes API-Design, das durch Disziplin allein nicht dauerhaft verhindert werden kann.
Die sauberste Lösung ist eine gemeinsame Datenquelle: Die DTO-Properties tragen sowohl Symfony-Validation-Attribute als auch OpenAPI-Property-Attribute, die dieselben Constraint-Werte enthalten. Durch eine einfache PHPUnit-Extension lässt sich automatisch prüfen, ob alle #[Assert\Length(max: X)]-Constraints einen entsprechenden maxLength: X im zugehörigen OA-Property-Attribut haben. Dasselbe gilt für #[Assert\Range] und minimum/maximum, für #[Assert\NotBlank] und das required-Array im Schema sowie für Enum-Constraints und den enum-Wert im Schema.
7. Components-Sektion: Schemas zentral verwalten
Die components/schemas-Sektion einer OpenAPI-Spezifikation ist das Äquivalent eines Typsystems. Schemas, die an mehreren Stellen verwendet werden – Pagination-Wrapper, Error-Response, Timestamps, Money-Objekte – gehören in die Components-Sektion und werden über $ref referenziert, niemals inline dupliziert. Ein inline dupliziertes Schema, das an zehn Stellen vorkommt, muss bei einer Änderung an zehn Stellen aktualisiert werden – und wird es in der Praxis nicht, weil der Entwickler drei davon vergisst.
In Symfony nutzt man dafür dedizierte Schema-Klassen, die ausschließlich als OpenAPI-Typdefinitionen dienen und nie instanziiert werden – reine Dokumentationsobjekte. Alternativ annotiert man bestehende Value Objects und DTOs mit #[OA\Schema(schema: "MoneyValue")]. Nelmio sammelt alle so markierten Klassen und fügt sie automatisch in die Components-Sektion ein. Die Referenzierung erfolgt dann durch Typdeklaration in anderen Schemas: Nelmio erkennt, dass ein Property den Typ MoneyValue hat, und generiert den entsprechenden $ref.
8. Contract-Tests: Schemas automatisch gegen Implementierung prüfen
Der letzte Schutzmechanismus gegen Schema-Drift sind automatisierte Contract-Tests. Das Paket league/openapi-psr7-validator oder spectator ermöglicht es, PHPUnit-Tests zu schreiben, die einen echten HTTP-Request gegen die Symfony-Applikation senden und den Response automatisch gegen das deklarierte OpenAPI-Schema validieren. Wenn ein Response ein Pflichtfeld weglässt, einen falschen Typ zurückgibt oder eine nicht deklarierte Property enthält, schlägt der Test fehl – ohne dass der Testautor selbst eine Assertion schreiben muss.
In der Praxis sieht ein Contract-Test aus wie ein normaler Symfony-WebTestCase: Kernel booten, Request senden, Response-Status prüfen. Der einzige Unterschied ist, dass der Response-Body zusätzlich durch den Schema-Validator geführt wird. Das Paket lädt dafür die generierte openapi.json aus dem Projekt, sucht den passenden Pfad und die passende Methode und validiert Body, Header und Status-Code. Dieser Ansatz findet Regressionsfehler, bevor sie in Produktion gelangen – und zwar automatisch, ohne dass jemand die Dokumentation manuell liest.
// tests/Api/ProductApiContractTest.php
<?php
declare(strict_types=1);
namespace App\Tests\Api;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
final class ProductApiContractTest extends WebTestCase
{
private static $validator;
public static function setUpBeforeClass(): void
{
// Load the exported OpenAPI spec
$specPath = __DIR__ . '/../../openapi.json';
self::$validator = (new ValidatorBuilder())
->fromJsonFile($specPath)
->getResponseValidator();
}
public function testCreateProductResponseMatchesSchema(): void
{
$client = static::createClient();
$client->request('POST', '/api/products', [], [], [
'CONTENT_TYPE' => 'application/json',
'HTTP_AUTHORIZATION' => 'Bearer ' . $this->getTestToken(),
], json_encode([
'name' => 'Wireless Headphones',
'price' => 4999,
'categoryId' => '01906c3f-1111-7e4d-9f1a-aabbccddeeff',
]));
$response = $client->getResponse();
$this->assertSame(201, $response->getStatusCode());
// Validate response body against OpenAPI schema — fails if schema drifts
$psr7Response = $this->convertToPsr7($response);
$operation = new \League\OpenAPIValidation\PSR7\OperationAddress('/api/products', 'post');
self::$validator->validate($operation, $psr7Response); // throws on mismatch
}
}
9. Ansätze im direkten Vergleich
Es gibt mehrere Strategien, OpenAPI-Schemas in Symfony zu pflegen. Die Wahl hat direkten Einfluss darauf, wie leicht Schema-Drift entsteht und wie schnell er erkannt wird.
| Ansatz | Drift-Risiko | Wartungsaufwand | Empfehlung |
|---|---|---|---|
| Manuelle YAML-Datei | Sehr hoch | Hoch (jede Änderung doppelt) | Nur für sehr kleine APIs |
| PHP-Attribute + Nelmio | Mittel | Niedrig (neben dem Code) | Empfohlen als Basis |
| Attribute + Contract-Tests | Niedrig | Niedrig + CI-Absicherung | Empfohlen für Teams |
| API-Platform (auto-gen) | Sehr niedrig | Sehr niedrig | Wenn API-Platform passt |
| Codegen aus YAML-First | Niedrig (andere Richtung) | Mittel (YAML als Source of Truth) | Für API-First-Teams |
Die Kombination aus PHP-Attributen direkt an DTOs und automatisierten Contract-Tests ist für die meisten Symfony-Teams der pragmatische Sweet Spot. Sie erfordert keine zusätzliche Infrastruktur, ist für jeden PHP-Entwickler lesbar und fängt Regressionsfehler automatisch ab. API-Platform ist die bessere Wahl, wenn die API stark an Doctrine-Entities gebunden ist und CRUD-Endpunkte dominieren.
Mironsoft
REST API Design, OpenAPI-Dokumentation und Symfony-Entwicklung
OpenAPI-Schemas, die nie mit dem Code driften?
Wir richten Nelmio API Doc, PHP-Attribute und Contract-Tests in eurem Symfony-Projekt ein und stellen sicher, dass eure OpenAPI-Spezifikation dauerhaft den echten API-Verhalten entspricht – automatisch geprüft bei jedem CI-Run.
Schema-Audit
Bestehende OpenAPI-Spezifikation auf Drift, fehlende Beispiele und Inkonsistenzen prüfen
Nelmio-Setup
PHP-Attribute, Components-Sektion und Area-Konfiguration für euer Projekt einrichten
Contract-Tests
Automatisierte Schema-Validierung in PHPUnit und CI-Pipeline integrieren
10. Zusammenfassung
Konsistente OpenAPI-Dokumentation in Symfony ist kein Zufall, sondern das Ergebnis struktureller Entscheidungen. PHP-Attribute direkt an DTOs und Controllern halten Schema-Definition und Implementierung nah beieinander – nicht in separaten Dateien, die unter Entwicklungsdruck auseinanderlaufen. Nelmio API Doc generiert die Spezifikation aus dem Code, nicht umgekehrt, und eliminiert damit die manuelle Synchronisation als Fehlerquelle. Die Components-Sektion zentralisiert wiederverwendbare Schemas und verhindert Duplikate.
Contract-Tests sind der entscheidende letzte Schutzmechanismus: Sie prüfen automatisch, ob tatsächliche API-Responses dem deklarierten Schema entsprechen. Kein Entwickler muss mehr manuell prüfen, ob eine Schema-Änderung alle Endpunkte betrifft – der CI-Run erledigt das. In Kombination bilden diese vier Werkzeuge einen geschlossenen Kreislauf: Code ändert sich, Dokumentation wird automatisch aktualisiert, Contract-Tests verifizieren die Konsistenz, CI blockiert den Merge bei Drift.
OpenAPI in Symfony konsistent pflegen — Das Wichtigste auf einen Blick
Nelmio API Doc
Generiert OpenAPI aus PHP-Code – keine manuell gepflegte YAML-Datei, keine veraltete Dokumentation. Export per Console-Command für CI.
PHP-Attribute am DTO
Schema-Definition, Validierungsconstraints und Typdeklaration zusammen an einer Stelle – Drift wird strukturell erschwert.
Components-Sektion
Wiederverwendbare Schemas, Beispiele und Responses zentral verwalten und per $ref referenzieren – nie inline duplizieren.
Contract-Tests
Automatische Schema-Validierung echter API-Responses in PHPUnit – fängt Regressionsfehler bevor sie in Produktion gelangen.