PHP 8.0: Typy danych (2/4)

4 lata temu przez David Grudl  

Ukazała się wersja PHP 8.0. Jest tak naładowana nowościami, jak żadna wersja wcześniej. Ich przedstawienie wymagało aż czterech oddzielnych artykułów. W tym drugim przyjrzymy się typom danych.

Wróćmy do historii. Zasadniczym przełomem PHP 7 było wprowadzenie skalarnych type hintów. Prawie do tego nie doszło. Autorkę wspaniałego rozwiązania Andreu Faulds, które dzięki declare(strict_types=1) było całkowicie wstecznie kompatybilne i opcjonalne, społeczność brzydko odrzuciła. Na szczęście jej i jej propozycji wtedy bronił Anthony Ferrara, uruchomił kampanię i RFC bardzo ciasno przeszło. Ufff. Większość nowości w PHP 8 zawdzięczamy legendarnemu Nikita Popov i w głosowaniu przeszły mu jak po maśle. Świat zmienia się na lepsze.

PHP 8 doprowadza typy do doskonałości. Zniknie absolutna większość adnotacji phpDoc @param, @return i @var w kodzie i zastąpi je natywny zapis, a przede wszystkim kontrola przez silnik PHP. W komentarzach pozostaną tylko opisy struktur jak string[] lub bardziej złożone adnotacje dla PHPStan.

Typy unijne

Typy unijne to wyliczenie dwóch lub więcej typów, które może przyjmować wartość:

class Button
{
	private string|object $caption;

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

Niektóre specjalne typy unijne PHP znało już wcześniej. Na przykład typy nullable jak ?string, co jest odpowiednikiem typu unijnego string|null, a zapis ze znakiem zapytania można uznać tylko za skrót. Oczywiście działa to również w PHP 8, ale nie można go łączyć z pionowymi kreskami, więc zamiast ?string|object trzeba pisać pełne string|object|null. Dalej iterable zawsze było odpowiednikiem array|Traversable. Być może zaskoczy Cię, że typem unijnym jest właściwie również float, który w rzeczywistości akceptuje int|float, jednak rzutuje na float.

W uniach nie można używać pseudotypów void i mixed, ponieważ nie miałoby to żadnego sensu.

Nette jest gotowe na typy związkowe. W Schema, Expect::from() akceptuje je, a prezentery również je akceptują. Możesz ich używać na przykład w metodach renderowania i działania:

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

Natomiast autowiring w Nette DI odrzuca typy unijne. Brakuje na razie przypadku użycia, w którym miałoby sens, aby na przykład konstruktor przyjmował albo ten, albo tamten obiekt. Oczywiście, gdy się pojawi, będzie można odpowiednio dostosować zachowanie kontenera.

Metody getParameterType(), getReturnType() i getPropertyType() w Nette\Utils\Reflection rzucają wyjątek w przypadku typu unijnego (w wersji 3.1, w starszej 3.0 zwracają null ze względu na kompatybilność).

mixed

Pseudotyp mixed mówi, że wartość może być absolutnie czymkolwiek.

W przypadku parametrów i właściwości jest to właściwie takie samo zachowanie, jak gdybyśmy nie podali żadnego typu. Do czego więc jest dobry? Aby można było rozróżnić, kiedy typ po prostu brakuje, a kiedy jest naprawdę mixed.

W przypadku wartości zwracanej funkcji i metody niepodanie typu różni się od podania typu mixed. Jest to właściwie przeciwieństwo void, ponieważ mówi, że funkcja musi coś zwrócić. Brakujący return jest wtedy błędem fatalnym.

W praktyce powinieneś go używać rzadko, ponieważ dzięki typom unijnym możesz dokładniej określić wartość. Nadaje się więc w wyjątkowych sytuacjach:

function dump(mixed $var): mixed
{
	// wypisz zmienną
	return $var;
}

false

Nowy pseudotyp false można natomiast używać tylko w typach unijnych. Powstał z potrzeby natywnego opisania typu wartości zwracanej przez funkcje natywne, które historycznie w przypadku niepowodzenia zwracają false:

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

Z tego powodu nie istnieje typ true, nie można używać również samego false lub false|null czy bool|false.

static

Pseudotyp static można użyć tylko jako typ zwracany metody. Mówi, że metoda zwraca obiekt tego samego typu, co sam obiekt (podczas gdy self mówi, że zwraca klasę, w której zdefiniowana jest metoda). Co doskonale nadaje się do opisu 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

Ten typ w PHP 8 nie istnieje i w przyszłości również nie zostanie wprowadzony. Zasoby (Resources) są historycznym reliktem z czasów, gdy PHP jeszcze nie miało obiektów. Stopniowo zasoby będą zastępowane obiektami i z czasem ten typ całkowicie zniknie. Na przykład PHP 8.0 zastępuje zasób reprezentujący obraz obiektem GdImage, a zasób połączenia curl obiektem CurlHandle.

Stringable

Jest to interfejs, który automatycznie implementuje każdy obiekt z magiczną metodą __toString().

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

function print(Stringable|string $s)
{
}

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

W definicji klasy można jawnie podać class Email implements Stringable, ale nie jest to konieczne.

Ten styl nazewnictwa odzwierciedla również Nette\Utils\Html, które implementuje interfejs Nette\HtmlStringable zamiast poprzedniego IHtmlString. Obiektów tego typu np. Latte nie escapuje.

Wariancja typów, kontrawariancja, kowariancja

Zasada substytucji Liskov (Liskov Substitution Principle – LSP) mówi, że potomkowie klasy i implementacje interfejsu nigdy nie mogą wymagać więcej i dostarczać mniej niż rodzic. Czyli, że metoda potomka nie może wymagać więcej argumentów lub akceptować w parametrach węższego zakresu typów niż przodek i odwrotnie nie może zwracać szerszego zakresu typów. Ale może zwracać węższy. Dlaczego? Ponieważ inaczej dziedziczenie w ogóle by nie działało. Funkcja wprawdzie przyjęłaby obiekt określonego typu, ale nie wiedziałaby, jakie parametry można przekazywać metodom i co faktycznie będą zwracać, ponieważ jakikolwiek potomek mógłby to zepsuć.

Tak więc w OOP obowiązuje, że potomek może:

  • w parametrach akceptować szerszy zakres typów (nazywa się to kontrawariancją)
  • zwracać węższy zakres typów (kowariancja)
  • a właściwości nie mogą zmieniać typu (są inwariantne)

PHP potrafi to od wersji 7.4, a wszystkie nowo wprowadzone typy w PHP 8.0 również wspierają kontrawariancję i kowariancję.

W przypadku mixed potomek może zawęzić wartość zwracaną do dowolnego typu, jednak nie void, ponieważ nie jest to typ wartości, ale jej brak. Ani potomek nie może nie podać typu, ponieważ to również dopuszcza brak.

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

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

Również typy unijne można w parametrach rozszerzać, a w wartościach zwracanych zawężać:

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

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

Dalej false może być w parametrze rozszerzone do bool lub odwrotnie bool w wartości zwracanej zawężone do false.

Wszystkie naruszenia kowariancji/kontrawariancji prowadzą w PHP 8.0 do błędu fatalnego.

W kolejnych częściach pokażemy sobie, czym są atrybuty, jakie nowe funkcje i klasy pojawiły się w PHP oraz przedstawimy Just in Time Compiler.