PHP 8.0: Typy danych (2/4)
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.
Aby przesłać komentarz, proszę się zalogować