ionicons-v5-a Blog

Mit dem stetigen Wachstum der Zahl der mit dem Internet verbundenen Geräte steigen die Anforderungen an Web-APIs mehr denn je. Das geht so weit, dass gängige Web-Frameworks häufig als Plattformen für die Entwicklung von Rest-APIs verwendet werden. Da jedoch keines von ihnen für die Abwicklung eines Rest-API-Projekts entwickelt wurde, erhöht ihr Einsatz schnell die Projektkomplexität, was zu längeren Entwicklungszeiten und höheren Wartungskosten führt.

Ziele

In diesem Dokument werden wir versuchen eine Frage zu beantworten, die so alt ist wie die Entwicklung selbst – wie strukturiert man ein Projekt richtig, damit es:

Diese Struktur ermöglicht die Entwicklung von Applikationen mit mehreren Modulen. Genau das haben wir in dem beigefügten Beispiel getan. Es funktioniert jedoch ebenso gut mit einem einzelnen Modul.

Was wir hier nicht behandeln

In diesem Artikel soll nicht auf die Einzelheiten der Code-Implementierung eingegangen werden; es wird jedoch ein Beispielprojekt vorgestellt, auf das Sie Ihre Projekte leicht aufbauen können. Wir gehen nicht im Detail darauf ein, wie Sie Ihre API-Ausgaben richtig handhaben – Das ist ein Thema für einen anderen Beitrag. In diesem Beispiel werden stattdessen einfache „JsonResponses“ verwendet. Für eine Anwendung in der realen Welt ist die Standardisierung der API-Ausgabe ein Muss. Auch die Sicherheitsimplementierung ist nicht Gegenstand dieses Textes. Sie werden jedoch wahrscheinlich die Sicherheitsvorteile des vorgestellten Ansatzes erkennen, auch wenn sie nicht erwähnt werden.

Das Framework

Wir verwenden Symfony, da es ein weit verbreitetes Framework für die webbasierte API-Entwicklung ist. Ein Framework sollte niemals die Anwendungsstruktur diktieren und genau das versucht Symfony 4 nicht zu tun. Bei Symfony 3 war das anders und der erste Fehler, den fast jedes Team machte, das an einem Symfony-basierten Projekt arbeitete, war die Verwendung seiner „gebündelten Struktur“. Diese Struktur sollte nur dann verwendet werden, wenn der Code exakt so wie er ist in anderen Anwendungen wiederverwendet werden soll, was in den meisten Fällen nicht der Fall ist.

Die API-Projektstruktur

Um die Struktur zu demonstrieren, haben wir ein Beispielprojekt erstellt, das wir bereitstellen. Dieses Projekt hat zwei Anwendungen, die sich einige Ressourcen teilen – die „Consumer“-Anwendung, die den API-Zugriff von der Verbraucherseite aus ermöglichen soll, und die „Admin“-Anwendung, die zur Verwaltung der API für den Administratorzugriff erstellt wurde. Die „Admin“-Anwendung hat Zugriff auf alle Ressourcen der „Consumer“-Anwendung, während die „Consumer“-Anwendung nur auf ihre eigenen Ressourcen zugreifen kann. Auf sie soll von verschiedenen Domains aus zugegriffen werden können (z.B. www.your-api-site.tld und admin.your-api-site.tld). Wir beginnen mit dem Root-Ordner des Projekts, der auch die Datei composer.json und den vendor Ordner enthält.

Root Struktur

```
├─ bin/
├─ config/
├─ docs/
├─ migrations/
├─ public/
├─ src/
├─ tests/
├─ translations/
├─ var/
└─ vendor/
...
```

Die Root-Ordner sind alle in Kleinbuchstaben geschrieben. Sie sind wahrscheinlich mit der Verwendung der meisten, wenn nicht sogar aller, vertraut. Aber lassen Sie uns diese durchgehen, damit wir auf demselben Stand sind.Die Ordner `bin` und `public` enthalten die Front-Controller für den Cli- bzw. Web-Zugriff. Der Ordner `config` enthält die globalen Einstellungen, die von allen Anwendungen im Projekt geerbt werden. `docs` ist für die Swagger-API-Dokumentation gedacht. Das kann ausgelassen werden, wenn Sie nicht vorhaben, Swagger zu verwenden oder wenn Sie beabsichtigen, Swagger Annotations zu verwenden (nicht empfehlenswert, da es die Lesbarkeit des Codes verringert. YAML ist besser lesbar und die Controller nicht überfrachtet). `migrations` wird alle Migrationen für alle Anwendungen enthalten. `src` enthält den Quellcode unserer Anwendungen und ist die Namespace-Root für unser Projekt. Im mitgelieferten Beispiel ist das die „SampleApp“. `tests` ist für Unit-Tests gedacht, falls Sie diese verwenden. `translations` sollte Übersetzungsbibliotheken enthalten. Der Ordner `var` wird für alle von der Anwendung generierten Dateien verwendet. `vendor` wird vom Composer generiert und enthält alle Abhängigkeiten.

Bin Ordner

Bei einer Standard-Symfony-Installation würde der Ordner `bin` nur den `console` Front-Controller für die CLI enthalten. Wenn Ihr Projekt nur eine Anwendung (ein Modul) enthielte, würde das so bleiben. Aber in einer Multi-Anwendungs-Umgebung, wie dem mitgelieferten Beispiel, hat `bin` mehr CLI-Front-Controller:

```
...
├─ bin/
│  ├─ admin
│  ├─ console
│  └─ consumer
...
```

In diesem Setup werden Befehle, die in der Datei `admin` aufgeführt sind, nur in der Admin-Anwendung ausgeführt. Dasselbe gilt für die Consumer-Anwendung. Die Konsole führt jedoch in beiden Anwendungen den gleichen Befehl aus. Dies ist für automatisierte Bereitstellungen oder Skripte erforderlich, die nach einem Composer-Update oder einer Composer-Installation ausgeführt werden.

Config Ordner

Der `config` Ordner enthält den Standardinhalt für eine Symfony-App. Das einzige was Sie sich merken sollten ist, dass das in diesem Setup als „globale“ Konfiguration zu betrachten ist. Jedes Modul/jede Applikation kann einen eigenen Config-Ordner haben, der die globale Konfiguration erweitert oder überschreibt. Eine globale Konfiguration kann z.B. einen globalen Entity-Manager haben, der so konfiguriert ist, dass er mit der Hauptdatenbank des Projekts arbeitet. Ein Modul kann seinen eigenen Entity-Manager definieren, der nur mit dem für dieses Modul spezifischen Schema arbeitet (kein anderes Modul hat Zugriff darauf).

Docs Ordner

Docs ist dazu gedacht, übergeordnete Definitionen zu enthalten. Z.B.:

```
...
├─ docs/
│  ├─ admin.yaml
│  ├─ common.yaml
│  └─ consumer.yaml
...
```

Erstellen Sie Ihre Swagger-Dokumente niemals mit Kommentaren. Sie sind schwer zu pflegen, werden nicht automatisch eingerückt und verringern die Lesbarkeit des Codes…

Migrations

Migrations sollten wie folgt geordnet werden:

```
...
├─ migrations/
│  ├─ Admin/
│  │  ├─ Version2019{...}.php
│  │  ├─ Version2019{...}.php
│  │  └─ ...
│  └─ Consumer/
│     ├─ Version2019{...}.php
│     ├─ Version2019{...}.php
│     └─ ...
...
```

Public Ordner

In den meisten Fällen enthält der Public-Ordner nur den Front-Controller für das gesamte Projekt – `index.php`. Bei Lösungen mit Multi-App-Anwendungen kann entweder der Front-Controller refaktorisiert werden, um die Domäne zu erkennen und zu entscheiden, welcher Kernel der Anwendung gestartet werden soll, oder es kann die folgende Lösung verwendet werden:

```
...
├─ public/
│  ├─ admin/
│  │  └─ index.php
│  └─ consumer/
│     └─ index.php
...
 
```

Auf diese Weise entscheiden Apache oder Nginx in Abhängigkeit von der Domäne, aus der die Anfrage kommt, welchen Front-Controller sie aufrufen.

Src Ordner

Der Hauptordner, um den es in diesem Dokument geht – bei einigen Anwendungen ist dies auch der Ordner `app`. Er ist das Root-Verzeichnis des Namespaces unseres Projektes und enthält die Hauptordner der Applikationen sowie den Ordner der gemeinsamen Ressourcen `Core`:

```
...
├─ src/
│  ├─ Admin/
│  ├─ Consumer/
│  └─ Core/
...
```

Der „Core“ und die gemeinsamen Kompenenten

Der `Core`  kann alle gemeinsamen Komponenten wie Subscribers, Exceptions, Sicherheitsimplementierungen auf Projektebene usw. enthalten. Er sollte jedoch keine Controller/Aktionen, Entitäten oder andere Ressourcen enthalten, die spezifisch für eine Anwendung im Projekt sind. Im vorliegenden Beispiel werden ein ExceptionSubscriber und ein RequestSubscriber bereitgestellt. Diese betreffen die Bearbeitung von Anfragen und Antworten für alle Anwendungen innerhalb des Projekts. RequestSubscriber überprüft die Gültigkeit eingehender Anfragen. Im gegebenen Beispiel ist das recht einfach; wir schlagen jedoch eine saubere Implementierung des Content Negotiators vor. ExceptionSubscriber fängt Ausnahmefälle ab und gibt darauf eine API-freundliche Antwort. Ein weiterer Subscriber, der unter den APIs häufig vorkommt, ist ein ResponseSubscriber. Dieser soll die Antworten korrekt verarbeiten, anstatt die JsonResponse zu verwenden, wie wir es im angegebenen Beispiel tun. Ein richtiger Ansatz wäre die Zurückweisung von Ressourcen oder Repräsentations-Objekten von Controllern und deren Serialisierung gemäß dem angeforderten Inhaltstyp im ResponseSubscriber. Eine Serialisierung in jedem Controller würde zu einer Menge sich wiederholenden Codes führen, die nicht mit dem von uns verwendeten Entwurfsmuster übereinstimmen (mehr dazu gleich).

Die application Ordner

Die Haupt-Anwendungsordner sind dort, wo die meiste Entwicklungsarbeit geleistet wird. Sie werden mit dem Umfang der Applikation wachsen und irgendwann könnte eine neue Version für die API eingeführt werden. Um dem Wachstum der Anwendung gerecht zu werden, teilen sie die Funktionen ihrer Anwendung in Gruppen auf. Zum Beispiel werden benutzerbezogene Funktionen wie Authentifizierung, Kontoverwaltung und Ähnliches in den Ordner „Account“ oder „User“ verschoben. Funktionen, die sich auf die Handhabung von Zahlungen oder die Ausstellung von Rechnungen beziehen, gehen in „Finance“. Wenn Sie diese Logik anwenden, sieht die Struktur am Ende etwa so aus:

```
...
├─ src/
│  ├─ Admin/
│  │  ├─ ...
│  │ ...
│  │  └─ Kernel.php
│  ├─ Consumer/
│  │  ├─ Account/
│  │  │  ├─ Action/
│  │  │  │  ├─ Login.php
│  │  │  │  ├─ Register.php
│  │  │  │  └─ ResetPassword.php
│  │  │  ├─ Command/
│  │  │  │  ├─ LoginCommand.php
│  │  │  │  ├─ RegisterCommand.php
│  │  │  │  └─ ResetPasswordCommand.php
│  │  │  ├─ Entity/
│  │  │  │  └─ User.php
│  │  │  ├─ Exception/
│  │  │  │  └─ InvalidCredentialsException.php
│  │  │  ├─ Handler/
│  │  │  │  ├─ LoginHandler.php
│  │  │  │  ├─ RegisterHandler.php
│  │  │  │  └─ ResetPasswordHandler.php
│  │  │  └─ Repository/
│  │  ├─ Catalog/
│  │  │  ├─ Action/
│  │  │  ├─ Command/
│  │  │  ├─ Entity/
│  │  │  └─ Handler/
│  │  ├─ config/
│  │  │  ├─ packages/
│  │  │  ├─ routes/
│  │  │  └─ services.yaml
│  │  ├─ Finance/
│  │  │  ├─ Action/
│  │  │  ├─ Command/
│  │  │  ├─ Entity/
│  │  │  ├─ Event/
│  │  │  └─ Handler/
│  │  └─ Kernel.php
│  └─ Core/
│     ├─ Exception/
│     │  ├─ ResourceExpiredException.php
│     │  ├─ UnsupportedContentTypeException.php
│     │  └─ ValidationException.php
│     ├─ Subscriber/
│     │  ├─ ExceptionSubscriber.php
│     │  ├─ RequestSubscriber.php
│     │  └─ ResponseSubscriber.php
│     └─ Traits/
│        └─ Timestampable.php
...
```

Bei kleinen Projekten kann auf eine solche Gruppierung verzichtet werden. Sie sollte jedoch eingeführt werden, wenn das Projekt wächst, da die Größe des Projekts es sonst zu ersticken droht.Hier ist ein Beispiel für ein kleineres Projekt:

```
...
├─ src/
│  ├─ API/
│  │  ├─ Action/
│  │  │  ├─ Auth/
│  │  │  │  ├─ Login.php
│  │  │  │  ├─ Register.php
│  │  │  │  └─ ResetPassword.php
│  │  │  ├─ ViewSomething.php
│  │  │  └─ CreateSomething.php
│  │  ├─ Command/
│  │  │  ├─ Auth/
│  │  │  │  ├─ LoginCommand.php
│  │  │  │  ├─ RegisterCommand.php
│  │  │  │  └─ ResetPasswordCommand.php
│  │  │  ├─ CreateSomethingCommand.php
│  │  │  └─ ViewSomethingCommand.php
│  │  ├─ config/
│  │  │  ├─ packages/
│  │  │  ├─ routes/
│  │  │  └─ services.yaml
│  │  ├─ Entity/
│  │  │  ├─ Something.php
│  │  │  └─ User.php
│  │  ├─ Event /
│  │  ├─ Exception/
│  │  │  └─ InvalidCredentialsException.php
│  │  ├─ Handler/
│  │  │  ├─ Auth/
│  │  │  │  ├─ LoginHandler.php
│  │  │  │  ├─ RegisterHandler.php
│  │  │  │  └─ ResetPasswordHandler.php
│  │  │  └─ SomethingHandler.php
│  │  ├─ Repository/
│  │  └─ Kernel.php
│  └─ Core/
│     ├─ Exception/
│     │  ├─ ResourceExpiredException.php
│     │  ├─ UnsupportedContentTypeException.php
│     │  └─ ValidationException.php
│     ├─ Subscriber/
│     │  ├─ ExceptionSubscriber.php
│     │  ├─ RequestSubscriber.php
│     │  └─ ResponseSubscriber.php
│     └─ Traits/
│        └─ Timestampable.php
...
```

Die genutzten Design Patterns

Die obigen Strukturen führen Änderungen am gemeinsamen MVC-Pattern ein. Wenn Sie sich mit Designpattern beschäftigen, haben Sie wahrscheinlich bereits die Elemente eines ADR-Patterns (Action-Domain-Responder) sowie des Befehls-Patterns erkannt.

Das erste, was Ihnen auffallen wird, ist, dass die Controller durch Actions ersetzt werden. Statt einer Controller-Datei, die mehrere Methoden (Actions) enthält, welche jeweils mehrere Endpunkte definieren, verwendet diese Struktur Actions. Dabei definiert jede Datei einen einzigen Endpunkt. Diese Action-Dateien sind eigentlich die Invokable-Controller von Symfony, die aus einem Constructor (der für die automatische Verknüpfung verwendet wird) und einer Aufruf-Methode bestehen, welche beim Zugriff auf diesen Endpunkt aufgerufen wird. Dies führt zu viel kleineren Dateien, die viel einfacher zu lesen und zu steuern sind. Sie müssen nicht mehr den Controller öffnen und von Methode zu Methode nach unten scrollen, um den Endpunkt zu suchen, an dem Sie arbeiten möchten…

Sobald die Anfrage auf eine Action trifft, versucht sie einen für die Bearbeitung der Anfrage erforderlichen Command zu instanziieren. Die Commands sollten eine Factory-Methode enthalten, die die Eingabedaten entgegennimmt und eine Instanz des Commands zurückgibt. Alle Assertions und Validierungen müssen in der Factory-Methode des Commands erfolgen. Wenn mit den Eingabedaten etwas nicht stimmt (fehlender Parameter, ungültige Eingabe usw.), muss die Factory-Methode eine Exception auslösen. Diese Exception wird dann vom ExceptionSubscriber abgefangen, der eine ordnungsgemäße 422-Antwort generiert. Der Command muss, sobald er instanziiert ist, alle Daten enthalten, die der Handler zur Behandlung der Anfrage benötigt. Die Daten in einer Befehlsinstanz müssen gültig sein und erfordern keine weitere Validierung.

Nach der Instanziierung wird der Befehl über den Command Bus abgewickelt. Ein passender Handler wird aufgerufen und der Großteil der Logik geschieht dort. In dem Beispielprojekt haben wir die einfachsten Handler als Proof-of-Concept erstellt. In einem realen Projekt sollte der Handler eine Sammlung mit Ressourcen oder sogar eine vollständige Darstellung zurückgeben, je nach Setup. Die Action sollte eine Darstellung zurückgeben, die dann von dem bereits erwähnten ResponseSubscriber serialisiert wird.

Voraussetzungen

Um eine Arbeitsstruktur wie oben erwähnt aufbauen zu können, müssen einige wenige Abhängigkeiten geschaffen werden (abgesehen natürlich vom Skelett von Symfony 4):

Empfehlungen

Die folgenden Punkte wurden in diesem Text nicht behandelt. Wir empfehlen Ihnen jedoch dringend, sich damit auseinanderzusetzen und sie in Ihren zukünftigen API-Projekten zu verwenden:

Weiterführende Literatur:

Newsletter

Erhalten Sie relevante Updates, wertvolle Informationen und kein Spam.