PHP 8.1 – Enums, Fibers, Performance

php 8.1 release für WordPress
Saša EbachVon Saša Ebach|16. Februar 2022

PHP 8.1 – Release im Überblick

Die neuste Version von PHP 8.1 ist am 25.11.2021 erschienen. Auch wenn es nur ein kleinerer Punkt-Release ist, enthält die Version einige sehr interessante neue Features. Darunter gibt es ein paar einfache Funktionserweiterungen, zum Beispiel das neue Argument $options rund um die hash-Funktionen, als auch ganz neue Syntax-Varianten. Dieser Beitrag beleuchtet die wichtigsten Neuerungen.

Was uns und PHP-Fans natürlich erfreut, ist, dass auch Version 8.1 wieder ein wenig schneller geworden ist. So gemessen von stitcher.io:

Falls du wissen willst, wofür PHP eigentlich verwendet wird und wie es funktioniert, schau einfach mal hier in unseren erklärenden Beitrag Was ist PHP?

 

php 8.1 release benchmark

Auch synthetische Benchmarks bescheinigen PHP 8.1 eine mindestens 3 % bessere Performance zum Vorgänger und noch viel mehr zur weit verbreiteten Version 7.1.

php 8.1 release benchmark
Performance-Verbesserung mit PHP 8.1 (PHP Benchmark Suite) I Quelle: Phoronix

Jetzt zu den neuen Features.

1. Enums

Enums sind Aufzählungen mit einer begrenzten Anzahl von möglichen Eigenschaften. Sie fungieren im Prinzip als eine Option zwischen Konstanten und Klassen. Ein einfaches Beispiel:

enum Status {
  case Entwurf;
  case Publiziert;
  case Archiviert;
}

Diese Syntax ersetzt die etwas schwerfälligere Methode der Klassenkonstanten:

class Status {
  public const Entwurf = 'Entwurf';
  public const Publiziert = 'Publiziert';
  public const Archiviert = 'Archiviert';
}

Ein enum lässt sich als Typ weiterverwenden.

function setze_status(Status $status): void
{
}

So lässt sich bei Aufruf der Funktion sicherstellen, dass der richtig Typ übergeben wurde.

setze_status(Status::Entwurf);

Dabei kann ein case auch einen Wert haben. Der muss entweder vom Typ string oder int sein. In diesem Fall handelt es sich um eine backed enumeration.

enum Farben: string
{
    case ROT  = '#ff0000';
    case BLAU = '#0000ff';
    case GELB = '#ffff00';
}

enum Fehler: int
{
    case NICHTS  =  0;
    case ZUWENIG = -1;
    case ZUVIEL  =  1;
}

Um auf Namen und Wert zuzugreifen, gibt es Eigenschaften:

echo Farben::ROT->value; #=> #ff0000
echo Farben::ROT->name;  #=> ROT

Enums sind strukturierte Konstanten. Sie können aber auch eigene Methoden haben.

enum Rolle: string
{
    case ADMIN  = 'admin';
    case USER = 'user';
    case SUPPORT = 'support';
    
    public function icon(): string
    {
    	return match($this)
    	{
    		Rolle::ADMIN   => 'admin-icon.png',
    		Rolle::USER    => 'user-icon.png',
    		Rolle::SUPPORT => 'support-icon.png'
    	}
    }
}

match($this) fungiert in diesem Zusammenhang wie ein switch-Statement.

Enums selbst haben keinen state. Demnach gibt es keine Eigenschaften und ein Enum lässt sich auch nicht instanziieren. Es lassen sich jedoch ein oder mehrere Interfaces implementieren.

interface Farbenfroh
{
    public function farbe(): string;
}

enum Karten implements Farbenfroh
{
    case Herzen;
    case Karos;
    case Piks;
    case Kreuze;

    # Erfüllt den Interface-Vertrag
    public function farbe(): string
    {
        return match($this) {
            Karten::Herzen, Karten::Karos => 'Rot',
            Karten::Piks, Karten::Kreuze => 'Schwarz'
        };
    }

    # Ohne Interface geht auch
    public function form(): string
    {
        return "Rechteck";
    }
}

Mit einem trait lässt sich Verhalten in Form von Methoden in Enums einmischen, ähnlich wie das auch schon bei Klassen möglich ist.

interface Farbenfroh
{
    public function farbe(): string;
}

trait Rechteck
{
    public function form(): string {
        return 'Rechteck';
    }
}

enum Karten implements Farbenfroh
{
	use Rechteck;
	
    case Herzen;
    case Karos;
    case Piks;
    case Kreuze;

    public function farbe(): string
    {
        return match($this) {
            Karten::Herzen, Karten::Karos => 'Rot',
            Karten::Piks, Karten::Kreuze => 'Schwarz'
        };
    }
}

echo Karten::Herzen->form(); #=> Rechteck

 

 

2. Fibers – PHP asynchron ausführen durch Koroutinen

Mit Fibers gibt es das erste echte Feature für die asynchrone Ausführung in PHP. PHP verlässt sich seit seinen Anfängen darauf, dass parallele Exekution (Mulithreading) durch den Webserver (Apache etc.) gehandelt wird. Alles in allem ein sehr gute Strategie, weil es viel Komplexität vor den Entwicklern versteckt und schließlich hat der Erfolg von PHP auch gezeigt, dass dies bisher eine exzellente strategische Entscheidung war. Die Popularität von JavaScript und Go zeigen aber auch, dass PHP hier eine konkrete Lücke hat, die das Core-Team in Zukunft vermutlich, wenigstens teilweise schließen möchte.

Hier ein Beispiel zur Verwendung des Fibers-Features:

$request1 = new Http("get", "http://example.com");
$request2 = new Http("get", "http://example.com");

foreach ([$request1, $request2] as $request){
	$child = new Fiber(function() use ($request){
		// ::await blockiert lediglich den aktuellen Thread. Alle anderen Fibers können weiterlaufen
		Async::await($request->connect());
		Async::await($request->fetch());

		// Code hier läuft sobald die URL gelesen wurde/
		// Sie wartet also nicht auf die anderen Fetch-Prozesse
	});
	$child->start();
}

# Aktuell blokiert ::run() das Program und nichts kann weiterlaufen
# bis alle Fibers fertig sind.
# In der Zukunft soll es Top Level-Fiber geben, die die gesamte App
# asynchron machen.
Async::run();

Quelle: https://github.com/nox7/async-php-8-io-http

Sehr ausführliche Informationen zur Verwendung von Fibers finden sich bei PHP.Watch.

Es ist davon auszugehen, dass hauptsächlich die Framework-Entwickler von diesem Feature Gebrauch machen, da parallele Programmierung sehr herausfordernd sein kann.

3. Readonly-Eigenschaften für Klassen

Neben den Keywords final und abstract kennt PHP 8.1 jetzt das Keyword readonly. Es kann nur im Zusammenhang mit Eigenschaften für Klassen verwendet werden. Ein Beispiel:

class Ergebnis
{
    public readonly string $wert;

    public function __construct(string $wert)
    {
        $this->wert = $wert;
    }
}

Die Variable $wert kann nur bei Instanziierung gesetzt werden und dann nicht mehr.

$ergebnis = new Ergebnis('123');
$ergebnis->key = '456'; # eine Exception wird ausgelöst

Wofür könnte das gut sein? Selbstverständlich lässt sich auch einfach mit isset überprüfen, ob eine Variable schon erstellt wurde. Es ist aber denkbar, dass im Zusammenhang zum Beispiel mit der Fibers-Funktionalität solche syntaktischen Lösungen sauberer aussehen. Denkbar wäre eine Klasse, die versucht, einen Wert von mehreren URLs abzurufen. Sobald die erste Response reinkommt, wird der readonly-Wert erstellt. Alle nachfolgenden Versuche erzeugen eine Exception, die dann programmtechnisch wieder verarbeitet werden kann.

 

 

4. Intersection-Typen

PHP bietet seit Version 8.0 die Möglichkeit, Typen zu kombinieren, auch union types genannt. Mit | getrennte Typen erlauben den einen oder anderen Typen, wie im folgenden Beispiel.

class Student 
{
    private int|float $roomNo;
    public function setRoomNo(int|float $roomNo): void {
        $this->roomNo;
    }
    public function getRoomNo(): int|float {
        return $this->roomNo;
    }
}
$newStudent1 = new Student;
$newStudent1->setRoomNo(25.0);
$newStudent2 = new Student;
$newStudent2->setRoomNo(4);
 
echo "Student 1 number is " .$newStudent1->getRoomNo(). "<br>";
echo "Student 2 number is " .$newStudent2->getRoomNo();

(Quelle: educative).

$roomNo darf also int ODER float sein. Mittels & lässt sich jetzt eine UND-Voraussetzung bestimmen.

public function paginate(
    Countable&Iterator $collection,
    int $offset,
    int $slice,
): array {
}

Dieses Beispiel definiert den Funktions-Parameter $collection, der sowohl das Interface Countable als auch das Interface Iterator implementiert haben muss. Dies verhindert, dass der Programmierer dieses interface erst selbst definieren muss, zum Beispiel so: interface CountableIterator extends Countable, Iterator.

 

 

5. Der never-Rückgabewert

Klingt erstmal ungewöhnlich, aber es gibt Funktionen, die sollen überhaupt nie etwas zurückgeben. Beispielsweise wenn sie nur eine Ausnahme auslösen, eine Weiterleitung auslösen oder das Skript mit exit beenden.

function visit($url): never
{
	if (true)
	{
		return 'Ergebnis'; # Gibt Fehler aus.
	}
	header('Location: /');
}

Das never-Keyword verhindert, dass die visit-Funktion return verwenden darf. Dies dient dazu, Fehler in der Zukunft zu vermeiden.

 

 

6. Das final-Keyword für Konstanten

Man soll es kaum glauben, aber es ist tatsächlich möglich Konstanten in Klassen zu überschreiben, wenn ein Interface oder eine Vererbung das wünscht. Das final-Keyword verhindert dies jetzt. Allerdings nur für public und protected Konstanten.

class Bla
{
    final public const A = "Bla";
}

class Blubb extends Bla
{
    public const  = "Blubb"; # Ein fatal error verhindert dies
}

 

 

7. Die array_is_list-Funktion

Diese Funktion gibt true zurück, falls das array eine echte Liste mit sequentiellen Integer-Werten ist, die lückenlos sind und mit 0 beginnen.

array_is_list([]); # true
array_is_list([1, 2, 3]); # true
array_is_list(['hostpress', 2, 3]); # true
array_is_list(['hostpress', 'wordpress']); # true
array_is_list([0 => 'hostpress', 'wordpress']); # true
array_is_list([0 => 'hostpress', 1 => 'wordpress']); # true

# Key startet nicht bei 0
array_is_list([1 => 'apple', 'orange']); # false
# Keys in der falschen Reihenfolge
array_is_list([1 => 'apple', 0 => 'orange']); # false
# Keys sind nicht vom Typ Integer 
array_is_list([0 => 'apple', 'foo' => 'bar']); # false
# Keys nicht lückenlos
array_is_list([0 => 'apple', 2 => 'bar']); # false

 

 

8. fsync undfdatasync verhindern Datenverlust

Erfreulich sind diese beiden neuen Funktionen, weil sie ein potenziell schwerwiegendes Problem lösen. Besucher füllt Formular aus, sendet ab, erhält E-Mail, aber die Daten sind nicht auf Platte geschrieben, der Bestätigungslink funktioniert nicht. Wer schon länger mit PHP arbeitet, dem ist das vielleicht schonmal passiert. Schreiboperationen können manchmal missglücken, obwohl PHP zurückgibt, dass alles gut gegangen ist. Manchmal speichert das Betriebssystem die Daten nur im Zwischenspeicher, ohne sie final auf den Festspeicher zu schreiben. fsync undfdatasync versprechen Abhilfe. So sieht die Verwendung aus:

$file = 'test.txt';

$fh = fopen($file, 'w');
fwrite($fh, 'hostpress');
fwrite($fh, "\r\n");
fwrite($fh, 'is super fast');

fsync($fh);
fclose($fh);

 

 

9. Array unpacking für Arrays mit String-Keys

Arrays ineinander kopieren geht schon seit PHP 7.4 mit dem sogenannten spread operator (...). Allerdings nur für indizierte Arrays, also Arrays mit Integer-Index:

$array = [0 => '123', 1 => '789'];

PHP 8.1 erlaubt unpacking jetzt auch mit maps-Arrays.

$array1 = ['eins' => 'one'];
$array2 = ['zwei' => 'two'];
# Alte Methode
$alle   = array_merge($array1, $array2, ['drei' => 'three']);

var_dump($alle);

# Ist äquvalent zu 
$alle = [...$array1, ...$array2, ...['drei' => 'three']];

var_dump($alle);

# beide geben aus:

/*
array(3) {
  ["eins"]=>
  string(3) "one"
  ["zwei"]=>
  string(3) "two"
  ["drei"]=>
  string(5) "three"
}
*/

Keys von „hinteren“ Arrays überschreiben gleichnamige Keys „vorderer“ Arrays. Der +-Operator kann diesen Prozess aber auch umkehren:

var_dump(
     ['eins' => 'one']
    +['eins' => 'uno']
);
# array(1) {
#   ["eins"]=>
#   string(3) "one"
# }

var_dump([
     ...['eins' => 'one'],
     ...['eins' => 'uno']
]);
# array(1) {
#   ["eins"]=>
#   string(3) "uno"
# }

 

 

10. Neue Syntax für Closures

Die sogenannte first class callable syntax erlaubt konzisen Code. Bisher war ein Aufruf von Closure:fromCallable notwendig. Jetzt gibt es eine übersichtliche Dreipunkt-Notation.

$callable = Closure::fromCallable('strtoupper');
# wird zu
$callable = strtoupper(...);
echo $callable('hostpress'); #=> HOSTPRESS

Die Syntax funktioniert für Klassenmethoden, statische Methoden und anonyme Funktionen

# Klassenmethode
$callable = $item->getHosting(...);
# Statische Methode
$callable = $item::getHosting(...);
# Anonyme Funktion
$function = function() {};
$callable = $function(...);

 

 

11. Fazit: Jetzt schon upgraden?

Na, wer hat da keine Lust, upzugraden? WordPress verlangt selbst noch eine 7er-Version zum stabilen Betrieb. Ein direktes Upgraden ist also nicht akut. Aber reizvoll machen es die vielen neuen Features von PHP 8.1 schon. Nicht alle davon werden ihre Fans haben, jedoch gefällt uns besonders, dass PHP stets Fortschritte macht und dabei mit jedem Release auch immer ein wenig schneller wird. Wir freuen uns schon auf 8.2 und 9.0.

Die Kompatibilität mit WordPress ist grundlegend gegeben, auch wenn hier an manchen Punkten noch Nachholbedarf besteht. Bei HostPress werden neue PHP-Versionen gleich nach dem Release getestet und so ist auch PHP 8.1 bereits verfügbar und kann über die Verwaltungsoberfläche mit einem Klick aktiviert werden. Generell empfiehlt sich aber, die neue Version vorab auf einer Staging-Seite zu testen.

Welches Feature gefällt dir am besten und wovon wirst du am meisten Gebrauch machen?

12. Quellen