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ć