PHP 8.0: Vollständiger Überblick über die Neuerungen (1/4)

vor 4 Jahren von David Grudl  

PHP Version 8.0 ist erschienen. Sie ist so vollgepackt mit Neuerungen wie keine Version zuvor. Ihre Vorstellung erforderte gleich vier separate Artikel. In diesem ersten Teil werfen wir einen Blick darauf, was es Neues auf der Sprachseite gibt.

Bevor wir uns in PHP vertiefen, sei gesagt, dass die aktuelle Version von Nette vollständig für die Achter-Version vorbereitet ist. Mehr noch, als Geschenk ist sogar Nette 2.4 erschienen, das vollständig damit kompatibel ist, sodass Ihnen aus Framework-Sicht nichts im Wege steht, die neue Version zu verwenden.

Benannte Argumente

Und wir beginnen gleich mit einer Bombe, die man getrost als Game Changer bezeichnen kann. Neu können Argumente an Funktionen und Methoden nicht nur positionell, sondern auch nach Namen übergeben werden. Das ist absolut großartig, wenn eine Methode wirklich viele Parameter hat:

class Response implements IResponse
{
	public function setCookie(
		string $name,
		string $value,
		string|DateInterface|null $time,
		string $path = null,
		string $domain = null,
		bool $secure = null,
		bool $httpOnly = null,
		string $sameSite = null
	) {
		...
	}
}

Die ersten beiden Argumente übergeben wir positionell, die weiteren nach Namen: (benannte müssen immer nach den positionellen folgen)

$response->setCookie('lang', $lang, sameSite: 'None');

// anstelle des verrückten
$response->setCookie('lang', $lang, null, null, null, null, null, 'None');

Vor der Einführung dieses Features war geplant, in Nette eine neue API für das Senden von Cookies zu erstellen, da die Anzahl der Parameter von setCookie() wirklich gewachsen war und die positionelle Schreibweise unübersichtlich war. Das ist jetzt nicht mehr nötig, da benannte Argumente in diesem Fall die praktischste API sind. Die IDE wird sie vorschlagen und sie haben eine Typkontrolle.

Sie eignen sich auch hervorragend zur Verdeutlichung logischer Parameter, bei denen ihre Verwendung zwar nicht notwendig ist, aber true oder false allein nicht viel aussagt:

// früher
$db = $container->getService(Database::class, true);

// jetzt
$db = $container->getService(Database::class, need: true);

Parameternamen werden nun Teil der öffentlichen API. Es ist nicht mehr möglich, sie beliebig zu ändern wie bisher. Aus diesem Grund durchläuft auch Nette ein Audit, ob alle Parameter eine geeignete Benennung haben.

Benannte Argumente können auch in Kombination mit Variadics verwendet werden:

function variadics($a, ...$args) {
	dump($args);
}

variadics(a: 1, b: 2, c: 3);
// in $args wird ['b' => 2, 'c' => 3] sein

Neu kann das Array $args also auch nicht-numerische Schlüssel enthalten, was ein gewisser BC-Break ist. Dasselbe gilt für das Verhalten der Funktion call_user_func_array($func, $args), wo die Schlüssel im Array $args nun eine signifikante Rolle spielen. Im Gegensatz dazu sind Funktionen der func_*() Familie von benannten Argumenten abgeschirmt.

Mit benannten Argumenten hängt auch eng zusammen, dass der Splat-Operator ... nun auch assoziative Arrays entpacken kann:

variadics(...['b' => 2, 'c' => 3]);

Merkwürdigerweise funktioniert das bisher nicht innerhalb von Arrays:

$arr = [ ...['a' => 1, 'b' => 2] ];
// Fatal error: Cannot unpack array with string keys

Die Kombination von benannten Argumenten und Variadics gibt die Möglichkeit, endlich eine feste Syntax zum Beispiel für die Presenter-Methode link() zu haben, der wir jetzt benannte Argumente genauso wie positionelle übergeben können:

// früher
$presenter->link('Product:detail', $id, 1, 2);
$presenter->link('Product:detail', [$id, 'page' => 1]); // musste ein Array sein

// jetzt
$presenter->link('Product:detail', $id, page: 1);

Die Syntax für benannte Argumente ist viel attraktiver als das Schreiben von Arrays, so dass Latte sie sofort übernommen hat, wo sie zum Beispiel in den Tags {include} und {link} verwendet werden kann:

{include 'file.latte' arg1: 1, arg2: 2}
{link default page: 1}

Auf benannte Parameter kommen wir im dritten Teil im Zusammenhang mit Attributen noch zurück.

Ausdruck kann eine Ausnahme auslösen

Das Auslösen einer Ausnahme ist jetzt ein Ausdruck. Sie können ihn zum Beispiel in Klammern setzen und zu einer if-Bedingung hinzufügen. Hmmm, das klingt nicht sehr praktisch. Aber das hier ist schon interessanter:

// früher
if (!isset($arr['value'])) {
	throw new \InvalidArgumentException('Wert nicht gesetzt');
}
$value = $arr['value'];


// jetzt, da throw ein Ausdruck ist
$value = $arr['value'] ?? throw new \InvalidArgumentException('Wert nicht gesetzt');

Da Arrow-Funktionen bisher nur einen Ausdruck enthalten können, können sie dank dieses Features Ausnahmen auslösen:

// nur ein einzelner Ausdruck
$fn = fn() => throw new \Exception('oops');

Match-Ausdrücke

Die switch-case-Konstruktion hat zwei große Nachteile:

  • sie verwendet einen nicht-strikten Vergleich == anstelle von ===
  • man muss aufpassen, dass man nicht versehentlich break vergisst

PHP kommt daher mit einer Alternative in Form der neuen Konstruktion match, die einen strikten Vergleich verwendet und umgekehrt kein break verwendet.

Beispiel für switch-Code:

switch ($statusCode) {
    case 200:
    case 300:
        $message = $this->formatMessage('ok');
        break;
    case 400:
        $message = $this->formatMessage('nicht gefunden');
        break;
    case 500:
        $message = $this->formatMessage('Serverfehler');
        break;
    default:
        $message = 'unbekannter Statuscode';
        break;
}

Und dasselbe (nur mit striktem Vergleich) mit match geschrieben:

$message = match ($statusCode) {
    200, 300 => $this->formatMessage('ok'),
    400 => $this->formatMessage('nicht gefunden'),
    500 => $this->formatMessage('Serverfehler'),
    default => 'unbekannter Statuscode',
};

Beachten Sie, dass match keine Kontrollstruktur wie switch ist, sondern ein Ausdruck. Seinen Ergebniswert weisen wir im Beispiel einer Variablen zu. Gleichzeitig sind auch die einzelnen “Optionen” Ausdrücke, es können also nicht mehrere Schritte wie bei switch geschrieben werden.

Wenn keine Übereinstimmung mit einer der Optionen gefunden wird (und keine default-Klausel existiert), wird die Ausnahme UnhandledMatchError ausgelöst.

Übrigens gibt es in Latte auch die Tags {switch}, {case} und {default}. Ihre Funktionsweise entspricht genau dem neuen match. Sie verwenden einen strikten Vergleich, erfordern kein break und in case können mehrere durch Kommas getrennte Werte angegeben werden.

Nullsafe-Operator

Optional Chaining ermöglicht das Schreiben eines Ausdrucks, dessen Auswertung stoppt, wenn er auf null trifft. Und das dank des neuen Operators ?->. Er ersetzt viel Code, der sonst wiederholt auf null prüfen würde:

$user?->getAddress()?->street

// bedeutet ungefähr
$user !== null && $user->getAddress() !== null
	? $user->getAddress()->street
	: null

Warum „bedeutet ungefähr“? Weil der Ausdruck tatsächlich ausgeklügelter ausgewertet wird und kein Schritt wiederholt wird. Zum Beispiel wird $user->getAddress() nur einmal aufgerufen, sodass kein Problem dadurch entstehen kann, dass die Methode beim ersten und zweiten Mal etwas anderes zurückgibt.

Diese Funktion wurde von Latte vor einem Jahr eingeführt. Jetzt wird sie von PHP selbst übernommen. Großartig.

Konstruktor-Property-Promotion

Syntaktischer Zucker, der das doppelte Schreiben des Typs und das vierfache Schreiben der Variablen erspart. Schade nur, dass er nicht zu einer Zeit kam, als wir noch keine so intelligenten IDEs hatten, die das heute für uns schreiben 🙂

class Facade
{
	private Nette\Database\Connection $db;
	private Nette\Mail\Mailer $mailer;

	public function __construct(Nette\Database\Connection $db, Nette\Mail\Mailer $mailer)
	{
		$this->db = $db;
		$this->mailer = $mailer;
	}
}
class Facade
{
	public function __construct(
		private Nette\Database\Connection $db,
		private Nette\Mail\Mailer $mailer,
	) {}
}

Mit Nette DI funktioniert es, Sie können sofort damit beginnen.

Striktes Verhalten von arithmetischen und bitweisen Operatoren

Was einst dynamische Skriptsprachen zum Erfolg führte, wurde im Laufe der Zeit zu ihrer größten Schwäche. PHP hat sich einst von “Magic Quotes” und der Registrierung globaler Variablen befreit, und nun wird das lockere Verhalten durch Striktheit ersetzt. Die Zeit, in der man in PHP fast beliebige Datentypen addieren, multiplizieren usw. konnte, bei denen es absolut keinen Sinn ergab, ist längst vorbei. Ab Version 7.0 wird PHP immer strikter, und ab Version 8.0 führt der Versuch, einen beliebigen arithmetischen/bitweisen Operator auf Arrays, Objekte oder Ressourcen anzuwenden, zu einem TypeError. Eine Ausnahme bildet die Addition von Arrays.

// arithmetische und bitweise Operatoren
+, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, --:

Vernünftigerer Vergleich von Zeichenketten und Zahlen

Oder: Make loose operator great again.

Es schien, als gäbe es für den losen Operator == keinen Platz mehr, als wäre er nur ein Tippfehler beim Schreiben von ===, aber diese Änderung bringt ihn wieder auf die Landkarte. Wenn wir ihn schon haben, soll er sich vernünftig verhalten. Eine Folge des früheren “unvernünftigen” Vergleichs war zum Beispiel das Verhalten von in_array(), das Sie unangenehm überraschen konnte:

$validValues = ['foo', 'bar', 'baz'];
$value = 0;

dump(in_array($value, $validValues));
// gab überraschenderweise true zurück
// ab PHP 8.0 gibt false zurück

Die Änderung im Verhalten von == betrifft den Vergleich von Zahlen und “numerischen” Zeichenketten und wird in der folgenden Tabelle gezeigt:

Vergleich Vorher PHP 8.0
0 == "0" true true
0 == "0.0" true true
0 == "foo" true false
0 == "" true false
42 == " 42" true true
42 == "42 " true true
42 == "42foo" true false
42 == "abc42" false false
"42" == " 42" true true
"42" == "42 " false true

Überraschend ist, dass es sich um einen BC-Break im Kern der Sprache handelt, der ohne jeglichen Widerstand genehmigt wurde. Und das ist gut so. Hier könnte JavaScript sehr neidisch sein.

Fehlerberichterstattung

Viele interne Funktionen lösen nun TypeError und ValueError anstelle von Warnungen aus, die leicht übersehen werden konnten. Eine Reihe von Kernwarnungen wurde neu klassifiziert. Der Shutup-Operator @ unterdrückt nun keine fatalen Fehler mehr. Und PDO löst im Standardmodus Ausnahmen aus.

Diese Dinge hat Nette immer versucht, auf irgendeine Weise zu lösen. Tracy hat das Verhalten des Shutup-Operators angepasst, Database hat das Verhalten von PDO umgeschaltet, Utils enthält Ersatz für Standardfunktionen, die Ausnahmen anstelle unauffälliger Warnungen auslösen usw. Es ist schön zu sehen, dass die strikte Richtung, die Nette in seiner DNA hat, zur nativen Richtung der Sprache wird.

Arrays mit negativem Index

$arr[-5] = 'first';
$arr[] = 'second';

Was wird der Schlüssel des zweiten Elements sein? Früher war es 0, ab PHP 8 ist es -4.

Nachgestelltes Komma

Der letzte Ort, an dem kein nachgestelltes Komma stehen konnte, war die Definition von Funktionsparametern. Das gehört nun der Vergangenheit an:

	public function __construct(
		Nette\Database\Connection $db,
		Nette\Mail\Mailer $mailer, // nachgestelltes Komma
	) {
		....
	}

$object::class

Die magische Konstante ::class funktioniert auch mit Objekten $object::class, wodurch die Funktion get_class() vollständig ersetzt wird.

catch ohne Variable

Und schließlich: In der catch-Klausel muss keine Variable für die Ausnahme angegeben werden:

try {
	$container->getService(Database::class);

} catch (MissingServiceException) {  // kein $e
	$logger->log('....');
}

In den nächsten Teilen erwarten uns grundlegende Neuerungen bei den Datentypen, wir zeigen, was Attribute sind, welche neuen Funktionen und Klassen in PHP aufgetaucht sind und stellen den Just-in-Time-Compiler vor.