Neos/Flow Update auf 6.x - Eine Anleitung

Hier kommen die PHP-Standards PSR-3 und PSR-7 ins Kundenprojekt

if (sad() === true) { sad().stop() ; beAwesome(); }

André Hoffmann
Ob Frontend, ob Backend, ob Javascript, ob PHP er findet immer einen Weg zu ihrem Erfolg.
Lesedauer: ca. 7 Minuten

Die Ausgangslage

Als Mitarbeiter der punkt.de arbeite ich an vielen Projekten und nutze dabei die verschiedensten Technologien um die Wünsche der Kunden zu erfüllen. Eine besondere Aufgabe aus der letzten Zeit war das Update einer Neos/Flow-Anwendung. Flow Neos dient in dem Kundenprojekt u.a. zur zentralen Datenhaltung und stellt sicher, dass die Daten nur dort angezeigt werden (Internet oder Intranet) wo diese auch angezeigt werden dürfen. Da dies im Hintergrund unbeobachtet und zuverlässig geschieht, wurden die Updates eher stiefmütterlich behandelt, und so kam es, dass das Projekt mehrere Major Releases zurückfiel.

Das Ziel ist 5.x

Da es hieß, zwei Major Version aufzuholen, sollte das Update in zwei Schritten durchgeführt werden. Zuerst wollen wir die Anwendung von Neos/Flow 4.3 auf Neos/Flow 5.0 bzw. 5.3 aktualisieren, um die Komplexität geringer zu halten.

Ein composer update kommt selten allein

Der erste Schritt beginnt mit dem Anpassen der composer.json. Schnell mal die Version auf 5.0.0 gesetzt und im Terminal im Projekt ein composer update ausführen und schon gibt es die ersten Fehlermeldungen. Also einen Blick darauf werfen und schauen, worüber sich composer beschwert. In den meisten Fällen dürfte es Probleme mit den im Projekt genutzten Version geben. Vor allem in einem Projekt, welches so lange nicht aktualisiert wurde, wird man zwangsläufig über gepinnte veraltete Version seiner Abhängigkeiten stolpern. Auch wenn man solche Updates selten macht, unterstützt composer einen mit Hinweisen auf mögliche Anpassungen von Abhängigkeiten. So kommt man auch bei diesen größeren Updates gut voran.
Es zeigte sich zum Beispiel, dass in unserem Projekt noch eine alte Version unseres Packages für die Fehlermonitoringlösung Sentry genutzt wurde. Im Projekt wurde die Version 1.2.0 genutzt. Ein Blick auf die Releases-Seite des Repositories auf Github zeigte, dass auch dieses Package bereits zwei neuere Major Versionen hat. Nach einem Blick in die composer.json Datei des Paketes sieht man recht schnell, dass man die aktuellste Version verwenden kann. Die folgenden Arbeiten sind straightforward und so kann man auch zügig die entsprechenden Packages für automatisch ausführbare Tests wie z.B. behat oder PHPUnit finden und wo notwendig anpassen. Sinnvoll ist es, die Testpaket erst einmal zu entfernen um die Komplexität zu reduzieren und sich auf das Wesentliche zu konzentrieren. Für die Tests habe ich dann zu einem späteren Zeitpunkt die Abhängigkeiten per composer require --dev package-name in der aktuellsten Version neu hinzugefügt.

./flow macht das (Entwickler-)Leben leichter

Sobald composer update ohne Fehler sowohl Neos/Flow als auch alle weiteren Abhängigkeiten aktualisiert hatte, kann man ./flow im Projektverzeichnis aufrufen. Die dann folgenden Fehlermeldungen werde auf der Update-Seite für Neos erläutert. An dieser Stelle ist wichtig zu wissen, wie das CMS Neos und das Neos/Flow-Framework zueinander stehen. 

Neos/Flow ist ein MVC-Framework, mit dessen Hilfe beliebige Webanwendungen entwickelt werden können. Es bietet alle Annehmlichkeiten gängiger MVC-Framworks wie zum Beispiel Dependency-Injection oder die Abstraktion der Datenbank unter Nutzung von Doctrine. Des Weiteren ist Neos/Flow der Unterbau für das CMS Neos, ähnlich wie ExtBase als Unterbau für TYPO3 fungiert. Da die Version von Neos immer um eins kleiner ist als die genutzte Neos/Flow Version, fand ich die für mein Update von Neos/Flow 4.3 nach 5.0 wichtigen Informationen im Artikel zum Update Neos 3.3 nach 4.0. So war unter dem Punkt Breaking Changes nachzulesen, dass sich die in dieser Version genutzte Symfony Yaml Component beim Parsen der YAML-Konfigurationsdateien strikter war. Dies zeigte sich dann auch in den Fehlermeldungen, die ich beim Aufrufen des Kommandos ./flow im Terminal sah. Nach dem Anpassen der Konfiguration entsprechend den Hinweisen in den Fehlermeldungen ließ sich der Befehl ./flow ausführen und somit konnte ich - den Anweisungen auf der Update-Seite folgend - per Terminal noch die Datenbankanpassungen sowie Codemigrationen durchführen lassen.

Endlich standardkonform - PSR-3 LoggerInterface

Da das Ziel generell Neos/Flow 5.3 war, zögerte ich nicht lange und zog per composer die Version hoch. Hier fügte ich auch die Entwicklungsabhängigkeiten für die verschiedenen Tests in den aktuellsten Versionen wieder zum Projekt hinzu, um einen stabilen Zwischenstand des Projektes mit Neos/Flow 5.3 zu haben. 

Eine große Änderung, die mit Neos/Flow 5.0 Einzug fand, ist die Nutzung des PHP-Standards PSR-3. Dieser Standard definiert ein für Neos/Flow neues LoggerInterface. Die Nutzung dieses Standards erfordert nun eine umfangreichere Code-Anpassung im Projekt. In den Neos/Flow-Versionen <= 4.3 definierte das Neos/Flow-eigene LoggerInterface für das Logging die folgenden zwei Funktionen:

public function log( $message, $severity = LOG_INFO, $additionalData = null, $packageKey = null, $className = null, $methodName = null );

public function logException(\Exception $exception, array $additionalData = []);

Der PSR-3-Definition ist zu entnehmen, dass nun dielog-Funktion durch weitere Funktion wie z.B. info, warning, debug ergänzt wird und diese bevorzugt zu benutzen sind. Weiterhin wurde die FunktionlogException aus dem allgemeinen Neos/Flow-LoggerInterface entfernt und durch die FunktionsdefinitionlogThrowable im ThrowableStorageInterface ersetzt. Basierend auf diesen Informationen passte ich den Code an und ersetzt so z.B. Aufrufe wie

$this->logger->log('my message', LOG_INFO);

durch

$this->logger->info('my message', LogEnvironment::fromMethodName(__METHOD__));

Dabei gibt die Funktion LogEnvironment::fromMethodName(__METHOD__) die Context-Informationen zurück, die bis jetzt im Hintergrund automatisch durch das Framework hinzugefügt wurden. Wenn die Context-Informationen des LogEnvironments nicht ausreichen, können diese z.B. wie folgt mit Zusatzinformationen angereichert werden:

$this->logger->info( 'my message', array_merge(LogEnvironment::fromMethodName(__METHOD__), ['extraInformation' => $data]) );

Ein kleiner Stolperstein war für mich das unterschiedliche Verhalten zwischen den Funktion logException und logThrowable. Anders als bei logException wird eine Exception nicht automatisch geloggt. Das heißt, man kann logException nicht einfach durch logThrowable ersetzen. Stattdessen muss man sich selbst um das Logging der Exception kümmern:

$this->logger->error( 'Error occurred, exception follows:'. $this->throwableStorage->logThrowable($exception), LogEnvironment::fromMethodName(__METHOD__) );

Zusätzlich zu diesen Änderungen mussten natürlich noch die use-Statements und die Type-Hints angepasst werden:

use Psr\Log\LoggerInterface; ... /** * @var LoggerInterface * @Flow\Inject */ protected $logger;

Gegebenenfalls muss noch die Konfiguration angepasst werden, wenn eigene Logger im Projekt genutzt werden. Die beste Dokumentation war für mich an dieser Stelle der Neos/Flow-Code selbst, da dies ein sehr spezieller Fall, und daher nicht wirklich so detailliert dokumentiert, ist. 

Der abschließende Schritt war die Stabilisierung der Anwendung unter Zuhilfenahme der verschiedenen Tests, die in meinem Fall die Funktionalität der Anwendung sehr gut abdecken. Großes Lob an meine Kollegen dafür. Dadurch wurde meine Arbeit, und die Suche nach möglichen Fehlern, um ein großes Stück erleichtert. 

Nachdem ich für Unit-, Functional- und Behat-Acceptance-Tests grün bekommen hatte, konnte ich ziemlich sicher sein, dass die Anwendung wieder so funktioniert wie vorher und ich damit das Update auf Neos/Flow 5.3 erfolgreich fertiggestellt hatte.

Das Finale ist Neos/Flow 6.x

Nach dem der 5.3-Zwischenschritt geschafft ist, kann man die Anwendung auf den aktuellsten Stand (Neos/Flow 6.x) bringen.

Deja Vue, oder: Composer update und ./flow beginnt von vorne

Ich erhöhte die Version von Neos/Flow auf 6.0, schaute nach den Fehlermeldungen von composer update, kümmerte mich darum, dass alle Abhängigkeiten erfüllt waren, und sich die Anwendung ohne Fehler per composer installieren lies. Auch die durch ./flow angemerkten Fehler ließen sich dank der Update-Seite schnell identifizieren und beheben.
Nach der Anpassung der Konfiguration der Logger im Projekt war ich schon recht weit gekommen.

Noch mehr Standardkonformität - PSR-7 HTTP Message Interface

Mit Neos/Flow 6.0 fand der PHP-Standard PSR-7 Einzug in der Welt von Neos/Flow und ins Neos CMS. Dieser Standard definiert die zwei Interfaces Psr\Http\Message\RequestInterface und Psr\Http\Message\ResponseInterface und somit, wie die Kommunikation zwischen einem Server und einem Client repräsentiert werden soll. 

Weiterhin wurde festgelegt, dass der Body einer Message durch das Psr\Http\Message\StreamInterface repräsentiert wird. Durch die Nutzung von Streams erhält man die Möglichkeit, die verschiedenen Formen von Message-Inhalten wie zum Beispiel Text, Bilder oder Videos durch eine einheitliche API anzusprechen. 

Da die Neos/Flow-Anwendung in meinem Fall als Web-API gegenüber einer TYPO3-Instanz fungiert, machte ich mich daran, die Anwendung PSR-7 kompatibel zu machen. In Neos/Flow werden die durch PSR-7 gegebenen Vorteile genutzt und für die Repräsentation von Requests an den Server die PSR-7-kompatiplen Implementierungen aus dem guzzlehttp-Package eingesetzt. Darüber hinaus werden in Neos/Flow für die Nutzung im MVC-Context die Klassen ActionRequest und ActionResponse genutzt. Diese fungieren innerhalb von Neos/Flow als zusätzliche Abstraktionsschicht zu den PSR-7-Implementierungen. Mit diesem Wissen ausgestattet machte ich mich daran, den Projektcode anzupassen. 

Die folgenden Beispiele zeigen wie z.B. ein Dateitransfer von einem Server an einen Client bisher realisiert wurde und wie eine PSR-7 konforme Umsetzung aussehen kann. Dabei ist dieser Code in einem ActionController anzusiedeln, wo ein direkter Zugriff auf den ActionRequest über die $request-Property des Controllers möglich ist.

PHP-native Funktionen

header('Content-type: ' . $contentType); header('Content-Disposition: attachment; filename="' . basename($filePath)); 
header("Cache-Control: max-age=0,private,must-revalidate"); 
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); 
ob_clean(); 
readfile($filePath); 
flush();
exit();

PSR-7 unter Nutzung von Streams

$this->response->setContentType($contentType); 
$this->response->setComponentParameter( SetHeaderComponent::class,'Content-Disposition', 'attachment; filename="' . basename($filePath) ); 
$this->response->setComponentParameter(SetHeaderComponent::class,'Cache-Control', 'max-age=0,private,must-revalidate'); 
$this->response->setComponentParameter(SetHeaderComponent::class,'Expires', 'Sat, 26 Jul 1997 05:00:00 GMT'); 
return GuzzleHttp\Psr7\stream_for(fopen($filePath, 'r+'));

Neos/Flow >= 6.0

$this->response->setHeader('Content-type', 'application/pdf'); 
$this->response->setHeader('Content-Disposition', 'attachment; filename="' . $fileName); 
$this->response->setContent($content); 
$this->response->send();

PSR-7 unter Nutzung von Streams

$this->response->setContentType('application/pdf'); 
$this->response->setComponentParameter( SetHeaderComponent::class,'Content-Disposition', 'attachment; filename="' . $fileName);
return stream_for($content);

Neben diesen Anpassungen, die direkt im Zusammenhang mit dem PSR-7-Standard stehen, kamen auch indirekte Änderungen hinzu, da einige Funktionen wie sendHeaders oder send aus der bisherigen ActionResponse-Implementierung entfernt wurden. Die folgenden Beispiele zeigen, wie man damit umgehen kann: 

Dateidownload von Server Neos/Flow < 6.0

$this->response->setHeader('Content-type', 'application/pdf'); 
$this->response->setHeader('Content-Disposition', 'attachment; filename="' . basename($this->absoluteFilePath)); 
$this->response->sendHeaders(); 
ob_clean(); 
readfile($this->absoluteFilePath); 
flush();

Neos/Flow => 6.0

$this->response->setContentType('application/pdf'); 
$this->response->setComponentParameter( SetHeaderComponent::class, 'Content-Disposition', 'attachment; filename="' . basename($absoluteFilePath) );
return stream_for(fopen($absoluteFilePath, 'rb'));

Abbruch der Request-Verarbeitung und senden der Fehlermeldung bei einem Fehler Neos/Flow < 6.0

$this->response->setStatus($httpStatusCode); 
$this->response->setContent( json_encode( [ 'statusCode' => $internalStatusCode, 'statusMessage' => $internalStatusMessage ] ) ); 
$this->response->send(); exit();

Neos/Flow => 6.0

$this->throwStatus( $httpStatusCode, $internalStatusMessage, json_encode( [ 'statusCode' => $internalStatusCode, 'statusMessage' => $internalStatusMessage ] ) );

Parallel zu diesen Anpassungen führte ich - immer wenn es sich anbot - ein Refactoring des Codes bzw. Fehlerkorrekturen durch, um auch diesen Update-Stand zu stabilisieren. 

Dabei ging ich wie beim 5.3-Meilenstein vor und nutzte ausgiebig die verschiedenen mir zur Verfügung stehenden Tests, um mögliche Fehler zu entdecken und zu beheben. Nachdem alle Tests fehlerfrei durchliefen, konnte ich die Anwendung ohne Aufwand auf die aktuellste Neos/Flow Version 6.1 aktualisieren. 

Doch damit war das Update noch nicht ganz fertig. In einem letzten Schritt wurde die Anwendung mit Hilfe des PHP-Linters PHP_CodeSniffer geprüft und gelintet, um so auch den Code Style zu aktualisieren.

Fazit

Das Update über zwei Major Versionen hinweg ist eine große Aufgabe, die sich aber lohnt, da man mit der neuen Version den PRS-7 Standard nutzen kann, und es so einfacher ist für alle Kollegen an einem großen Projekt zusammenzuarbeiten. 

Da Flow in dem Projekt an zentraler Stelle sitzt, konnte ich mir einen guten Überblick über das Gesamtprojekt verschaffen, ohne zu sehr in die Details gehen zu müssen. Nach ein paar aufregenden Tagen ist das Update ohne größere Schmerzen erledigt, und kann im Projekt genutzt werden.

Teilen:

Weitere Beiträge

Code gestaltet Zukunft – Unsere Leidenschaft für Innovation
Fahim Nasirzadeh, Entwicklung bei punkt.de
Arbeiten bei punkt.de