APIs mit Zend Expressive erstellen

Alles an Bord: REST-APIs mit Zend aufbauen
1 Kommentar

Das Microframework Zend Expressive setzt auf standardisierte Komponenten aus dem Zend Framework auf, einem der am häufigsten eingesetzten PHP-Frameworks der letzten Dekade. In diesem Artikel erfahren Sie, wie Sie mit Zend Expressive einen RESTful Web Service (oder kurz ein REST-API) aufbauen können.

In der PHP-Welt gab es in den letzten zwanzig Jahren viele Frameworks, die kamen und wieder gingen. Das Zend Framework erschien bereits vor zehn Jahren im Juni 2007 als 1.0-Release und hat sich seitdem stetig weiterentwickelt. Auch wenn der Name anderes impliziert, war das Zend Framework schon immer nicht nur ein klassisches Full-Stack-Framework für PHP, sondern vor allem auch eine Komponentensammlung. So bietet es alle Werkzeuge, die ein PHP-Entwickler für klassische Webanwendungen benötigt:

  • einen Datenbankabstraktionslayer mit Zend\Db
  • einen Dependency-Injection-Container mit Zend\ServiceManager
  • eine Formularkomponente mit Zend\Form
  • eine Authentifizierungskomponente mit Zend\Authentication
  • einen View-Layer mit Zend\View
  • einen MVC-Layer mit Zend\Mvc
  • und viele weitere Komponenten für Caching, Eingabevalidierung, Benutzerautorisierung, Navigation, Paginierung, uvm.

Seit dem Release 3.0, das im Juni 2016 erschienen ist, bietet das Zend Framework neben dem klassischen MVC-Layer auch die leichtgewichtige Alternative Zend Expressive, die auf einige standardisierte Komponenten setzt und die gesamte Anwendung auf Middleware-Basis aufbaut.

RESTful Web Services mit dem Zend Framework

Das Zend Framework bot schon seit dem 1.0-Release einige unterstützende Komponenten für den Aufbau von Web Services. So konnten Entwickler mit Zend_Rest sowohl REST-Server betreiben als auch REST-Services abrufen. Zudem gab es auch Unterstützung für SOAP mit Zend_Soap und XML-RPC mit Zend_XmlRpc.

Mit dem 2.0-Release vom Zend Framework, das im September 2012 veröffentlicht wurde, ist die Unterstützung für REST-APIs noch einmal verbessert worden. Nun wurde ein spezieller RESTful-Controller bereitgestellt, mit dem der Betrieb eines RESTful Web Service noch einfacher implementiert werden konnte. Dieser Controller hat automatisch die HTTP-Methode ausgewertet und die passenden Aktionsmethoden aufgerufen. Ein Nachteil hierbei ist, dass dies nur mit dem klassischen MVC-Layer Zend\Mvc funktioniert, der durch den Einsatz im Full-Stack-Framework einen gewissen Overhead mit sich bringt.

Tools every PHP Developer needs to know

mit Sebastian Bergmann (thePHP.cc)

Learning Machine Learning

mit Joel Lord (Auth0)

Ein weiteres Projekt aus der Zend-Framework-Welt ist Apigility. Dabei handelt es sich um eine Sammlung weiterer Module für den Aufbau von REST und RPC Web Services. Einige Features von Apigility sind die Unterstützung von HAL JSON, Problem-API, Versionierung, Authentifizierung per HTTP Basic/Digest und OAuth2 sowie eine automatische Dokumentation des API. Apigility setzt in seiner 1.0-Version auf dem MVC-Layer vom Zend Framework auf. Apigility 2 wird dies ändern und fortan als Basis auf Zend Expressive und damit einer Middleware-Anwendung aufbauen. Derzeit wird schon an einzelnen Modulen für Apigility 2 entwickelt. Ein Erscheinungstermin ist aber zum Zeitpunkt der Drucklegung noch nicht in Sicht.

Zend Expressive setzt und nutzt Standards

Zend Expressive setzt auf einigen standardisierten Zend-Framework-Komponenten auf. Die beiden Standards PSR-7 und PSR-15 werden von der PHP-FIG definiert und herausgegeben (Kasten: „Was ist ein PSR?“).

Was ist ein PSR?

Die PHP Framework Interop Group (kurz PHP-FIG) ist ein Zusammenschluss von PHP-Frameworks (CakePHP, Symfony, Flow, Zend Framework) und Projekten (Composer, Drupal, Joomla!, Magento, SugarCRM), dessen Ziel eine Standardisierung für die PHP-Welt ist. Die PHP-FIG hat eine Reihe von sogenannten PHP Standards Recommendations (kurz PSR) verabschiedet, die unter anderem Themen wie Codestil, Logging, Autoloading, Caching, Dependency-Injection-Container und HTTP-Nachrichten standardisieren. Jedes Mitgliedsprojekt stellt einen Vertreter, die dann gemeinsam an den PSRs arbeiten. Dabei ist keines der unterstützenden Projekte verpflichtet, allen Standardempfehlungen zu folgen. Dennoch versuchen alle Mitglieder, dieses Ziel zu erreichen.

Die Komponente zend-diactoros implementiert den PSR-7 und stellt somit eine Gruppe von Klassen für HTTP Messages wie Request und Responses bereit. Damit lassen sich schon einfache Anwendungen aufbauen, dennoch ist beim direkten Einsatz einiges an Handarbeit erforderlich. Der PSR-15 ist derzeit (Ende Juli 2017) noch nicht vollständig verabschiedet, wird aber bereits weitestgehend von der Zend-Framework-Komponente zend-stratigility implementiert. Diese Komponente bildet die Grundlage für den Aufbau und die Verarbeitung sogenannter Middleware-Pipelines (Kasten: „Was ist eine Middleware-Pipeline?“).

Zend Expressive setzt nun auf diesen beiden standardisierten Komponenten auf und bildet die Grundlage für komplexe Middleware-Anwendungen. Die starke Standardisierung ermöglicht dabei auch den Einsatz von Komponenten, die nicht aus dem Zend-Framework-Kosmos stammen. So werden unterschiedliche Router, Dependency-Injection-Container, View-Layer und Template-Engines sowie Error Handler unterstützt. Theoretisch könnte jeder PHP-Router, DI-Container sowie jede Template-Engine auf Basis von PHP Zend Expressive unterstützt werden. Wurden bei Veröffentlichung des 1.0-Releases „nur“ Pimple, Aura.DI und Zend\ServiceManager als DI-Container unterstützt, so wird mittlerweile auch an der Implementation weiterer DI-Container wie Symfony DI oder auryn gearbeitet. Somit ist Zend Expressive wohl das PHP-Microframework, das sich am meisten für Komponenten aus anderen Projekten und Frameworks geöffnet hat.

Was ist eine Middleware-Pipeline?

Eine einzelne Middleware hängt sich quasi zwischen den Request, der vom Client an den Server geschickt wird, und die Response, die vom Server an den Client zurückgeschickt wird. Theoretisch könnte in dieser einzelnen Middleware die komplette Verarbeitung der Anwendung erfolgen. Doch dann wäre der Vorteil des Middleware-Konzepts schnell dahin.

Stattdessen können mehrere Middleware-Komponenten in einer Pipeline hintereinander ausgeführt werden. Diese Middleware-Klassen erhalten Zugriff auf die Request- und Response-Objekte und können diese manipulieren. Middleware-Komponenten aus der Pipeline kümmern sich um verschiedene Aspekte wie das Routing, die Authentifizierung, die Internationalisierung oder das Dispatching. Dies hat den großen Vorteil, dass diese Middleware-Komponenten sehr einfach in verschiedenen Projekten eingesetzt werden können, was zu einer starken Modularisierung führen kann.

Die Installation von Zend Expressive kann relativ einfach mit dem Composer erfolgen (Kasten: „Was ist der Composer?“). Als Voraussetzung sollten Sie PHP 7 sowie den Composer installiert haben. Ein Anwendungsgerüst von Zend Expressive kann somit einfach installiert werden. Sie werden dabei durch einen interaktiven Installer geleitet:

$ composer create-project zendframework/zend-expressive-skeleton meine-neue-api

Wichtig: Bevor Sie dieses Anwendungsgerüst installieren, schauen Sie bitte in den nächsten Abschnitt und installieren gleich die Beispielanwendung. Wie die Installation mit dem Zend Expressive Installer aussieht, können Sie in Abbildung 1 sehen.

Abb. 1: Eine beispielhafte Installation mit dem Zend Expressive Installer

Abb. 1: Eine beispielhafte Installation mit dem Zend Expressive Installer

Was ist der Composer?

Der Composer ist ein Dependency-Manager für PHP und ermöglicht die einfache Installation von PHP-Paketen in ein vorhandenes Projekt. Zudem kümmert er sich um das Autoloading der Abhängigkeiten und kann auch für die Installation ganzer Projekte verwendet werden. Basis für die Konfiguration ist die Datei composer.json, die im Wurzelverzeichnis des PHP-Projekts liegen muss. Die Abhängigkeiten werden in der Regel in das Verzeichnis /vendor/ im Projekt installiert. Außerdem kann der Composer auch um weitere Skripte erweitert werden.

Beispielanwendung installieren

Die Beispielanwendung finden Sie auf GitHub in einem eigenen Repository [9]. Die Beispielanwendung entspricht der Installation des Zend Expressive Skeleton, wobei alle Fragmente für den Einsatz von Templates entfernt worden sind. Im API brauchen Sie diese genauso wenig wie Grafiken. Zudem wurden die Tests entfernt, da das Thema Unit-Tests den Rahmen dieses Artikels mehr als sprengen würde. Als Basis für ein neues API ist diese Beispielanwendung aber ideal. Die Installation kann in folgenden Schritten erfolgen:

$ git clone https://github.com/RalfEggert/entwicklermagazin.api
$ cd entwicklermagazin.api
$ composer install
$ composer development-enable
$ composer serve

Nun können Sie in Ihrem Browser das Projekt unter http://localhost:8080/ aufrufen. Dabei sollte in etwa folgender JSON-Code angezeigt werden:

{
  "welcome": "Herzlichen Glückwunsch! Die Installation hat geklappt.",
  "projectUrl": "https://github.com/RalfEggert/entwicklermagazin.api"
}

Es gibt auch noch einen weiteren Aufruf, den Sie mit http://localhost:8080/api/ping ausprobieren können. Dieser API-Request gibt einen aktuellen Zeitstempel zurück.

Im Browser führen Sie in der Regel durch den Aufruf der URLs nur GET Requests aus. Ein besserer Weg zum Testen des API ist es, wenn Sie einen REST-Client verwenden. Ein Beispiel dafür wäre Postman. Nach der Installation von Postman führen Sie einmal einen GET Request für den URL http://localhost:8080/ aus und schauen sich auch dort das Ergebnis an. Wechseln Sie dann zu einem POST Request und sehen Sie, dass der Body der Response leer ist. Stattdessen wird aber der Allow-Header zurückgegeben. Dieser zeigt an, dass für diesen URL nur GET Requests erlaubt sind. Zudem wird der Statuscode 405 Method Not Allowed zurückgegeben (Abb. 2). Übrigens gibt auch ein OPTIONS Request die erlaubte HTTP-Methode im Header zurück.

Abb. 2: Der REST-Client Postman gibt den Statuscode 405 zurück

Abb. 2: Der REST-Client Postman gibt den Statuscode 405 zurück

Ein erstes API mit Zend Expressive

Bevor Sie tiefer in ein echtes API einsteigen, implementieren Sie nun ein erstes eigenes API-Modul mit Zend Expressive. Dafür legen Sie zuerst das Modul selbst an. Dafür bietet Zend Expressive ein paar Kommandozeilentools an, die Sie sich hier zunutze machen können. Wenn Sie die Anwendung noch durch den Composer ausführen lassen, müssen Sie sie erst mit STRG + C abbrechen. Führen Sie dann im Projektverzeichnis dieses Kommando aus:

$ php vendor/bin/expressive module:create Check -p module/

Hierbei werden drei Schritte ausgeführt:

  1. Das Modulverzeichnis /module/Check/ wurde angelegt und die Klasse Check\ConfigProvider erstellt.
  2. Das Autoloading für das Check-Modul wurde in der composer.json für das Verzeichnis /module/Check/src/ eingerichtet.
  3. Der ConfigProvider für das Check-Modul wurde in der Anwendungskonfiguration unter /config/config.php geladen.

Die Aufgabe der ConfigProvider-Klasse ist es, die Konfiguration des Moduls für die Anwendung zu kapseln. Sie können dabei mit einer oder mehreren Konfigurationsdateien arbeiten oder die Konfiguration direkt in der Klasse vorhalten. Leider sind in der Klasse durch die automatische Codegenerierung Bereiche enthalten, die Sie für ein API nicht brauchen. Sie können die Klasse soweit kürzen, damit sie wie in Listing 1 aussieht, also alle Bezüge zu den Templates entfernen.

<?php
namespace Check;
class ConfigProvider
{
  public function __invoke()
  {
    return [
      'dependencies' => $this->getDependencies(),
    ];
  }

  public function getDependencies()
  {
    return [
      'factories'  => [
      ],
    ];
  }
}

Nun ist es an der Zeit, die erste Aktion zu implementieren. Dafür legen Sie das Verzeichnis /module/Check/src/Action/ an; darin wird die Datei CheckMethodAction.php erstellt. Diese Aktions-Middleware soll die Aufgabe übernehmen, die aktuelle HTTP-Methode zu prüfen und an den Aufrufer auszugeben. Um eine Middleware zu erstellen, müssen Sie das Interface Interop\Http\ServerMiddleware\MiddlewareInterface implementieren (Kasten: „Welches Middleware-Interface benutzen?“). Die Ausgabe in der zu implementierenden process()-Methode ist relativ einfach. Sie instanziieren eine Zend\Diactoros\Response\JsonResponse und übergeben die gewünschten Daten. Zugriff auf die HTTP-Methode erhalten Sie über das Request-Objekt. In Listing 2 sehen Sie die vollständige Klasse CheckMethodAction.

<?php

namespace Check\Action;

use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;

class CheckMethodAction implements MiddlewareInterface
{
  public function process(
    ServerRequestInterface $request,
    DelegateInterface $delegate
  ) {
    return new JsonResponse(
      [
        'check'  => 'Der Check der HTTP Methode war erfolgreich.',
        'method' => $request->getMethod(),
      ]
    );
  }
}

Welches Middleware-Interface benutzen?

Wenn Sie genauer hinschauen, werden bei der Entwicklung mit einer vernünftigen IDE wie z. B. PhpStorm mehrere Interfaces mit dem Namen MiddlewareInterface vorgeschlagen. Auf der einen Seite das Interface Zend\Stratigility\MiddlewareInterface und auf der anderen Seite das Interface Interop\Http\ServerMiddleware\MiddlewareInterface. Das Package Interop\Http\ServerMiddleware wird aktuell (Stand Ende Juli 2017) übergangsweise verwendet, weil der PSR-15 derzeit noch nicht komplett verabschiedet wurde. Sobald dies der Fall ist, wird das Package Psr\Http\ServerMiddleware bereitstehen, und Sie können die Implementation austauschen.

Damit implementieren Sie Ihre Middleware nach den empfohlenen Standards. Für das Interface Zend\Stratigility\MiddlewareInterface ist dies geplant. Aktuell wird noch der alte Weg von der Zend\Stratigility-Komponente beschritten.

Damit die Aktion ausgeführt werden kann, müssen Sie noch eine Route definieren. Zend Expressive bietet verschiedene Wege zur Definition von Routen. Sie können die Definitionen in einem Array vornehmen, einen programmatischen Ansatz oder einen objektorientierten wählen. Der programmatische und der objektorientierte Ansatz haben den Vorteil, dass Sie die Autovervollständigung Ihrer IDE nutzen können. Bei einem Array müssen Sie genau auf die Struktur achten. Beim programmatischen Ansatz lagern Sie die Routen in einer PHP-Datei aus und inkludieren sie an geeigneter Stelle. Beim objektorientierten Ansatz verwenden Sie eine entsprechende Factory, welche die Routendefinitionen kapselt. Ich persönlich favorisiere den objektorientierten Ansatz, da dieser am saubersten für modulare Anwendungen ist.

Bleibt noch die Frage, wo die Routen definiert werden. Auch dafür gibt es mehrere Ansätze, z. B. zentral im Konfigurationsverzeichnis der Anwendung oder modular in den jeweiligen Modulverzeichnissen. Je nach Komplexität der Anwendung ist der modulare Ansatz zu bevorzugen. Dies macht den Austausch und das Aktivieren von Modulen viel einfacher. Sie müssen dann nicht mehr manuell noch Routendefinitionen entfernen, sollten Sie ein Modul einmal deaktivieren wollen.

Um die Verwirrung nicht zu groß werden zu lassen, zeige ich Ihnen nun den objektorientierten Ansatz für eine modulare Struktur, da sich dieser in mittelgroßen bis sehr umfangreichen Anwendungen bewährt hat. Dafür wird im anzulegenden Verzeichnis /module/Check/src/Router/ die Datei RouterDelegatorFactory.php angelegt. Eine Delegator Factory ist eine Factory, an die die Ausführung weitergeleitet wird, nachdem die eigentliche Factory zur Instanziierung des Objekts ausgeführt worden ist. In unserem Fall wird zuerst die Factory für Zend\Expressive\Application ausgeführt und danach dann diese neue RouterDelegatorFactory. Somit können Sie die Routendefinitionen der Anwendung problemlos erweitern. Schauen Sie sich einmal den Code in Listing 3 genauer an.

<?php

namespace Check\Router;

use Check\Action\CheckMethodAction;
use Interop\Container\ContainerInterface;
use Zend\Expressive\Application;
use Zend\ServiceManager\Factory\DelegatorFactoryInterface;

class RouterDelegatorFactory implements DelegatorFactoryInterface
{
  public function __invoke(
    ContainerInterface $container,
    $name,
    callable $callback,
    array $options = null
  ) {
    /** @var Application $app */
    $app = $callback();

    $app->route(
      '/check/method',
      CheckMethodAction::class,
      ['get', 'post', 'put', 'delete', 'patch'],
      'check.method'
    );

    return $app;
  }
}

Über den Aufruf der injizierten $callback-Methode stellen Sie sicher, dass Sie die Zend\Expressive\Application-Instanz erhalten. Dann fügen Sie die neue Route mit der Methode route() hinzu. Dabei werden die folgenden vier Parameter übergeben:

  • der Pfad der Route
  • die Middleware-Klasse für die auszuführende Aktion
  • die HTTP-Methoden, für die die Route gültig sein soll
  • der Name der Route

Am Ende des Listings wird das durch diese Route erweiterte Application-Objekt zurückgegeben. Jetzt können theoretisch weitere Delegator Factories aus anderen Modulen weitere Routen zur Anwendung hinzufügen.

Bevor Sie die neue Route testen können, müssen Sie sich noch um die Konfiguration kümmern. Dabei müssen die neue Aktions-Middleware und die Delegator Factory definiert werden, damit die Zend-Expressive-Anwendung sie auch kennt. Die Definition erfolgt in der Check\ConfigProvider-Klasse, die Sie im ersten Schritt überarbeitet hatten. Diese müssen Sie nun so anpassen, wie in Listing 4 gezeigt. Darin wird sowohl die Delegator Factory RouterDelegatorFactory als auch die Aktions-Middleware CheckMethodAction konfiguriert.

<?php

namespace Check;

use Check\Action\CheckMethodAction;
use Check\Router\RouterDelegatorFactory;
use Zend\Expressive\Application;
use Zend\ServiceManager\Factory\InvokableFactory;

class ConfigProvider
{
  public function __invoke()
  {
    return [
      'dependencies' => $this->getDependencies(),
    ];
  }

  public function getDependencies()
  {
    return [
      'delegators' => [
        Application::class => [
          RouterDelegatorFactory::class,
        ],
      ],
      'factories'  => [
        CheckMethodAction::class => InvokableFactory::class,
      ],
    ];
  }
}

Wenn Sie die Änderungen nicht gemacht haben, können Sie auch auf den Branch step1 wechseln. Dieser enthält alle beschriebenen Änderungen:

$ git checkout step1

Nun kann die neue Route in Postman getestet werden. Verschicken Sie einen GET Request für den URL http://localhost:8080/check/method und schauen Sie sich die Rückgabe genauer an. Die HTTP-Methode sollte ausgegeben werden und der Statuscode sollte 200 sein. Wiederholen Sie den Aufruf dann mit einer der anderen HTTP-Methoden, die im Routing konfiguriert worden sind, also POST, PUT, DELETE oder PATCH. Die Response sollte dann die entsprechende HTTP-Methode auch anzeigen und der Statuscode bei 200 bleiben. Probieren Sie danach auch eine nicht konfigurierte HTTP-Methode wie COPY aus. Dann sollte der Statuscode bei 405 liegen und zudem sollten im Allow-Header die erlaubten Methoden angegeben werden. Zu guter Letzt probieren Sie auch noch einen OPTION Request aus und betrachten Rückgabe, Header und Statuscode.

Ein echtes API mit Zend Expressive einrichten

Bisher war das API nur eher zum Kennenlernen gedacht. Als Nächstes geht es darum, ein echtes API einzurichten, das Daten auslesen und verändern kann. Damit wollen wir ein REST-API für Kundendaten aufbauen. Damit Sie nicht alles komplett selbst vorbereiten müssen, wechseln Sie nun auf den Branch step2, in dem einige Änderungen für Sie vorbereitet wurden:

$ git checkout step2
$ composer update

Nach dem Check-out und Composer-Update sollten Sie folgende Dinge beachten:

  • Legen Sie eine MySQL-Datenbank an. In der Datei /data/database.sql finden Sie einen entsprechenden Dump, der die Tabelle entwicklermagazin.api, den Nutzer em.api mit dem Passwort em.api und eine Tabelle customer mit einigen Daten einrichtet. Sollte der Dump bei Ihnen nicht fehlerfrei durchlaufen, richten Sie die Datenbank, den Nutzer und die Tabelle manuell ein.
  • Durch den Check-out wurde Doctrine in Ihrem Projekt installiert und vorkonfiguriert. Wenn Sie nicht genau wissen, was Doctrine ist, schauen Sie in den Kasten „Was ist Doctrine?“.
  • Zudem wurde ein neues Modul Customer eingerichtet, das bisher nur einen neuen Customer/ConfigProvider sowie eine Entitätsklasse Customer\Entity\Customer für die Kunden bereitstellt. Außerdem wurde eine RouterDelegatorFactory eingerichtet und vorkonfiguriert.

Was ist Doctrine?

Doctrine ist ein PHP-basiertes Open-Source-Projekt, das neben einem Datenbankabstraktionslayer auch einen objektrelationalen Mapper (ORM) mit sich bringt. Ein ORM bildet eine relationale Datenbankstruktur auf eine Objektstruktur ab. Doctrine nimmt Ihnen dabei viel Arbeit ab, sodass Sie damit schnelle Ergebnisse erzielen können.

Nach diesen Vorbereitungen können Sie das REST-API einrichten. Zum Einstieg geht es darum, den GET Request zur Rückgabe einer Liste mit allen Kunden zu implementieren. Dafür brauchen Sie zuerst natürlich eine Aktions-Middleware. Im noch anzulegenden Verzeichnis /module/Customer/src/Action/ legen Sie dafür die Klasse Customer\Action\GetCollectionAction an. Der Zugriff auf die Kundendaten erfolgt über den Entity-Manager von Doctrine, weshalb diese Instanz über den Konstruktor in die Klasse injiziert werden muss. In der process()-Methode der Middleware erfolgt der Zugriff auf das Repository, und über dessen findAll()-Methode können alle Daten der Kunden angefordert werden. Diese Kundendaten werden an die JsonResponse-Instanz übergeben. Den vollständigen Code finden Sie in Listing 5.

<?php

namespace Customer\Action;

use Customer\Entity\Customer;
use Doctrine\ORM\EntityManager;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;

class GetCollectionAction implements MiddlewareInterface
{
  /** @var  EntityManager */
  private $entityManager;

  public function __construct(EntityManager $entityManager)
  {
    $this->entityManager = $entityManager;
  }

  public function process(
    ServerRequestInterface $request,
    DelegateInterface $delegate
  ) {
    $customerRepository = $this->entityManager->getRepository(Customer::class);

    return new JsonResponse(
      [
        'data' => $customerRepository->findAll(),
      ]
    );
  }
}

Um die Injizierung des Entity-Managers soll sich eine spezielle Factory kümmern. Dafür wird die Klasse Customer\Action\GetCollectionFactory erstellt. Diese Factory fordert den Entity-Manager vom DI-Container (in diesem Fall der Zend\ServiceManager) an und übergibt ihn an den Konstruktor der Customer\Action\GetCollectionAction Klasse. Listing 6 zeigt die Klasse GetCollectionFactory.

<?php

namespace Customer\Action;

use Doctrine\ORM\EntityManager;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;

class GetCollectionFactory implements FactoryInterface
{
  public function __invoke(
    ContainerInterface $container,
    $requestedName,
    array $options = null
  ) {
    /** @var EntityManager $entityManager */
    $entityManager = $container->get('doctrine.entity_manager.orm_default');

    return new GetCollectionAction($entityManager);
  }
}

Nun müssen Sie dem DI-Container mitteilen, dass die GetCollectionAction durch die Factory GetCollectionFactory instanziiert werden soll. Dies erfolgt wie bei dem einfachen API im Config-Provider des Moduls. In Listing 7 sehen Sie die erforderlichen Änderungen, um die neue Aktions-Middleware zu konfigurieren.

<?php

namespace Customer;

use Customer\Action\AbstractActionFactory;
use Customer\Action\GetCollectionAction;

class ConfigProvider
{
  /* ... */

  public function getDependencies()
  {
    return [
      /* ... */

      'factories'  => [
        GetCollectionAction::class => GetCollectionFactory::class,
      ],
    ];
  }
}

Im letzten Schritt müssen Sie die Route konfigurieren. Dafür steht die bereits vorbereitete RouterDelegatorFactory aus dem Customer-Modul bereit. Da die GetCollectionAction nur für den GET Request ausgeführt werden soll, kann die Route über die get()-Methode der Anwendung bekannt gemacht werden. Dadurch kann der dritte Parameter mit den als Array definierten HTTP-Methoden ausgelassen werden. Den genauen Aufruf sehen Sie in Listing 8.

<?php

namespace Customer\Router;

use Customer\Action\GetCollectionAction;
use Customer\Action\GetEntityAction;
use Interop\Container\ContainerInterface;
use Zend\Expressive\Application;
use Zend\ServiceManager\Factory\DelegatorFactoryInterface;

class RouterDelegatorFactory implements DelegatorFactoryInterface
{
  public function __invoke(
    ContainerInterface $container,
    $name,
    callable $callback,
    array $options = null
  ) {
    /** @var Application $app */
    $app = $callback();

    $app->get('/customer', GetCollectionAction::class, 'customer-get-collection');

    return $app;
  }
}

Jetzt können Sie den neuen URL http://localhost:8080/customer im Postman als GET Request testen. Sie sollten die Daten aller Kunden ausgegeben bekommen wie es Abbildung 3 in etwa zeigt. Wenn Sie eine andere HTTP-Methode verwenden, sollten Sie wieder eine Response mit dem 405-Statuscode erhalten. Damit haben Sie den ersten kleinen Teil Ihres REST-API abgeschlossen.

Abb. 3: Postman gibt nach dem „GET“ Request die Daten aller Kunden aus

Abb. 3: Postman gibt nach dem „GET“ Request die Daten aller Kunden aus

Als Nächstes muss der GET Request für die Rückgabe eines einzelnen Kunden implementiert werden. Dafür brauchen Sie ebenfalls eine Aktions-Middleware, eine Factory, die Konfiguration und eine Route. Die Aktions-Middleware Customer\Action\GetEntityAction liegt im selben Verzeichnis wie die andere Aktions-Middleware. Sie sieht der vorherigen auch sehr ähnlich. Wichtigster Unterschied ist, dass Sie den id-Parameter für den Kunden aus dem Request-Objekt anfordern müssen, um dann aus dem Repository den einzelnen Kunden per find()-Methode abrufen zu können. Dabei wird auch noch ein Fehler abgefangen, falls kein Kunde für eine id gefunden werden kann. Listing 9 zeigt die vollständige GetEntityAction-Klasse.

<?php

namespace Customer\Action;

use Customer\Entity\Customer;
use Doctrine\ORM\EntityManager;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;

class GetEntityAction implements MiddlewareInterface
{
  /** @var  EntityManager */
  private $entityManager;

  public function __construct(EntityManager $entityManager)
  {
    $this->entityManager = $entityManager;
  }

  public function process(
    ServerRequestInterface $request,
    DelegateInterface $delegate
  ) {
    $customerRepository = $this->entityManager->getRepository(Customer::class);

    $id = $request->getAttribute('id');

    /** @var Customer $customer */
    $customer = $customerRepository->find($id);

    if (!$customer) {
      return new JsonResponse([], 404);
    }

    return new JsonResponse(['entity' => $customer]);
  }
}

Bei der Factory für diese neue Aktions-Middleware sähe die Factory nahezu identisch aus, denn auch hier müsste der Entity-Manager injiziert werden. Um am Ende nicht eine Reihe von sehr ähnlichen Factories erstellen zu müssen, benennen Sie nun die Klasse Customer\Action\GetCollectionFactory in Customer\Action\AbstractActionFactory um und passen auch den Dateinamen entsprechend an. Da an die __invoke()-Methode der Factory immer auch der Name der angeforderten Klasse übergeben wird, können Sie sich dies nun zunutze machen. Die vollständige AbstractActionFactory sehen Sie in Listing 10. Der Vorteil ist, dass Sie nun beide Aktions-Middlewares mit derselben Factory initialisieren lassen können.

<?php

namespace Customer\Action;

use Doctrine\ORM\EntityManager;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;

class AbstractActionFactory implements FactoryInterface
{
  public function __invoke(
    ContainerInterface $container,
    $requestedName,
    array $options = null
  ) {
    /** @var EntityManager $entityManager */
    $entityManager = $container->get('doctrine.entity_manager.orm_default');

    return new $requestedName($entityManager);
  }
}

Selbstverständlich muss die Konfiguration im Config-Provider des Customer-Moduls nun angepasst werden. Beide bisher vorhandene Aktions-Middlewares sollen durch die AbstractActionFactory initialisiert werden (Listing 11).

<?php

namespace Customer;

use Customer\Action\AbstractActionFactory;
use Customer\Action\GetCollectionAction;

class ConfigProvider
{
  /* ... */

  public function getDependencies()
  {
    return [
      /* ... */

      'factories'  => [
        GetCollectionAction::class => AbstractActionFactory::class,
        GetEntityAction::class     => AbstractActionFactory::class,
      ],
    ];
  }
}

Zum Abschluss richten Sie für die neue Aktion eine weitere Route in der RouterDelegatorFactory des Customer-Moduls ein. Das Besondere hierbei ist, dass Sie im Pfad der Route mit einem Platzhalter für den id-Parameter arbeiten müssen. Da in diesem Projekt FastRoute als Router verwendet wird, sind die einzelnen Routenparameter in geschweifte Klammern zu setzen. Zudem sollten Sie eine Bedingung für den Parameter definieren, sodass nur Ziffern als id erlaubt sind. Die vollständige Route entnehmen Sie Listing 12.

<?php

namespace Customer\Router;

use Customer\Action\GetCollectionAction;
use Customer\Action\GetEntityAction;
use Interop\Container\ContainerInterface;
use Zend\Expressive\Application;
use Zend\ServiceManager\Factory\DelegatorFactoryInterface;

class RouterDelegatorFactory implements DelegatorFactoryInterface
{
  public function __invoke(
    ContainerInterface $container,
    $name,
    callable $callback,
    array $options = null
  ) {
    /** @var Application $app */
    $app = $callback();

    $idConstraint = ['constraints' => ['id' => '[0-9]+']];

    $app->get('/customer', GetCollectionAction::class, 'customer-get-collection');
    $app->get('/customer/{id}', GetEntityAction::class, 'customer-get-entity')->setOptions($idConstraint);

    return $app;
  }
}

Wenn Sie nun in Postman den URL http://localhost:8080/customer/1 als GET Request versenden, sollte Ihnen der Datensatz für die ID 1 angezeigt werden. Sie können auch die ID ändern, um zu sehen, wie weitere Datensätze abgefragt werden können. Damit sind die wichtigen GET Requests eines REST-API bereits implementiert.

POST, PUT und DELETE implementieren

Um das REST-API zu vervollständigen, müssen Sie auch neue Kunden anlegen sowie bestehende ändern und löschen können. Um das Ganze etwas übersichtlicher anzugehen, werden zuerst die drei erforderlichen Aktions-Middlewares betrachtet und danach folgt erst die Konfiguration für den DI-Container und die Routen.

Die Aktions-Middleware zum Anlegen neuer Kunden ist die Customer\Action\PostEntityAction, die in Listing 13 zu sehen ist. Hier müssen die übermittelten Daten zuerst aus dem Request Body geholt und aufbereitet werden, d. h. der JSON-String wird in ein Array umgewandelt. Danach kann eine neue Customer-Instanz angelegt werden, wobei die Werte aus dem POST Request übergeben werden – mit Ausnahme einer ID, weil diese unbekannt ist. Selbstverständlich können Sie diese Stelle auch anpassen, wenn Sie gezielt neue Datensätze mit fest vorgegebener ID anlegen lassen möchten. Mithilfe des Entity-Managers von Doctrine wird der neue Kunde persistiert, und es wird eine JsonResponse mit den Daten und einem 201 Created-Statuscode zurückgesandt.

<?php

namespace Customer\Action;

use Customer\Entity\Customer;
use Doctrine\ORM\EntityManager;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;

class PostEntityAction implements MiddlewareInterface
{
  /** @var  EntityManager */
  private $entityManager;

  public function __construct(EntityManager $entityManager)
  {
    $this->entityManager = $entityManager;
  }

  public function process(
    ServerRequestInterface $request,
    DelegateInterface $delegate
  ) {
    $postData = (array)json_decode($request->getBody()->getContents());

    $customer = new Customer(
      null,
      $postData['first_name'],
      $postData['last_name'],
      $postData['country']
    );

    $this->entityManager->persist($customer);
    $this->entityManager->flush();

    return new JsonResponse(['entity' => $customer], 201);
  }
}

Die Verarbeitung des PUT Requests wird durch die Aktions-Middleware Customer\Action\PutEntityAction ausgeführt (Listing 14). Im Gegensatz zu POST, muss hier zuerst mithilfe des Kunden-Repositorys der zu überschreibende Datensatz gelesen werden. Kann dieser nicht gefunden werden, wird eine JsonResponse ohne Body, aber mit einem 404 Not Found-Statuscode zurückgesandt. Wurde der Kunde gefunden, dann können die Daten durch die update()-Methode der Entität aktualisiert werden. Danach kümmert sich der Entity-Manager von Doctrine wieder um die Persistierung.

<?php

namespace Customer\Action;

use Customer\Entity\Customer;
use Doctrine\ORM\EntityManager;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;

class PutEntityAction implements MiddlewareInterface
{
  /** @var  EntityManager */
  private $entityManager;

  public function __construct(EntityManager $entityManager)
  {
    $this->entityManager = $entityManager;
  }

  public function process(
    ServerRequestInterface $request,
    DelegateInterface $delegate
  ) {
    $customerRepository = $this->entityManager->getRepository(Customer::class);

    $id = $request->getAttribute('id');

    $putData = (array) json_decode($request->getBody()->getContents());

    /** @var Customer $customer */
    $customer = $customerRepository->find($id);

    if (!$customer) {
      return new JsonResponse([], 404);
    }

    $customer->update(
      $putData['first_name'],
      $putData['last_name'],
      $putData['country']
    );

    $this->entityManager->persist($customer);
    $this->entityManager->flush();

    return new JsonResponse(['entity' => $customer]);
  }
}

Listing 15 zeigt die Aktions-Middleware Customer\Action\DeleteEntityAction, die den DELETE Request verarbeiten soll. Auch hier wird zuerst geprüft, ob das Kunden-Repository den zu ändernden Datensatz findet. Im Fehlerfall wird wieder eine JsonResponse ohne Body und mit 404 Not Found-Statuscode zurückgesandt. Im Erfolgsfall wird der Kunde aus der Datenbank gelöscht, und es wird eine JsonResponse ohne Body und mit 204 No Content-Statuscode als Antwort an den Client geschickt.

<?php

namespace Customer\Action;

use Customer\Entity\Customer;
use Doctrine\ORM\EntityManager;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;

class DeleteEntityAction implements MiddlewareInterface
{
  /** @var  EntityManager */
  private $entityManager;

  public function __construct(EntityManager $entityManager)
  {
    $this->entityManager = $entityManager;
  }

  public function process(
    ServerRequestInterface $request,
    DelegateInterface $delegate
  ) {
    $customerRepository = $this->entityManager->getRepository(Customer::class);

    $id = $request->getAttribute('id');

    /** @var Customer $customer */
    $customer = $customerRepository->find($id);

    if (!$customer) {
      return new JsonResponse([], 404);
    }

    $this->entityManager->remove($customer);
    $this->entityManager->flush();

    return new JsonResponse([], 204);
  }
}

Als Nächstes können Sie die drei neuen Aktions-Middleware-Klassen für den DI-Container konfigurieren. Listing 16 zeigt den vollständigen Config-Provider für das Customer-Modul. Spätestens hier zahlt sich die allgemein einsetzbare Customer\Action\AbstractActionFactory aus, da sie für alle fünf Aktionen verwendet werden kann.

<?php

namespace Customer;

use Customer\Action\AbstractActionFactory;
use Customer\Action\DeleteEntityAction;
use Customer\Action\GetCollectionAction;
use Customer\Action\GetEntityAction;
use Customer\Action\PostEntityAction;
use Customer\Action\PutEntityAction;
use Customer\Router\RouterDelegatorFactory;
use Zend\Expressive\Application;

class ConfigProvider
{
  public function __invoke()
  {
    return [
      'dependencies' => $this->getDependencies(),
    ];
  }

  public function getDependencies()
  {
    return [
      'delegators' => [
        Application::class => [
         RouterDelegatorFactory::class,
        ],
      ],
      'factories'  => [
        GetCollectionAction::class => AbstractActionFactory::class,
        GetEntityAction::class     => AbstractActionFactory::class,
        PostEntityAction::class    => AbstractActionFactory::class,
        PutEntityAction::class     => AbstractActionFactory::class,
        DeleteEntityAction::class  => AbstractActionFactory::class,
      ],
    ];
  }
}

Bleibt nun noch das Routing für die POST-, PUT– und DELETE Requests übrig. Hier können Sie die vollständige Customer\Router\RouterDelegatorFactory in Listing 17 einsehen. Die Konfiguration sollte nahezu selbsterklärend sein. Für jede HTTP-Methode gibt es eine eigene Route, wobei hier die Pfade der beiden GET Requests wiederverwendet werden, aber jeweils auf eine andere Aktions-Middleware umgeleitet wird und jede Route auch einen eigenen Namen zugewiesen bekommt.

<?php

namespace Customer\Router;

use Customer\Action\DeleteEntityAction;
use Customer\Action\GetCollectionAction;
use Customer\Action\GetEntityAction;
use Customer\Action\PostEntityAction;
use Customer\Action\PutEntityAction;
use Interop\Container\ContainerInterface;
use Zend\Expressive\Application;
use Zend\ServiceManager\Factory\DelegatorFactoryInterface;

class RouterDelegatorFactory implements DelegatorFactoryInterface
{
  public function __invoke(
    ContainerInterface $container,
    $name,
    callable $callback,
    array $options = null
  ) {
    /** @var Application $app */
    $app = $callback();

    $idConstraint = ['constraints' => ['id' => '[0-9]+']];

    $app->get('/customer', GetCollectionAction::class, 'customer-get-collection');
    $app->get('/customer/{id}', GetEntityAction::class, 'customer-get-entity')->setOptions($idConstraint);
    $app->post('/customer', PostEntityAction::class, 'customer-post-entity');
    $app->put('/customer/{id}', PutEntityAction::class, 'customer-put-entity')->setOptions($idConstraint);
    $app->delete('/customer/{id}', DeleteEntityAction::class, 'customer-delete-entity')->setOptions($idConstraint);

    return $app;
  }
}

Nun können Sie das Anlegen, Ändern und Löschen von Kunden mit Ihrem neuen API testen. Bei einem POST Request wird der URL http://localhost:8080/customer verwendet und die zu übermittelnden Daten werden im JSON-Format in den Body des Requests geschrieben. Nach dem Absenden erhalten Sie die vollständigen Kundendaten inklusive der neuen ID zurück. Achten Sie auch auf den Status 201. Abbildung 4 zeigt diesen POST Request mit Postman.

Abb. 4: Der „POST“ Request in Postman

Abb. 4: Der „POST“ Request in Postman

Ein PUT Request ist übrigens sehr ähnlich aufgebaut. Der URL lautet dabei jedoch http://localhost:8080/customer/8, um den Kunden mit der ID 8 zu verändern. Außerdem wird die HTTP-Methode selbstverständlich auf PUT statt auf POST gesetzt. Ansonsten ist für den Nutzer des API der Aufbau ähnlich. Bei einem DELETE Request wird es noch einfacher, da hier nur der URL http://localhost:8080/customer/8 und die DELETE-HTTP-Methode eingestellt werden muss.

Sollten Sie nicht alle Änderungen erfolgreich ausgeführt haben, können Sie auch auf den Branch step3 wechseln, der das vollständige API für die Kundendaten enthält:

$ git checkout step3
$ composer update

Probieren Sie das neue REST-API einmal aus und legen Sie neue Kunden an. Ändern Sie diese auch wieder und löschen Sie einige der Kunden. Die aktuellen Kundendaten können Sie sich ja jederzeit über einen GET Request anzeigen lassen. Natürlich können Sie das API noch erweitern, aber die Grundprinzipien sollten hoffentlich klar sein.

Sollten Sie Probleme mit dem Code haben oder die Beispielanwendung bei Ihnen nicht laufen, so nutzen Sie den Issue Tracker im GitHub Repository.

Middleware-Pipeline

Eine Besonderheit stellt die Middleware-Pipeline da. Sie sorgt für den gesamten Ablauf der Anwendung und ist im App-Modul in der Klasse App\Pipeline\PipelineDelegatorFactory zu sehen. Auch hier handelt es sich um eine Delegator Factory, welche die Zend-Expressive-Anwendung erweitert. In Listing 18 können Sie einen Blick darauf werfen. Die einzelnen Middlewares in der Pipeline werden nacheinander ausgeführt, und jede Middleware reicht die Verarbeitung an die nächste weiter oder kann die Verarbeitung auch abbrechen.

<?php

namespace App\Pipeline;

use Interop\Container\ContainerInterface;
use Zend\Expressive\Application;
use Zend\Expressive\Helper\ServerUrlMiddleware;
use Zend\Expressive\Helper\UrlHelperMiddleware;
use Zend\Expressive\Middleware\ImplicitHeadMiddleware;
use Zend\Expressive\Middleware\ImplicitOptionsMiddleware;
use Zend\Expressive\Middleware\NotFoundHandler;
use Zend\ServiceManager\Factory\DelegatorFactoryInterface;
use Zend\Stratigility\Middleware\ErrorHandler;

class PipelineDelegatorFactory implements DelegatorFactoryInterface
{
  public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null)
  {
    /** @var Application $app */
    $app = $callback();

    $app->pipe(ErrorHandler::class);
    $app->pipe(ServerUrlMiddleware::class);

    $app->pipeRoutingMiddleware();
    $app->pipe(ImplicitHeadMiddleware::class);
    $app->pipe(ImplicitOptionsMiddleware::class);
    $app->pipe(UrlHelperMiddleware::class);

    $app->pipeDispatchMiddleware();

    $app->pipe(NotFoundHandler::class);

    return $app;
  }
}

Im Einzelnen werden unter anderem folgende Middleware-Klassen in die Pipeline integriert:

  • ein ErrorHandler, um Fehler abzufangen
  • eine Routing-Middleware, die das Routing ausführt und dabei ermittelt, welche Aktions-Middleware später ausgeführt werden soll
  • eine ImplicitHeadMiddleware, die HEAD Requests verarbeiten kann, die nicht explizit durch das Routing verarbeitet werden sollen
  • die ImplicitOptionsMiddleware macht dasselbe für OPTIONS Requests
  • eine Dispatching-Middleware, die die durch das Routing ermittelte Aktions-Middleware ausführt
  • ein NotFoundHandler, um Anfragen abzufangen, für die keine Route und damit keine Aktions-Middleware ermittelt werden konnte

Sie können die Middleware-Pipeline selbstverständlich auch erweitern und damit gezielt in die Verarbeitung eingreifen. Sinnvolle Anwendungsfälle wären hier unter anderem:

  • eine Authentifizierungs-Middleware, die sich um die Authentifizierung des Nutzers kümmert
  • eine Autorisierungs-Middleware, die für den aktuellen Nutzer prüft, ob er autorisiert ist
  • eine Internationalisierungs-Middleware, die die Sprache und Lokalisierung konfiguriert

Ein detailliertes Beispiel würde an dieser Stelle den Rahmen sprengen. Ein Beispiel finden Sie aber in der Dokumentation.

Fazit

Sie haben in diesem Artikel die Grundlagen einer Zend-Expressive-Anwendung kennengelernt und danach eine erste Beispielanwendung installiert. Als Nächstes haben Sie ein erstes kleines API aufgebaut, dessen Funktionalität aber überschaubar war. Dennoch haben Sie die wichtigsten Aspekte für den Aufbau eines APIs erfahren. Ins Eingemachte ging es dann mit einem konkreten Beispiel für ein REST-API für Kundendaten. Dabei kam auch Doctrine für die Anbindung an eine Datenbank zum Einsatz.

Zend Expressive bringt von Haus aus alles mit, um leichtgewichtige APIs mit PHP aufzubauen. Das Routing und die direkte Verbindung zu den Aktions-Middleware-Klassen ist sehr intuitiv. Zudem setzt Zend Expressive auf PHP-Standards und unterstützt zudem verschiedene Router und DI-Container. Es spricht also nichts dagegen, für den Aufbau eines API einmal zu PHP und zu Zend Expressive zu greifen. Sobald das eingangs erwähnte Apigility-2-Projekt weiter voranschreitet, werden Sie auch weitere Module für die OAuth2- oder HAL-JSON-Unterstützung in Ihrer Anwendung einsetzen können.

Entwickler Magazin

Entwickler Magazin abonnierenDieser Artikel ist im Entwickler Magazin erschienen.

Natürlich können Sie das Entwickler Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

1 Kommentar auf "Alles an Bord: REST-APIs mit Zend aufbauen"

avatar
400
  Subscribe  
Benachrichtige mich zu:
rossi
Gast

Gibts so ne Anleitung auch für Symfony und oder Laravel ???

X
- Gib Deinen Standort ein -
- or -