Code Refactoring? Das war die letzten beiden Jahre mein Job. In den letzten Projekten, in denen ich als Freelancer aktiv war, ging es um teilweise über zwanzig Jahre alten PHP Code. Bei der Planung zur Restrukturierung von altem Code kam auch immer wieder Laravel ins Gespräch. Warum ich mich klar gegen Laravel in einem Enterprise Umfeld positioniere, möchte ich an einem kleinen Beispiel erklären.
Man redet von Enterprise Software, wenn sich alles um individuell zu entwickelnde Software dreht. Software, die je nach Anwendungsfall mit vielen Daten umgehen und rasend schnell funktionieren muss. Unternehmen setzen oft auf eigens für ihren Anwendungsfall programmierte Software, die für sie sehr gut und schnell funktieren muss und gleichzeitig so wenige Abhängigkeiten wie möglich beinhaltet, um mit wenig Aufwand wartbar zu bleiben.
Als Basis für eine solche Software hat man als Programmierer zwei Möglichkeiten: Fange ich auf der grünen Wiese an und entwickle alles selbst? Oder nutze ich wiederkehrende, gleichbleibende Funktionalität von bereits vorhandenen Frameworks, um schneller voran zu kommen? Wenn man sich für den zuletzt genannten Punkt entscheidet, landet man relativ schnell bei Laravel.
Laravel ist ein PHP-Framework, welches grundsolide Funktionalität anbietet, um relativ schnell Software auf einer modularen Basis zu entwickeln. Neben Laravel gibt es noch viele weitere, wie z.B. Symfony oder Laminas (das ehemalige Zend Framework), welche ebenfalls sehr gute PHP Frameworks sind.
Warum also nicht Laravel?
Es galt abzuwägen, ob Laravel die Komplexität der neu aufzusetzenden Software schnell erledigen kann. Leider konnte Laravel die Anforderungen hier nicht erfüllen. Alter, prozeduraler Code, der zu großen Teilen noch unter PHP 3 und PHP 4 geschrieben wurde, sollte unter PHP 8.4 als strikt objektorienterter Code modular umgesetzt werden. Das alles kann Laravel sehr gut umsetzen. Allerdings fängt es bei der Modularität ein wenig an zu hinken, weil Laravel die PSR-Standards relativ schlecht umsetzt. Das größte Problem bestand aber in der Performance. Laravel ist einfach nicht schnell genug.
Wieso ist Laravel nicht schnell genug? Weil es die Möglichkeiten von PHP 8 nicht vollkommen ausschöpft und in Teilen auf Strategien setzt, die man heute in einem Enterprise Umfeld in dieser Art und Weise nicht mehr nutzen sollte. Schon vor knapp 6 Jahren schrieb ich einen Artikel in diesem Blog, wie man Resourcen-schonend und optimiert mit Iteratoren arbeiten kann. Laravel nutzt das selbst heute noch relativ wenig.
Die Laravel Collection als schlechtes Beispiel
Um dem Team des Auftraggebers vor Augen zu führen, wo ein möglicher Flaschenhals liegen könnte, habe ich mich für die Laravel Collection entschieden. Grob umschrieben ist die Laravel Collection ein Wrapper, der einfache Funktionalität rund um Arrays kapselt. Die Laravel Collection wird in vielen weiteren Laravel Modulen verwendet und ist fester Bestandteil des Laravel Frameworks. Problematisch daran ist, dass die Laravel Collection auf Arrays basiert.
Vor PHP 5.4 waren Arrays performanter als Objekte. Mit der damals relativ neuen objektorientierten Ausrichtung von PHP selbst hat sich das allerdings geändert. Seit PHP 5.4 ist die Performance von Objekten besser als die von Arrays. PHP 5.4 ist seit 2012 verfügbar. Seitdem ist eine Menge Zeit vergangen und die Performance von Objekten hat sich seitdem nochmals stark verbessert.
Folgendes Beispiel ist unter Laravel Entwicklern bestens bekannt. Das Benchmark wurde mit PHP 8.4.2 erzielt.
$start = microtime(true);
$collection = collect([
'blah@gmail.com',
'yadda@yahoo.com',
'blubb@gmail.com',
]);
$counted = $collection->countBy(
fn($value) => substr($value, strpos($value, '@') + 1)
);
$end = microtime(true);
var_dump($counted->all(), round($end - $start, 8), memory_get_peak_usage(true));
Das Codebeispiel zeigt eine Laravel Collection mit drei Einträgen in Form einer E-Mail Adresse. Diese Collection wird dann mittels der countBy()-Methode und einer an sie übergebene Callback-methode nach Vorkommen der E-Mail Provider gezählt. Das Ergebnis ist zweimal Google und einmal Yahoo.
Das Ergebnis sieht wie folgt aus.
array(2) {
["gmail.com"]=>
int(2)
["yahoo.com"]=>
int(1)
}
float(0.01605892)
int(2097152)
Allein das Filtern der drei Einträge mit der Laravel Collection dauert 0.01605892 Sekunden. Das klingt wenig, ist es aber nicht.
Wie geht das performanter?
Für den direkten Vergleich habe ich meine eigene native PHP Umsetzung herangezogen, die bis auf einen wesentlichen Unterschied genau das gleiche macht, wie die Laravel Collection. Der Unterschied ist, dass ich hier ausschließlich Objekte und Iteratoren verwende und auf Arrays verzichte.
<?php
declare(strict_types=1);
namespace Marcel;
use ArrayObject;
final class Collection extends ArrayObject
{
public function all(): array
{
return $this->getArrayCopy();
}
public function countBy(?callable $callback = null): Collection
{
$result = new self();
foreach ($this->getIterator() as $value) {
$value = $callback !== null ? $callback($value) : $value;
if (! $result->offsetExists($value)) {
$result->offsetSet($value, 0);
}
$result->offsetSet($value, $result->offsetGet($value) + 1);
}
return $result;
}
}
$start = microtime(true);
$collection = new Collection([
'blah@gmail.com',
'yadda@yahoo.com',
'blubb@gmail.com',
]);
$counted = $collection->countBy(
fn($value) => substr(strrchr($value, '@'), 1)
);
$end = microtime(true);
var_dump($counted->all(), round($end - $start, 8), memory_get_peak_usage(true));
Um die gleiche Semantik wie die Laravel Collection zu benutzen, nutze ich hier meine eigene ArrayObject Implementierung. Für dieses Beispiel beinhaltet die ArrayObject Implementierung lediglich die beiden benötigten Methoden. Mir ist bewusst, dass der Laravel Collection Wrapper wesentlich mehr Funktionalität beinhaltet. Für dieses Beispiel soll es aber erstmal reichen.
Das Ergebnis dieser Implementierung sieht wie folgt aus.
array(2) {
["gmail.com"]=>
int(2)
["yahoo.com"]=>
int(1)
}
float(0.00012994)
int(2097152)
Man sieht auf den ersten Blick, dass das Ergebnis genau das gleiche und der Speicherverbrauch in der Spitze genau identisch ist. Lediglich die Ausführungszeit ist über das hundertfache hinaus schneller.
Dieser Geschwindigkeitsvorteil ist bei Enterprise Anwendungen entscheidend. Genau hier arbeitet man mit Millionen von Datensätzen. Genau hier wird die Ausführungszeit dann wichtig. Laravel Collections stehen hier ganz weit im Abseits.
Fazit
Das Ergebnis hat selbst mich überrascht. Grundsätzlich stellt sich mir dann die Frage, ob man die Laravel Collection Implementierung nicht einfach mal grundsätzlich überarbeiten oder eine auf Objekten basierende Alternative mit dem Einsatz von Iteratoren anbieten sollte?
In diesem einen Anwendungsfall wurde auf Laravel verzichtet. Wesentliche Komponenten der Software wurden als Module, die über Composer verwaltet werden, eigens programmiert. Lediglich an Stellen, an denen die selbst programmierte Alternative in puncto Aufwand zu teuer wurde, hat man auf vorhandene Komponenten von Symfony oder Laminas zurückgegriffen.
Das eigentliche Fazit heißt aber klar und deutlich, dass Laravel für Enterprise Applikationen wenig geeignet zu sein scheint. Die Laravel Collection Klasse wird in allen großen Laravel Modulen, wie z.B. Laravel Eloquent (die Laravel ORM Implementierung) verwendet. Das genau das im Umgang mit großen Datenmengen eher kontraproduktiv ist, hat obiges Beispiel gezeigt.
Was sind Deine Erfahrungen im Enterprise Bereich? Hast Du Ähnliches schon mal selbst erlebt? Schreib ’s mir in die Kommentare.