Externe Datenbanken über OpenAPI an TYPO3-CMS anschließen
Die Erfahrung zeigt, dass Kunden und Entwickler bei einem Projekt einen unterschiedlichen Fokus haben (können): Kunden haben oft eher die Webseiteninhalte im Blick und möchten Landingpages erstellen, Impressumsdaten ändern oder ähnliche Inhaltsanpassungen vornehmen. Dagegen habe ich als Entwickler meinen Fokus eher auf den Daten, deren Strukturen und logischen Verbindungen, und wie diese in einem Domain-Model abgebildet werden können.
Genau diese Herausforderung gab es auch in einem Projekt für einen Kunden aus der Reisebranche. Dieser hat bereits ein System zur Pflege von Reisedaten, welches passgenau auf seine Anforderungen zugeschnitten ist. Jetzt möchte er jedoch das System bei der Ausgabe (Webseite) um die Flexibilität eines CMS erweitern, damit individuellere Reise-Angebote erstellt und im Frontend ausgeben werden können.
Das sind allerdings Inhalte, die keiner klaren Linie folgen, die sich nicht in Strukturen packen lassen. Mir stellte sich daher die Frage: Mache ich jetzt noch ein Page-Objekt auf, welches Any-Content aufnehmen kann?
Aber natürlich gibt es für so etwas Content-Management-Systeme. So ein System nachzubauen wäre aber Quatsch, da es inzwischen viele verschiedene Anbieter auf dem Markt gibt, die ausgefeilte Lösungen für diese Problematik bereitstellen - so wie zum Beispiel TYPO3.
Unser Kunde stand also vor folgender Frage: Sollte seine proprietäre Software dahingehend erweitert werden, dass sie auch CMS-Funktionalitäten beinhaltet? Oder sollte er auf ein CMS umsatteln, welches auf die eigenen Bedürfnisse zugeschnitten ist?
Und so haben wir die Wünsche unseres Kunden realisiert
Schritt 1: Daten mit OpenAPI bereitstellen
Anstatt direkt auf die Datenbank des Reisepflege-Systems zuzugreifen, habe ich mich dafür entschieden, die Daten über eine REST API bereitzustellen.
Warum? Das bringt folgende Vorteile mit sich:
- Lese- und Schreibrechte lassen sich passgenau definieren.
- Durch Reverse-Proxy-Caching mit Varnish wird die Datenbanklast reduziert.
- Daten können durch verschiedene (server- und clientseitig) Clients angefragt werden.
- Zukünftige Anwendungen können dank der Schnittstelle leichter angebunden werden.
Mit OpenAPI Specification ist eine API in überschaubarer Zeit sauber definiert. Durch Code-Generatoren lassen sich ein API-Server und API-Clients in der präferierten Programmiersprache generieren.
Ich habe mich bei der Auswahl der serverseitigen Sprache für PHP und bei den Clients für PHP und JavaScript entschieden. Dazu habe ich zunächst die vorhandene Datenbankstruktur genutzt, um daraus eine Konfigurationsdatei für OpenAPI zu generieren.
docker-compose exec apache-php bin/console api:openapi:export --spec-version=3 --yaml > openapi.yml
Beim Generieren des Frontends wird eine “lebendige” API-Dokumentation generiert, welche die verfügbaren Endpunkte mit ihren Parametern auflistet. Doch das ist noch nicht alles! In dieser Dokumentation können beispielhafte Anfragen an die API direkt ausgeführt und die Antwort ausgewertet werden.
Diese Dokumentation ist inzwischen auch automatisch in GitLab integriert:
Schritt 2: Ausgabe der Daten mittels Plugins und Vue.js
Mit TYPO3-Plugins
Für wenig dynamische Ausgaben reicht eine serverseitige Generierung der Ausgabe. In den von uns erzeugten TYPO3-Plugins kommt der generierte PHP API-Client zum Einsatz. Durch hinzugefügte Parameter war es uns möglich, dem Redakteur Konfigurations- und Filteroptionen in den Plugins anzubieten. Zum Beispiel kann der Redakteur bei der Ausgabe von Reisen bei den Datensätzen nach Fluss- oder Meereskreuzfahrten filtern.
Mit Vue.js
Die Geschwindigkeit von Reiseportalen ist für Nutzer ein wichtiger Punkt. Sie wollen schnell passende Angebote entdecken und buchen können. Ein ständiges Neuladen der Seite kann da den Buchungsspaß schnell trüben.
Für eine hohe Website-Geschwindigkeit und ein optimales Benutzererlebnis bedurfte es in diesem Projekt einer Business-Logik, die direkt im Browser des Nutzers ausgeführt wird. Frameworks wie Angular, React, Vue.js oder Svelte sind für solche Aufgaben prädestiniert. Durch das two way data binding sind Datenhaltung und deren Abbildung synchron. Bei der erweiterten Suche nach Reisen brachte mir das den Vorteil, dass ich die voneinander abhängigen Filteroptionen immer wieder anpassen konnte, ohne mich dabei um die Synchronität der Daten mit der DOM aktiv kümmern zu müssen.
Schritt 3: Vue.js als Plugin bzw. Inhaltselement einbinden
Die generierte Vue.js-Anwendung habe ich in ein TYPO3-Inhaltselement gepackt. Das hat den Vorteil, dass Konfigurationen an die Vue.js-App durch TYPO3 übergeben werden können. Dies habe ich durch data-Attribute realisiert:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://typo3.org/ns/TYPO3/Fluid/ViewHelpers"
xmlns:flux="http://typo3.org/ns/FluidTYPO3/Flux/ViewHelpers"
xmlns:v="http://typo3.org/ns/FluidTYPO3/Vhs/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:layout name="Content" />
<f:section name="Configuration">
<flux:form id="Banner">
<flux:field.select
name="preselectCruiseType"
default="0"
items="{
0: {0: 'ocean', 1: 'ocean'},
1: {0: 'river', 1: 'river'}
}"/>
[…]
</flux:form>
</f:section>
<f:section name="Preview">
[…]
</f:section>
<f:section name="Main">
<f:format.raw>
<script type="text/javascript">
window.addEventListener('load', () => {
let element = document.createElement('script');
element.onload = function() {
window.finder();
}
element.src = '/path/to/vuejsapp/dist/app.js';
document.head.appendChild(element);
});
</script>
</f:format.raw>
<app id="app"
data-action="{f:uri.page(pageUid: settings.page.search)}"
data-type="Banner"
data-url-api="{settings.url.api}"
data-preselect-cruise-type="{preselectCruiseType}">
</app>
</f:section>
</html>
Vue.js und das Beschneiden von Bildern
TYPO3 bietet durch GraphicsMagick die Möglichkeit, bequem Bilder zu komprimieren oder zurechtzuschneiden. Dies geschieht serverseitig, also bevor die Seite gerendert und zum Client geschickt wird.
Aber wie sollte mit clientseitigen Anwendungen umgegangen werden, die immer wieder neue Assets anfordern?
Die bisherige Lösung beim besagten Kunden war, all diese Bilder schon vorzurendern und unter bestimmten Pfaden abzulegen. Ich habe mich dagegen für einen Image-Crop-Service entschieden, um flexibler auf Projektanforderungen reagieren zu können. Dabei wird das originale Bild mit Parametern zu Breite, Höhe und Dateiformat an den Service gegeben. Das verkleinerte und optimierte, ggf. auch zugeschnittene Bild kommt als Antwort zurück.
Es gibt dazu einige SaaS-Anbieter und Open-Source-Projekte. Zum Zeitpunkt des Projektstarts war thumbor das Tool unserer Wahl. Ebenfalls empfehlenswert ist imgproxy.net. Vom Prinzip her funktionieren sie aber gleich:
- Ein Image-Proxy-Service wird gestartet
- An diesem Service wird die URL des originalen, hochaufgelösten Bildes als GET-Parameter übergeben
- Es werden Ergänzungen vorgenommen (z. B. die gewünschte Ziel-Größe oder das Bildformat, ob das Bild beschnitten werden soll oder nicht, etc.)
- Der Service generiert ein Bild nach den gemachten Vorgaben und legt es in einem eigenen Cache-Verzeichnis ab.
Abschließend liefert der Service als Antwort das generierte Bild im gewünschten Dateiformat zurück.
Optional - aber dringend empfehlenswert - ist es noch zu verhindern, dass der Service durch Fremde verwendet werden kann. Im schlimmsten Fall werden eine zu große Last erzeugt und ggf. Copyrights verletzt. Das wird durch einen SALT erreicht. Damit ist es für unsere Client-Anwendung erlaubt, für bestimmte Bildgrößen zugeschnittene Bilder abzurufen, was die getrennte Entwicklung von Frontend und Backend ermöglicht.
Hier ein Beispiel:
- Innerhalb unserer Vue.js-Anwendung soll ein Bild auf die Größe von 350 x 230 Pixeln zugeschnitten werden
- Den Pfad zum Originalbild erhalte ich von der API. Hier ein Beispielpfad: https://placekitten.com/1440/1200
- Durch einen SALT wird verhindert, dass der Service missbraucht wird. Er ist Bestandteil der URL und sieht in etwa so aus: o_tAXuF0nsTgwUk9YL_T4VxxxxY=
- Nun versehe ich den Pfad samt der notwendigen Parameter an den Image-Crop-Service und bekomme das zugeschnittene Bild zurück
<template>
<picture>
<source :media="cropSettings.sizes.lg.mediaBp" :srcset="`${cropSettings.server}/${cropObject[cropSettings.sizes.lg.pixels]}/${cropSettings.sizes.lg.pixels}/${imagePath}`">
<source :media="cropSettings.sizes.sm.mediaBp" :srcset="`${cropSettings.server}/${cropObject[cropSettings.sizes.sm.pixels]}/${cropSettings.sizes.sm.pixels}/${imagePath}`">
<img
:class="classes"
:id="id"
:alt="alt"
:src="`${cropSettings.server}/${cropObject[cropSettings.sizes[cropSettings.default].pixels]}/${cropSettings.sizes[cropSettings.default].pixels}/${imagePath}`"
>
</picture>
</template>
<script>
import { cropSettings } from '../../data/defaults'
export default {
props: {
id: {
type: String,
required: true
},
classes: {
type: String,
default: ''
},
alt: {
type: String,
required: true
},
imagePath: {
type: String,
required: true
},
cropObject: {
type: Object,
required: true
},
cropServer: {
type: String
}
},
data () {
return {
cropSettings: {
server: this.cropServer, // config from data attribute
sizes: {
lg: { pixels: '600x395', mediaBp: '(max-width: 768px)' },
sm: { pixels: '350x230', mediaBp: '(min-width: 769px)' }
},
default: 'sm'
}
}
}
}
</script>
Daraus wird dann folgendes HTML generiert:
<img src=”https://media.example.de/o_tAX0F0nsTgwUk9YL_T4VxxxxY=/350x230/rps.example.de/files/media_gallery/size_x/Liegestuhl_3_1.jpg” />
Was könnte noch verbessert werden?
Steht ein leistungsfähiger Image-Crop-Service zur Verfügung, könnte dieser auch serverseitig verwendet werden. Hier ist meine erste Idee, den Fluid-ViewHelper f:image so zu erweitern, dass er die externen Bilder mit entsprechenden Parametern nicht an GraphicsMagick, sondern an den Crop-Service schickt.
Infrastruktur und Zugewinn für den Kunden
Der Kunde kann weiterhin die Daten seiner Reisen in seinem Backend pflegen. Mit TYPO3 kann er die Ausgabe der Daten genau steuern und durch nicht-strukturierte Daten ergänzen.
Die gewonnene Flexibilität schlägt sich allerdings in einer höheren Komplexität der Infrastruktur nieder.
Fazit
Unsere Erkenntnis bei diesem Kundenprojekt: Es muss nicht immer alles gleich neu gemacht werden. Durch OpenAPI ist es ein Leichtes, moderne Anwendungen über Schnittstellen an bestehende Systeme anzubinden. Mit reaktiven Frameworks wie Vue.js ist es möglich, ein besseres Nutzererlebnis zu schaffen als mit serverseitig gerenderten Webseiten.