PHP 8.0: Neuigkeiten bei Datentypen (2/4)

vor 4 Jahren von David Grudl  

Die PHP-Version 8.0 wurde soeben veröffentlicht. Sie ist voll von neuen Funktionen, wie keine Version zuvor. Ihre Einführung verdient vier separate Artikel. Im zweiten Teil werden wir einen Blick auf die Datentypen werfen.

Gehen wir in der Geschichte zurück. Die Einführung von skalaren Typ-Hinweisen war ein bedeutender Durchbruch in PHP 7. Fast wäre es nicht dazu gekommen. Andreu Faulds, der Autor der genialen Lösung, die dank declare(strict_types=1) vollständig rückwärtskompatibel und optional war, wurde von der Community harsch abgelehnt. Glücklicherweise hat Anthony Ferrara sie und den Vorschlag damals verteidigt, eine Kampagne gestartet und den RFC sehr knapp verabschiedet. Uff. Die meisten Neuerungen in PHP 8 sind dem legendären Nikita Popov zu verdanken und sie alle gingen durch die Abstimmung wie ein Messer durch Butter. Die Welt verändert sich zum Besseren.

PHP 8 bringt Typen zur Perfektion. Die überwiegende Mehrheit der phpDoc-Annotationen wie @param, @return und @var werden durch native Notation ersetzt. Vor allem aber werden die Typen von der PHP-Engine überprüft. Nur Beschreibungen von Strukturen wie string[] oder komplexere Anmerkungen für PHPStan werden in den Kommentaren verbleiben.

Union-Typen

Vereinigungstypen sind eine Aufzählung von zwei oder mehr Typen, die eine Variable annehmen kann:

class Button
{
	private string|object $caption;

	public function setCaption(string|object $caption)
	{
		$this->caption = $caption;
	}
}

Bestimmte Union-Typen wurden bereits in PHP eingeführt. Nullbare Typen, zum Beispiel. Zum Beispiel ?string, was dem Union-Typ string|null entspricht. Die Fragezeichenschreibweise kann als Abkürzung betrachtet werden. Natürlich funktioniert sie auch in PHP 8, aber man kann sie nicht mit vertikalen Balken kombinieren. Anstelle von ?string|object müssen Sie also string|object|null schreiben. Außerdem war iterable immer äquivalent zu array|Traversable. Es mag Sie überraschen, dass float auch ein Union-Typ ist, da er sowohl int|float akzeptiert, aber den Wert in float umwandelt.

Sie können die Pseudotypen void und mixed nicht in Unions verwenden, da dies keinen Sinn ergeben würde.

Nette ist bereit für Union-Typen. In Schema, Expect::from() werden sie akzeptiert, und auch Presenter akzeptieren sie. Sie können sie z.B. in Render- und Action-Methoden verwenden:

public function renderDetail(int|array $id)
{
	...
}

Auf der anderen Seite lehnt das Autowiring in Nette DI Union-Typen ab. Bislang gibt es keinen Anwendungsfall, bei dem es sinnvoll wäre, dass der Konstruktor entweder das eine oder das andere Objekt akzeptiert. Sollte ein solcher Anwendungsfall eintreten, kann das Verhalten des Containers natürlich entsprechend angepasst werden.

Die Methoden getParameterType(), getReturnType() und getPropertyType() in Nette\Utils\Reflection lösen im Falle des Union-Typs eine Ausnahme aus (das ist in Version 3.1; in der früheren Version 3.0 kehren diese Methoden zurück, um die Null-Kompatibilität zu wahren).

mixed

Der Pseudotyp mixed akzeptiert jeden Wert.

Im Falle von Parametern und Eigenschaften führt er zu demselben Verhalten, als wenn wir keinen Typ angeben. Was ist also sein Zweck? Um zu unterscheiden, wann ein Typ lediglich fehlt und wann er absichtlich mixed ist.

Im Falle von Funktions- und Methodenrückgabewerten unterscheidet sich die Nichtangabe des Typs von der Verwendung von mixed. Es bedeutet das Gegenteil von void, da es voraussetzt, dass die Funktion etwas zurückgibt. Ein fehlender Rückgabewert führt dann zu einem fatalen Fehler.

In der Praxis sollten Sie es nur selten verwenden, da Sie dank der Union-Typen den Wert genau angeben können. Sie ist daher nur in besonderen Situationen geeignet:

function dump(mixed $var): mixed
{
	// print variable
	return $var;
}

false

Im Gegensatz zu mixed können Sie den neuen Pseudotyp false ausschließlich in Unionstypen verwenden. Er entstand aus der Notwendigkeit, den Rückgabetyp von nativen Funktionen zu beschreiben, die im Falle eines Fehlers historisch gesehen false zurückgeben:

function strpos(string $haystack, string $needle): int|false
{
}

Daher gibt es keinen Typ true. Sie können weder false allein noch false|null oder bool|false verwenden.

static

Der Pseudotyp static kann nur als Rückgabetyp einer Methode verwendet werden. Er besagt, dass die Methode ein Objekt desselben Typs wie das Objekt selbst zurückgibt (während self besagt, dass sie die Klasse zurückgibt, in der die Methode definiert ist). Er eignet sich hervorragend für die Beschreibung von fließenden Schnittstellen:

class Item
{
	public function setValue($val): static
	{
		$this->value = $val;
		return $this;
	}
}

class ItemChild extends Item
{
	public function childMethod()
	{
	}
}

$child = new ItemChild;
$child->setValue(10)
	->childMethod();

resource

Dieser Typ existiert nicht in PHP 8 und wird auch in Zukunft nicht eingeführt werden. Ressourcen sind ein Relikt aus der Zeit, als es in PHP noch keine Objekte gab. Nach und nach werden Ressourcen durch Objekte ersetzt werden. Letztendlich werden sie ganz verschwinden. Zum Beispiel ersetzt PHP 8.0 die Ressource image durch das Objekt GdImage und die Ressource curl durch das Objekt CurlHandle.

Stringable

Es ist eine Schnittstelle, die automatisch von jedem Objekt mit einer magischen Methode __toString() implementiert wird.

class Email
{
	public function __toString(): string
	{
		return $this->value;
	}
}

function print(Stringable|string $s)
{
}

print('abc');
print(new Email);

Es ist möglich, class Email implements Stringable in der Klassendefinition explizit anzugeben, aber es ist nicht notwendig.

Nette\Utils\Html Auch die Klasse Latte spiegelt dieses Namensschema wider, indem sie die Schnittstelle Nette\HtmlStringable anstelle der früheren IHtmlString implementiert. Objekte dieses Typs werden zum Beispiel von Latte nicht escaped.

Typ Varianz, Kontravarianz, Kovarianz

Das Liskov-Substitutionsprinzip (LSP) besagt, dass die Erweiterungsklassen und Schnittstellenimplementierungen niemals mehr verlangen und weniger liefern dürfen als die Eltern. Das heißt, die untergeordnete Methode darf nicht mehr Argumente verlangen oder einen engeren Bereich von Parametertypen akzeptieren als die übergeordnete Methode, und umgekehrt darf sie nicht einen breiteren Bereich von Typen zurückgeben. Aber sie kann weniger zurückgeben. Und warum? Weil sonst die Vererbung zusammenbrechen würde. Eine Funktion würde ein Objekt eines bestimmten Typs akzeptieren, aber sie hätte keine Ahnung, welche Parameter sie an ihre Methoden übergeben kann und welche Typen sie zurückgeben würden. Jedes Kind könnte sie unterbrechen.

In der OOP kann das die untergeordnete Klasse:

  • einen breiteren Bereich von Typen in den Parametern akzeptieren (dies wird Kontravarianz genannt)
  • einen engeren Bereich von Typen zurückgeben (Kovarianz)
  • und Eigenschaften können ihren Typ nicht ändern (sie sind invariant)

PHP kann dies seit Version 7.4, und alle neu eingeführten Typen in PHP 8.0 unterstützen auch Kontravarianz und Kovarianz.

Im Fall von mixed kann das Kind den Rückgabewert auf einen beliebigen Typ eingrenzen, aber nicht void, da es keinen Wert, sondern dessen Abwesenheit repräsentiert. Das Kind kann auch keine Typdeklaration auslassen, da dies ebenfalls das Fehlen eines Wertes ermöglicht.

class A
{
    public function foo(mixed $foo): mixed
    {}
}

class B extends A
{
    public function foo($foo): string
    {}
}

Union-Typen können auch in den Parametern erweitert und in den Rückgabewerten eingegrenzt werden:

class A
{
    public function foo(string|int $foo): string|int
    {}
}

class B extends A
{
    public function foo(string|int|float $foo): string
    {}
}

Außerdem kann false im Parameter zu bool erweitert werden oder umgekehrt bool zu false im Rückgabewert.

Alle Verstöße gegen die Kovarianz/Kontravarianz führen in PHP 8.0 zu einem fatalen Fehler.

*In den nächsten Teilen dieser Serie werden wir zeigen, was die Attribute sind, welche neuen Funktionen und Klassen in PHP erschienen sind und wir werden den Just in Time Compiler vorstellen.