Anzeige
Anzeige
UX & Design
Artikel merken

RESTful entwickeln mit TYPO3 Flow: Der Weg zum eigenen Webservice

Webservices gehören zu den wichtigsten Bestandteilen des WWW, denn sie ermöglichen die Kommunikation von Anwendungen untereinander. In den letzten Jahren hat sich der „Representational State Transfer“ als Standard-Architekturstil dafür etabliert. Dieser Artikel zeigt, wie sich standardkonforme REST-Webservices auf Grundlage von TYPO3 Flow entwickeln lassen.

7 Min. Lesezeit
Anzeige
Anzeige

(Screenshot: commons.wikimedia.org)

Webservices sind ein essenzieller Bestandteil des Webs. Sie sorgen dafür, dass Anwendungen nicht nur von menschlichen Nutzern, sondern auch von anderen Applikationen genutzt werden können. Nahezu alle bekannten Betreiber bieten solche Schnittstellen an, sodass Anwendungsentwickler zum Beispiel die Dienste von Twitter oder Facebook direkt in eigene Anwendungen integrieren können. Lange Jahre waren XML-basierte Technologien wie etwa XML-RPC oder SOAP das Mittel der Wahl, um solche Webservices zu implementieren. Diese haben jedoch den Nachteil, dass sie ineffizient und übermäßig komplex sind. Speziell für SOAP existieren beispielsweise einige hundert Zusatz-Standards, über die niemand mehr einen richtigen Überblick hat.

Anzeige
Anzeige

Als „einfachere“ Alternative hat sich in den letzten Jahren der REST-Architekturstil etabliert. Er besticht durch seine Einfachheit und harmonisiert gut mit den ursprünglichen Architekturprinzipien des WWW. Anstatt wie SOAP das Rad in immer neuen Spezifikationen neu zu erfinden, nutzen REST-Webservices geschickt die Funktionen des gewählten Übertragungsprotokolls aus (meist HTTP). Sinnvolle Funktionen wie Caching und Skalierbarkeit gibt es dabei quasi gratis dazu.

Architekturprinzipien

Der Begriff des Representational State Transfer (kurz REST) – ursprünglich 2000 von Roy Fielding geprägt – beschreibt einen Architekturstil für verteilte Anwendungen. Dieser ist in der Theorie unabhängig von konkreten Übertragungsprotokollen. In der Regel wird allerdings HTTP als Protokoll genutzt.

Anzeige
Anzeige

Grundlegende Bausteine eines REST-Services sind die Ressourcen, also die von einer Anwendung modellierten Geschäftsobjekte. Jede davon muss laut Fielding die folgenden Bedingungen erfüllen:

Anzeige
Anzeige
  • Eindeutige Adressierbarkeit über einen Unique Resource Identifier (URI) – ein Produkt in einem Warenwirtschaftssystem wäre etwa über die URI http://ihre.url/products/12345 eindeutig identifizierbar.
  • Der Zugriff auf Ressourcen ist zustandslos. Dies bedeutet, dass bei jeder Anfrage alle Informationen mitzuschicken sind, die der Server zur Verarbeitung der Anfrage benötigt. Ein Beispiel dafür sind Authentifizierungs-Daten. Benutzersitzungen, etwa in Form von Sessions und Cookies, sind nicht vorgesehen.
  • Alle Ressourcen müssen den Zugriff über einen einheitlichen Satz von Operationen erlauben. HTTP bietet hierzu (unter anderem) die Methoden GET, POST, PUT und DELETE an.
  • Jede Ressource kann verschiedene Repräsentationen haben (beispielsweise als JSON, XML und HTML). Clients können explizit bestimmte Repräsentationsformen anfordern.
Für den Einsatz von REST wird häufig auf HTTP als Übertragungsprotokoll gesetzt. Hierfür stehen die HTTP-Methoden GET, POST, PUT und DELETE zur Verfügung. (Grafik: Martin Helmich)

Für den Einsatz von REST wird häufig auf HTTP als Übertragungsprotokoll gesetzt. Hierfür stehen die HTTP-Methoden GET, POST, PUT und DELETE zur Verfügung. (Grafik: Martin Helmich)

RESTful HTTP mit TYPO3 Flow

RestController und JsonView

Seit TYPO3 Flow 2.3 lassen sich REST-Webservices über einen herkömmlichen ActionController abbilden. Um Konfigurationsarbeit zu sparen, kann auch vom RestController geerbt werden. Der einzige Trick besteht darin, die einzelnen Action-Methoden nicht über verschiedene URLs, sondern über verschiedene HTTP-Methoden anzusprechen. Dies kann später über das Routing erfolgen, beispielsweise nach dem Schema in der folgenden Tabelle:

HTTP-Verb Action-Methode
GET listAction()
showAction($resource)
POST createAction($resource)
PUT updateAction($resource)
DELETE deleteAction($resource)

Um maschinenlesbare Antworten ausgeben zu können, setzt Flow außerdem auf die JsonView-Klasse, die PHP-Objekte ins JSON-Format übersetzt. Die JSON-Repräsentation wird dabei aus den aufrufbaren get…()-Methoden einer Klasse generiert.

Anzeige
Anzeige

Listing 1 zeigt als Beispiel einen einfachen Controller zur Verwaltung von Produkten. Dieser kommt bisher nicht ganz ohne eigene Konfiguration aus. Über das Attribut $supportedMediaTypes muss explizit definiert werden, dass der Controller JSON als Inhaltstyp unterstützt (1). Das Attribut $viewFormatToObjectNameMap übernimmt anschließend die Verknüpfung des JSON-Formats mit der JsonView-Klasse (2).

Einfacher Controller zur Verwaltung von Produkten

class ProductController extends RestController {

	/** @var ProductRepository
	*  @Flow\Inject */
	protected $productRepository;
	
	protected $supportedMediaTypes = ['application/json']; /* (1) */
	
	protected $viewFormatToObjectNameMap = [
		'json' => \TYPO3\Flow\Mvc\View\JsonView::class]; /* (2) */
		
	public function listAction() {
		$this->view->assign('products', $this->productRepository->findAll());
	
	public function showAction(Product $product) {
		$this->view->assign('product', $product);
	}

	public function createAction(Product $product) {
		$this->productRepository->add($product);
		$this->response->setStatus(201);
	}

	public function updateAction(Product $product) {
		$this->productRepository->update($product);
	}
	
	public function deleteAction(Product $product) {
		$this->productRepository->remove($product);
	}
}

Listing 1

Routing

Damit Flow die entsprechenden Action-Methoden korrekt aufruft, benötigt das Routing eine entsprechende Konfiguration. Listing 2 zeigt dafür ein Beispiel. Das uriPattern variiert nur geringfügig. Die Routen werden hauptsächlich über die HTTP-Methode differenziert.

Routing

- name: Product API (list)
  uriPattern: products
  defaults:
    @package: Helmich.RestExample
	@controller: Product
	@action: index
	@format: json
  httpMethods: [GET]
  
- name: Product API (create)
  uriPattern: products
  defaults: {"@action": "create"} # Restliche Defaults ausgelassen
  httpMethods: [POST]
  
- name: Product API (show)
  uriPattern: products/{product.__identifier}
  defaults: {"@action": "show"}
  httpMethods: [GET]
  
# Und so weiter für PUT/update und DELETE

Listing 2

Ob der Webservice funktioniert, lässt sich anschließend mit ein paar kurzen Kommandozeilen-Aufrufen testen:

Anzeige
Anzeige

Webservice testen

> curl –X POST –H'Content-Type: application/json' \
	–d'{"product":{"name":"Test","quantity":3}}' \
	http://flow.local/products
> curl –X GET http://flow.local/products

Listing 3

Ein Schnelltest mit einem Konsolenwerkzeug wie HTTPie (http://httpie.org) ermittelt, ob der Webservice wie gewünscht funktioniert. (Screenshot: httpie.org)

Ein Schnelltest mit einem Konsolenwerkzeug wie HTTPie (http://httpie.org) ermittelt, ob der Webservice wie gewünscht funktioniert. (Screenshot: httpie.org)

Wenn Daten an den Controller übermittelt werden, greift das von TYPO3 Flow bekannte Property Mapping. Besitzt die oben verwendete Product-Klasse also die Methoden setName und setQuantity, wird automatisch eine passende Instanz der Klasse erstellt. Dabei ist es egal, ob die Daten JSON-, XML- oder URL-codiert sind. Wichtig ist lediglich, dass ein entsprechender Content-Type-Header dem Framework übermittelt, wie der Request zu verarbeiten ist.

Implementierung von Best Practice: Drei Beispiele

Im Laufe der Zeit haben sich einige Best Practices für den Entwurf von REST-APIs herauskristallisiert, deren Gebrauch in der Regel sinnvoll ist. Brauchbare Quellen sind etwa hier und hier zu finden. Im folgenden Abschnitt geht es darum, wie einige dieser Empfehlungen mit TYPO3 Flow umgesetzt werden können.

Versionierung

In der Praxis entwickelt sich das Fachmodell einer Applikation im Lauf der Zeit weiter. Ein Webservice sollte jedoch möglichst stabil bleiben und sich nicht verändern, sobald er einmal öffentlich ist; schließlich weiß man nicht, wer die angebotene API überhaupt nutzt und sich darauf verlässt, dass sie funktioniert. Um die Abwärtskompatibilität zu erhalten, empfiehlt sich daher eine Versionierung der API. So lassen sich neue Funktionen oder nicht abwärtskompatible Änderungen einfach in einer neuen Version der API implementieren. Clients können dann (etwa über die URL) die zu nutzende Version der API angeben:

Anzeige
Anzeige

Versionierung der API

/api/v1/products/12345

Listing 4

Idealerweise berücksichtigt man von Anfang an, dass es einmal mehrere Versionen des Webservices geben wird. Eine mögliche Herangehensweise ist, das eigentliche Domänenmodell und die REST-API in verschiedene Flow-Packages zu entkoppeln; kommen später neue Versionen der API hinzu, können einfach weitere Pakete hinzugefügt werden.

Soll die Schnittstelle stabil bleiben, ist es problematisch, zum Property Mapping direkt die Objekte des Fachmodells zu nutzen, da sich dieses prinzipiell ändern kann. Eine Lösung besteht darin, Hilfsklassen in Form einfacher Data Transfer Objects einzuführen. Diese werden vom Property Mapper abgebildet (1) und lassen sich später nutzen, um (über eine Mapper-Klasse, die man dann selbst implementieren muss) die eigentlichen Fachobjekte zu erstellen (2).

Data Transfer Objects

public function createAction(ProductDto $productDto) { /* (1) */
	$product = $this->productMapper->createProduct($productDto); /* (2) */
	$this->productRepository->add($product);
}

Listing 5

Vorteil dieser Lösung ist, dass die vom Property Mapper genutzten Transfer-Klassen unverändert bleiben. Ändert sich das Fachmodell, kann die Abbildungslogik innerhalb des Webservices angepasst werden, die Schnittstelle nach außen bleibt gleich. Eine Implementierung dieses Musters findet sich im Beispielquelltext am Ende dieses Artikels).

Anzeige
Anzeige

Über das Routing ist es zudem einfach, die API-Version über die URL zu spezifizieren. Dies geschieht durch den Import der Subrouten aus den jeweiligen Paketen in der globalen Routing.yaml und die Einbindung mit einem URL-Präfix:

Import von Subrouten

- name: Products API v1
  uriPattern: api/v1/<ProductsV1Subroutes>
  subRoutes:
    ProductsV1Subroutes: {Package: Helmich.ProductsApiV1}
- name: Products API v2
  uriPattern: api/v2/<ProductsV2Subroutes>
  subRoutes:
    ProductsV2Subroutes: {Package: Helmich.ProductsApiV2}

Listing 6

Status-Codes

Der Teil eines HTTP-Responses, auf den Clients wahrscheinlich als erstes achten, ist der HTTP-Statuscode. RFC 7231 spezifiziert eine ganze Reihe Statuscodes, mit deren Auswertung man Anwender über Erfolg (oder Misserfolg) einer HTTP-Anfrage informieren kann. Diese sollten von einer guten REST-API auch genutzt werden.

Vieles übernimmt hierbei bereits Flow. Es sendet beispielsweise bei einem nicht korrekt interpretierbaren Request einen 400-Bad-Request-Response, beim Fehlen eines benötigten Domänenobjekt einen 404-Not-Found-Response. Darüber hinaus kann der Status-Code auch direkt im Controller verändert werden (1):

Anzeige
Anzeige

Status-Code im Controller verändern

public function createAction(Product $product) {
	$this->productRepository->add($product);
	$this->response->setStatus(201); (1)
}

Listing 7

Cache-Control-Header

Genau wie der Statuscode lassen sich auch Response-Header direkt im Controller setzen – zum Beispiel für Cache-Header:

Response Header im Controller

public function showAction(Product $product) {
	$this->response->setHeader('Cache-Control', 'public, max-age=86400');
	$this->response->setHeader('Last-Modified',
		$product->getModificationTime()->format('r'));
	$this->response->setHeader('Etag',
		sha1($product->getModificationTime()->getTimestamp()));
	$this->view->assign('product', $product);
}

Listing 8

Aus architektonischer Sicht ist hier jedoch Vorsicht geboten: Auch wenn sich im Controller relativ einfach verschiedene Cache-Header setzen lassen, verletzt man hier sehr schnell das Single Responsibility Principle. Eine „saubere“ Lösung könnte hier beispielsweise darin bestehen, die Möglichkeiten zur aspektorientierten Programmierung zu nutzen und die Caching-Logik in einen eigenen Caching-Aspekt auszugliedern.

Zusammenfassung

Einen tatsächlich im ursprünglichen Sinne RESTful Webservice zu entwickeln, ist gar nicht so einfach. Mit dem REST Maturity Model gibt es mittlerweile sogar Ansätze, um zu beurteilen, wie RESTful ein Webservice überhaupt ist. Auch in TYPO3 Flow ist zuweilen noch Handarbeit nötig, um eine echt REST-konforme API zu konstruieren. Dennoch bekommen Entwickler in TYPO3 Flow ein gutes Werkzeug an die Hand, nicht zuletzt wegen der guten HTTP-Implementierung.

Beispiel-Quelltext zum Artikel
Die Code-Beispiele finden sich (zusammen mit weiterführenden Beispielen) unter diesem Link auf GitHub.
Mehr zu diesem Thema
Fast fertig!

Bitte klicke auf den Link in der Bestätigungsmail, um deine Anmeldung abzuschließen.

Du willst noch weitere Infos zum Newsletter? Jetzt mehr erfahren

Anzeige
Anzeige
4 Kommentare
Bitte beachte unsere Community-Richtlinien

Wir freuen uns über kontroverse Diskussionen, die gerne auch mal hitzig geführt werden dürfen. Beleidigende, grob anstößige, rassistische und strafrechtlich relevante Äußerungen und Beiträge tolerieren wir nicht. Bitte achte darauf, dass du keine Texte veröffentlichst, für die du keine ausdrückliche Erlaubnis des Urhebers hast. Ebenfalls nicht erlaubt ist der Missbrauch der Webangebote unter t3n.de als Werbeplattform. Die Nennung von Produktnamen, Herstellern, Dienstleistern und Websites ist nur dann zulässig, wenn damit nicht vorrangig der Zweck der Werbung verfolgt wird. Wir behalten uns vor, Beiträge, die diese Regeln verletzen, zu löschen und Accounts zeitweilig oder auf Dauer zu sperren.

Trotz all dieser notwendigen Regeln: Diskutiere kontrovers, sage anderen deine Meinung, trage mit weiterführenden Informationen zum Wissensaustausch bei, aber bleibe dabei fair und respektiere die Meinung anderer. Wir wünschen Dir viel Spaß mit den Webangeboten von t3n und freuen uns auf spannende Beiträge.

Dein t3n-Team

Werner

.. wäre ja nicht verkehrt gewesen, den Namen Rails im Artikel mal zu erwähnen..

Controller sieht da so aus:

class ArticlesController < ApplicationController

before_action :set_article, only: :show

def index
@articles = Article.online
end

def show
end

private
def set_article
@article = Article.find(params[:id])
end

end

Wenn ich das nun mit dem Typo code vergleiche..ist klar
warum ich Rails bevorzuge.

Antworten
Marcus Woy

Es geht auch einfach: https://apigility.org/

Antworten
Murks

Ich sehe kein Typo3/Flow ich sehe nur ein Symfony erweitert um völligen Murks…
Dann doch lieber was gescheites anstatt sowas…

Antworten

Melde dich mit deinem t3n Account an oder fülle die unteren Felder aus.

Bitte schalte deinen Adblocker für t3n.de aus!
Hallo und herzlich willkommen bei t3n!

Bitte schalte deinen Adblocker für t3n.de aus, um diesen Artikel zu lesen.

Wir sind ein unabhängiger Publisher mit einem Team von mehr als 75 fantastischen Menschen, aber ohne riesigen Konzern im Rücken. Banner und ähnliche Werbemittel sind für unsere Finanzierung sehr wichtig.

Schon jetzt und im Namen der gesamten t3n-Crew: vielen Dank für deine Unterstützung! 🙌

Deine t3n-Crew

Anleitung zur Deaktivierung
Artikel merken

Bitte melde dich an, um diesen Artikel in deiner persönlichen Merkliste auf t3n zu speichern.

Jetzt registrieren und merken

Du hast schon einen t3n-Account? Hier anmelden

oder
Auf Mastodon teilen

Gib die URL deiner Mastodon-Instanz ein, um den Artikel zu teilen.

Anzeige
Anzeige