PHP 8.0: Kompletny przegląd nowości (1/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 pierwszym przyjrzymy się, co nowego przynosi pod względem językowym.

Zanim zagłębimy się w PHP, wiedzcie, że aktualna wersja Nette jest w pełni gotowa na ósemkę. Co więcej, jako prezent wydano nawet Nette 2.4, które jest z nią w pełni kompatybilne, więc z punktu widzenia frameworka nic nie stoi na przeszkodzie, aby zacząć używać nowej wersji.
Nazwane argumenty
A zaczniemy od razu bombą, którą śmiało można określić jako game changer. Od teraz można przekazywać argumenty do funkcji i metod nie tylko pozycyjnie, ale także według nazw. Co jest absolutnie świetne w przypadku, gdy metoda ma naprawdę dużo parametrów:
class Response implements IResponse
{
public function setCookie(
string $name,
string $value,
string|DateInterface|null $time,
string $path = null,
string $domain = null,
bool $secure = null,
bool $httpOnly = null,
string $sameSite = null
) {
...
}
}
Pierwsze dwa argumenty przekażemy pozycyjnie, kolejne według nazwy: (nazwane muszą zawsze następować po pozycyjnych)
$response->setCookie('lang', $lang, sameSite: 'None');
// zamiast szalonego
$response->setCookie('lang', $lang, null, null, null, null, null, 'None');
Przed pojawieniem się tej funkcji planowano stworzyć w Nette nowe API do
wysyłania ciasteczek, ponieważ liczba parametrów setCookie()
naprawdę wzrosła, a zapis pozycyjny był nieczytelny. Teraz już nie jest to
potrzebne, ponieważ nazwane argumenty są w tym przypadku najpraktyczniejszym
API. IDE będą je podpowiadać i mają kontrolę typów.
Świetnie nadają się również do wyjaśnienia parametrów logicznych,
gdzie ich użycie wprawdzie nie jest konieczne, ale samo true
lub
false
niewiele mówi:
// wcześniej
$db = $container->getService(Database::class, true);
// teraz
$db = $container->getService(Database::class, need: true);
Nazwy parametrów stają się teraz częścią publicznego API. Nie można ich dowolnie zmieniać jak dotychczas. Z tego powodu również Nette przechodzi audyt, czy wszystkie parametry mają odpowiednie nazwy.
Nazwane argumenty można używać również w połączeniu z variadics:
function variadics($a, ...$args) {
dump($args);
}
variadics(a: 1, b: 2, c: 3);
// w $args będzie ['b' => 2, 'c' => 3]
Od teraz tablica $args
może zawierać również klucze
nienumeryczne, co jest pewnym BC break. To samo dotyczy zachowania funkcji
call_user_func_array($func, $args)
, gdzie teraz klucze w tablicy
$args
odgrywają znaczącą rolę. Natomiast funkcje z rodziny
func_*()
są odizolowane od nazwanych argumentów.
Z nazwanymi argumentami ściśle wiąże się również fakt, że operator
splat ...
może teraz rozpakowywać również tablice
asocjacyjne:
variadics(...['b' => 2, 'c' => 3]);
Co dziwne, na razie nie działa to wewnątrz tablic:
$arr = [ ...['a' => 1, 'b' => 2] ];
// Fatal error: Cannot unpack array with string keys
Połączenie nazwanych argumentów i variadics daje możliwość wreszcie
mieć stałą składnię na przykład dla metody presentera link()
,
której teraz możemy przekazywać nazwane argumenty tak samo jak pozycyjne:
// wcześniej
$presenter->link('Product:detail', $id, 1, 2);
$presenter->link('Product:detail', [$id, 'page' => 1]); // musiała być tablica
// teraz
$presenter->link('Product:detail', $id, page: 1);
Składnia dla nazwanych parametrów jest znacznie bardziej seksowna niż
zapis tablic, dlatego też natychmiast przyswoiło
ją Latte, gdzie można jej używać na przykład w tagach
{include}
i {link}
:
{include 'file.latte' arg1: 1, arg2: 2}
{link default page: 1}
Do nazwanych parametrów wrócimy jeszcze w trzeciej części w kontekście atrybutów.
Wyrażenie może rzucić wyjątek
Rzucenie wyjątku jest teraz wyrażeniem. Można je na przykład opakować w
nawiasy i dodać do warunku if
. Hmmm, to nie brzmi zbyt
praktycznie. Ale to już jest ciekawsze:
// wcześniej
if (!isset($arr['value'])) {
throw new \InvalidArgumentException('wartość nieustawiona');
}
$value = $arr['value'];
// teraz, gdy throw jest wyrażeniem
$value = $arr['value'] ?? throw new \InvalidArgumentException('wartość nieustawiona');
Ponieważ funkcje strzałkowe (arrow functions) mogą dotychczas zawierać tylko jedno wyrażenie, dzięki tej funkcji mogą rzucać wyjątki:
// tylko jedno wyrażenie
$fn = fn() => throw new \Exception('ups');
Wyrażenia Match
Konstrukcja switch-case
ma dwie duże wady:
- używa porównania niestriktnego
==
zamiast===
- trzeba uważać, aby przypadkowo nie zapomnieć o
break
Dlatego PHP wprowadza alternatywę w postaci nowej konstrukcji
match
, która używa porównania ścisłego i odwrotnie nie używa
break
.
Przykład kodu switch
:
switch ($statusCode) {
case 200:
case 300:
$message = $this->formatMessage('ok');
break;
case 400:
$message = $this->formatMessage('nie znaleziono');
break;
case 500:
$message = $this->formatMessage('błąd serwera');
break;
default:
$message = 'nieznany kod statusu';
break;
}
A to samo (tylko ze ścisłym porównaniem) zapisane za pomocą
match
:
$message = match ($statusCode) {
200, 300 => $this->formatMessage('ok'),
400 => $this->formatMessage('nie znaleziono'),
500 => $this->formatMessage('błąd serwera'),
default => 'nieznany kod statusu',
};
Zauważcie, że match
nie jest strukturą kontrolną jak
switch
, ale wyrażeniem. Jego wynikową wartość w przykładzie
przypisujemy do zmiennej. Jednocześnie poszczególne “opcje” są również
wyrażeniami, więc nie można zapisać wielu kroków, jak w przypadku
switch
.
Jeśli nie dojdzie do dopasowania żadnej z opcji (i nie istnieje klauzula
default), zostanie rzucony wyjątek UnhandledMatchError
.
Nawiasem mówiąc, w Latte również istnieją tagi {switch}
,
{case}
i {default}
. Ich działanie odpowiada
dokładnie nowemu match
. Używają ścisłego porównania, nie
wymagają break
, a w case
można podać wiele
wartości oddzielonych przecinkami.
Operator Nullsafe
Opcjonalne łączenie (optional chaining) umożliwia pisanie wyrażenia,
którego ewaluacja zatrzymuje się, jeśli napotka null. A to dzięki nowemu
operatorowi ?->
. Zastąpi mnóstwo kodu, który w przeciwnym
razie wielokrotnie sprawdzałby null:
$user?->getAddress()?->street
// oznacza około
$user !== null && $user->getAddress() !== null
? $user->getAddress()->street
: null
Dlaczego „oznacza około”? Ponieważ w rzeczywistości wyrażenie jest
ewaluowane bardziej pomysłowo i żaden krok się nie powtarza. Na przykład
$user->getAddress()
jest wywoływane tylko raz, więc nie może
wystąpić problem spowodowany tym, że metoda za pierwszym i drugim razem
zwróciłaby coś innego.
Tę świeżą nowinkę przyniosło rok temu Latte. Nyní se dostává do samotného PHP. Paráda.
Promocja właściwości konstruktora
Cukier syntaktyczny, który oszczędza podwójne pisanie typu i czterokrotne pisanie zmiennej. Szkoda tylko, że nie pojawił się w czasach, gdy nie mieliśmy tak inteligentnych IDE, które dzisiaj piszą to za nas 🙂
class Facade
{
private Nette\Database\Connection $db;
private Nette\Mail\Mailer $mailer;
public function __construct(Nette\Database\Connection $db, Nette\Mail\Mailer $mailer)
{
$this->db = $db;
$this->mailer = $mailer;
}
}
class Facade
{
public function __construct(
private Nette\Database\Connection $db,
private Nette\Mail\Mailer $mailer,
) {}
}
Z Nette DI działa, można od razu zacząć używać.
Ścisłe zachowanie operatorów arytmetycznych i bitowych
To, co kiedyś wyniosło dynamiczne języki skryptowe na szczyt, z czasem stało się ich najsłabszym punktem. PHP kiedyś pozbywało się “magic quotes”, rejestrowania zmiennych globalnych, a teraz luźne zachowanie zastępuje ścisłość. Czas, kiedy w PHP można było dodawać, mnożyć itp. prawie dowolne typy danych, dla których nie miało to najmniejszego sensu, dawno minął. Począwszy od wersji 7.0, PHP staje się coraz bardziej ścisłe, a od wersji 8.0 próba użycia jakiegokolwiek operatora arytmetycznego/bitowego na tablicach, obiektach czy zasobach kończy się TypeError. Wyjątkiem jest dodawanie tablic.
// operatory arytmetyczne i bitowe
+, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, --:
Rozsądniejsze porównywanie ciągów znaków i liczb
Czyli make loose operator great again.
Wydawałoby się, że dla luźnego operatora ==
już nie ma
miejsca, że jest to tylko literówka przy pisaniu ===
, ale ta
zmiana przywraca go ponownie na mapę. Skoro już go mamy, niech zachowuje się
rozsądnie. Konsekwencją wcześniejszego “nierozsądnego” porównywania
było na przykład zachowanie in_array()
, które mogło niemiło
zaskoczyć:
$validValues = ['foo', 'bar', 'baz'];
$value = 0;
dump(in_array($value, $validValues));
// zaskakująco zwracało true
// od PHP 8.0 zwraca false
Zmiana w zachowaniu ==
dotyczy porównywania liczb i
“liczbowych” ciągów znaków i pokazuje ją poniższa tabela:
Porównanie | Wcześniej | PHP 8.0 |
---|---|---|
0 == "0" |
true | true |
0 == "0.0" |
true | true |
0 == "foo" |
true | false |
0 == "" |
true | false |
42 == " 42" |
true | true |
42 == "42 " |
true | true |
42 == "42foo" |
true | false |
42 == "abc42" |
false | false |
"42" == " 42" |
true | true |
"42" == "42 " |
false | true |
Zaskakujące jest, że jest to BC break w samym rdzeniu języka, który został zatwierdzony bez żadnego sprzeciwu. I to dobrze. Tutaj JavaScript mógłby bardzo zazdrościć.
Raportowanie błędów
Wiele wewnętrznych funkcji teraz wywołuje TypeError i ValueError zamiast
ostrzeżeń, które łatwo można było przeoczyć. Przeklasyfikowano wiele
ostrzeżeń jądra. Operator shutup @
teraz nie wyciszy błędów
fatalnych. A PDO w domyślnym trybie rzuca wyjątki.
Te rzeczy Nette zawsze starało się w jakiś sposób rozwiązywać. Tracy modyfikowało zachowanie operatora shutup, Database przełączało zachowanie PDO, Utils zawiera zamienniki standardowych funkcji, które rzucają wyjątki zamiast dyskretnych ostrzeżeń itp. Miło widzieć, że ścisły kierunek, który Nette ma w swoim DNA, staje się natywnym kierunkiem języka.
Tablica z indeksem ujemnym
$arr[-5] = 'first';
$arr[] = 'second';
Jaki będzie klucz drugiego elementu? Wcześniej było to 0
, od
PHP 8 jest to -4
.
Końcowy przecinek
Ostatnie miejsce, gdzie nie mógł być końcowy przecinek, to definicja parametrów funkcji. To już przeszłość:
public function __construct(
Nette\Database\Connection $db,
Nette\Mail\Mailer $mailer, // końcowy przecinek
) {
....
}
$object::class
Magiczna stała ::class
działa również z obiektami
$object::class
, całkowicie zastępując funkcję
get_class()
.
catch bez zmiennej
I na koniec: w klauzuli catch nie trzeba podawać zmiennej dla wyjątku:
try {
$container->getService(Database::class);
} catch (MissingServiceException) { // bez $e
$logger->log('....');
}
W kolejnych częściach czekają nas zasadnicze nowości w typach danych, 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ć