PHP 8.0: Nowości w typach danych (2/4)

3 lata temu Ze strony David Grudl  

PHP w wersji 8.0 zostało właśnie wydane. Jest ona pełna nowych funkcji, jak żadna wcześniejsza wersja. Ich wprowadzenie zasługuje na cztery osobne artykuły. W drugiej części przyjrzymy się typom danych.

Cofnijmy się do historii. Wprowadzenie podpowiedzi typów skalarnych było znaczącym przełomem w PHP 7. O mały włos nie doszło do niego. Andreu Faulds, autor genialnego rozwiązania, które było całkowicie kompatybilne wstecz i opcjonalne dzięki declare(strict_types=1), został ostro odrzucony przez społeczność. Na szczęście Anthony Ferrara bronił jej i propozycji w tym czasie, rozpoczął kampanię i RFC przeszedł bardzo blisko. Whew. Większość nowości w PHP 8 jest dzięki uprzejmości legendarnego Nikity Popova i wszystkie przeszły przez głosowanie jak nóż przez masło. Świat zmienia się na lepsze.

PHP 8 doprowadza typy do perfekcji. Zdecydowana większość adnotacji phpDoc jak @param, @return i @var zostanie zastąpiona natywną notacją. Ale co najważniejsze, typy będą sprawdzane przez silnik PHP. W komentarzach pozostaną jedynie opisy struktur takich jak string[] czy bardziej złożone adnotacje dla PHPStan.

Typy związków

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

class Button
{
	private string|object $caption;

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

Niektóre typy unii zostały wprowadzone do PHP już wcześniej. Na przykład typy nullable. Takie jak ?string, który jest równoważny typowi unii string|null. Notacja znaku zapytania może być uważana za skrót. Oczywiście działa on również w PHP 8, ale nie można go połączyć z pionowymi paskami. Tak więc zamiast ?string|object musisz napisać string|object|null. Ponadto, iterable zawsze był równoważny array|Traversable. Możesz być zaskoczony, że float jest również typem związkowym, ponieważ akceptuje zarówno int|float, ale rzutuje wartość na float.

Nie można używać pseudotypów void i mixed w związkach, ponieważ nie miałoby to 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)
{
	...
}

Z drugiej strony, autowiring w Nette DI odrzuca typy unii. Jak dotąd nie ma przypadku użycia, w którym miałoby sens, aby konstruktor akceptował taki lub inny obiekt. Oczywiście, jeśli taki use-case 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 union (tak jest w wersji 3.1; we wcześniejszej wersji 3.0 metody te zwracają, aby zachować kompatybilność null).

mixed

Pseudotyp mixed akceptuje dowolną wartość.

W przypadku parametrów i właściwości powoduje takie samo zachowanie, jak gdybyśmy nie określili żadnego typu. Jaki jest więc jego cel? Aby rozróżnić, kiedy typ jest jedynie pominięty, a kiedy jest celowo mixed.

W przypadku wartości zwracanych przez funkcje i metody, nieokreślenie typu różni się od użycia mixed. Oznacza to przeciwieństwo void, ponieważ wymaga, aby funkcja zwróciła coś. Brakujący return wywołuje wtedy błąd fatalny.

W praktyce powinieneś rzadko go używać, ponieważ dzięki typom unii możesz dokładnie określić wartość. Dlatego nadaje się tylko w wyjątkowych sytuacjach:

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

false

W przeciwieństwie do mixed, nowego pseudotypu false można używać wyłącznie w typach unii. Powstał on z potrzeby opisania typu zwracanego przez funkcje natywne, które historycznie zwracają false w przypadku niepowodzenia:

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

Dlatego nie ma typu true. Nie można też używać samego false, ani false|null, ani bool|false.

static

Pseudotyp static może być użyty tylko jako typ zwrotny 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). Jest to doskonałe rozwiązanie do opisywania płynnych interfejsów:

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 nie istnieje w PHP 8 i nie będzie wprowadzony w przyszłości. Zasoby są reliktem z czasów, gdy PHP nie posiadał obiektów. Stopniowo zasoby będą zastępowane przez obiekty. W końcu znikną całkowicie. Na przykład PHP 8.0 zastępuje zasób image obiektem GdImage, a zasób curl obiektem CurlHandle.

Stringable

Jest to interfejs, który zostaje automatycznie zaimplementowany przez 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);

Możliwe jest wyraźne stwierdzenie class Email implements Stringable w definicji klasy, ale nie jest to konieczne.

Nette\Utils\Html również odzwierciedla ten schemat nazewnictwa poprzez implementację interfejsu Nette\HtmlStringable zamiast dawnego IHtmlString. Obiekty tego typu, na przykład, nie są wymykane przez Latte.

Wariancja typu, kontrawariancja, kowariancja

Zasada substytucji Liskov (LSP) mówi, że klasy rozszerzające i implementacje interfejsów nigdy nie mogą wymagać więcej i dostarczać mniej niż rodzic. To znaczy, metoda dziecka nie może wymagać więcej argumentów lub akceptować węższego zakresu typów dla parametrów niż rodzic i odwrotnie, nie może zwrócić szerszego zakresu typów. Ale może zwrócić mniejszą liczbę. Dlaczego. Ponieważ w przeciwnym razie dziedziczenie zostałoby złamane. Funkcja akceptowałaby obiekt określonego typu, ale nie miałaby pojęcia, jakie parametry może przekazać do swoich metod i jakie typy zwrócą. Każde dziecko mogłoby ją złamać.

Więc w OOP, klasa dziecka może:

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

PHP potrafi to robić od wersji 7.4, a wszystkie nowo wprowadzone typy w PHP 8.0 również obsługują contravariance i covariance.

W przypadku mixed, dziecko może zawęzić wartość zwracaną do dowolnego typu, ale nie void, ponieważ nie reprezentuje ona wartości, ale raczej jej brak. Dziecko nie może również pominąć deklaracji typu, ponieważ pozwala to również na brak wartości.

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

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

Typy Unii mogą być również rozszerzane w parametrach i zawężane w wartościach zwracanych:

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

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

Ponadto false można rozszerzyć do bool w parametrze lub odwrotnie bool do false w wartości zwrotnej.

Wszystkie wykroczenia przeciwko kowariancji/kontrawariancji prowadzą do błędu krytycznego w PHP 8.0.

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