PHP 8.0: Datentypen (2/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 zweiten Teil werfen wir einen Blick auf die Datentypen.

Gehen wir zurück in die Geschichte. Der entscheidende Durchbruch von PHP 7 war die Einführung skalarer Type Hints. Fast wäre es dazu nicht gekommen. Die Autorin der erstaunlichen Lösung, Andrea Faulds, die dank declare(strict_types=1) vollständig abwärtskompatibel und optional war, wurde von der Community hässlich abgelehnt. Glücklicherweise setzte sich damals Anthony Ferrara für sie und ihren Vorschlag ein, startete eine Kampagne und das RFC ging sehr knapp durch. Ufff. Die meisten Neuerungen in PHP 8 gehen auf das Konto des legendären Nikita Popov und gingen bei der Abstimmung wie Butter durch. Die Welt verändert sich zum Besseren.

PHP 8 bringt die Typen zur Perfektion. Die absolute Mehrheit der phpDoc-Annotationen @param, @return und @var im Code wird verschwinden und durch native Schreibweise und vor allem durch die Kontrolle der PHP-Engine ersetzt. In Kommentaren bleiben nur Beschreibungen von Strukturen wie string[] oder komplexere Annotationen für PHPStan übrig.

Union-Typen

Union-Typen sind eine Aufzählung von zwei oder mehr Typen, die ein Wert annehmen kann:

class Button
{
	private string|object $caption;

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

Einige spezielle Union-Typen kannte PHP schon früher. Zum Beispiel nullable Typen wie ?string, was dem Union-Typ string|null entspricht, und die Fragezeichen-Schreibweise kann nur als Abkürzung betrachtet werden. Natürlich funktioniert sie auch in PHP 8, kann aber nicht mit senkrechten Strichen kombiniert werden, d.h. anstelle von ?string|object muss das vollständige string|object|null geschrieben werden. Weiterhin war iterable immer ein Äquivalent zu array|Traversable. Vielleicht überrascht es Sie, dass auch float eigentlich ein Union-Typ ist, der int|float akzeptiert, jedoch zu float umwandelt.

In Union-Typen können die Pseudotypen void und mixed nicht verwendet werden, 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)
{
	...
}

Im Gegensatz dazu lehnt Autowiring in Nette DI Union-Typen ab. Es fehlt bisher ein Anwendungsfall, bei dem es sinnvoll wäre, dass beispielsweise ein Konstruktor entweder das eine oder das andere Objekt akzeptiert. Natürlich, wenn sich ein solcher Fall ergibt, kann das Verhalten des Containers entsprechend angepasst werden.

Die Methoden getParameterType(), getReturnType() und getPropertyType() in Nette\Utils\Reflection werfen im Falle eines Union-Typs eine Ausnahme (in Version 3.1, in der älteren Version 3.0 geben sie aus Kompatibilitätsgründen null zurück).

mixed

Der Pseudotyp mixed besagt, dass der Wert absolut alles sein kann.

Im Falle von Parametern und Properties ist dies eigentlich dasselbe Verhalten, als ob wir keinen Typ angeben würden. Wozu ist er also gut? Um unterscheiden zu können, wann der Typ einfach fehlt und wann er wirklich mixed ist.

Im Falle des Rückgabewerts einer Funktion oder Methode unterscheidet sich die Nichtangabe des Typs von der Angabe des Typs mixed. Es ist eigentlich das Gegenteil von void, da es besagt, dass die Funktion etwas zurückgeben muss. Ein fehlendes return ist dann ein fataler Fehler.

In der Praxis sollten Sie ihn selten verwenden, da Sie dank Union-Typen den Wert genauer spezifizieren können. Er eignet sich also in Ausnahmesituationen:

function dump(mixed $var): mixed
{
	// Variable ausgeben
	return $var;
}

false

Der neue Pseudotyp false kann hingegen nur in Union-Typen verwendet werden. Er entstand aus der Notwendigkeit, den Typ des Rückgabewerts bei nativen Funktionen nativ zu beschreiben, die historisch im Fehlerfall false zurückgeben:

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

Aus diesem Grund existiert kein Typ true, es kann auch nicht false allein oder false|null oder bool|false verwendet werden.

static

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

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 in PHP 8 nicht und wird auch in Zukunft nicht eingeführt werden. Ressourcen sind ein historisches Relikt aus Zeiten, als PHP noch keine Objekte hatte. Nach und nach werden Ressourcen durch Objekte ersetzt und irgendwann wird dieser Typ vollständig verschwinden. Zum Beispiel ersetzt PHP 8.0 die Ressource, die ein Bild darstellt, durch das Objekt GdImage und die Ressource der curl-Verbindung durch das Objekt CurlHandle.

Stringable

Dies ist ein Interface, das automatisch von jedem Objekt mit der magischen Methode __toString() implementiert wird.

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

function print(Stringable|string $s)
{
}

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

In der Klassendefinition kann explizit class Email implements Stringable angegeben werden, dies ist jedoch nicht notwendig.

Dieser Benennungsstil spiegelt sich auch in Nette\Utils\Html wider, das das Interface Nette\HtmlStringable anstelle des vorherigen IHtmlString implementiert. Objekte dieses Typs werden dann z.B. von Latte nicht escaped.

Typvarianz, Kontravarianz, Kovarianz

Das Liskovsche Substitutionsprinzip (LSP) besagt, dass Nachkommen einer Klasse und Implementierungen eines Interfaces niemals mehr verlangen und weniger bereitstellen dürfen als der Elternteil. Das heißt, dass die Methode eines Nachkommen nicht mehr Argumente verlangen oder bei Parametern einen engeren Typbereich akzeptieren darf als der Vorfahre und umgekehrt keinen breiteren Typbereich zurückgeben darf. Sie kann aber einen engeren zurückgeben. Warum? Weil sonst die Vererbung überhaupt nicht funktionieren würde. Eine Funktion würde zwar ein Objekt eines bestimmten Typs akzeptieren, wüsste aber nicht, welche Parameter an die Methoden übergeben werden können und was sie tatsächlich zurückgeben werden, da jeder beliebige Nachkomme dies untergraben könnte.

Daher gilt in OOP, dass ein Nachkomme:

  • in Parametern einen breiteren Typbereich akzeptieren kann (dies nennt man Kontravarianz)
  • einen engeren Typbereich zurückgeben kann (Kovarianz)
  • und Properties den Typ nicht ändern können (sie sind invariant)

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

Im Falle von mixed kann der Nachkomme den Rückgabewert auf jeden beliebigen Typ einschränken, jedoch nicht auf void, da es sich nicht um einen Werttyp, sondern um dessen Abwesenheit handelt. Auch der Nachkomme kann den Typ nicht weglassen, da dies ebenfalls die Abwesenheit zulässt.

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

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

Auch Union-Typen können in Parametern erweitert und in Rückgabewerten eingeschränkt werden:

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

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

Weiterhin kann false im Parameter auf bool erweitert werden oder umgekehrt bool im Rückgabewert auf false eingeschränkt werden.

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

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