PHP 8.0: Kompletny przegląd nowości (1/4)
PHP w wersji 8.0 jest właśnie wydawane. Jest ona pełna nowych rzeczy, jak żadna inna wersja wcześniej. Ich wprowadzenie zasługiwało na cztery osobne artykuły. W pierwszym z nich przyjrzymy się temu, co wnosi na poziomie języka.
Zanim zagłębimy się w PHP, niech będzie wiadomo, że obecna wersja Nette jest w pełni przygotowana na wersję ósmą. Co więcej, w prezencie została wydana w pełni kompatybilna Nette 2.4, więc z punktu widzenia frameworka nic nie stoi na przeszkodzie, aby go używać.
Argumenty nazwane
Zacznijmy od razu od bomby, którą można śmiało określić jako game changer. Argumenty mogą być teraz przekazywane do funkcji i metod nie tylko pozycyjnie, ale zgodnie z ich nazwą. Co jest absolutnie fajne w przypadku, gdy metoda ma naprawdę za 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 są przekazywane pozycyjnie, pozostałe po nazwie: (nazwane muszą następować po pozycyjnych)
$response->setCookie('lang', $lang, sameSite: 'None');
// instead of the horrible
$response->setCookie('lang', $lang, null, null, null, null, null, 'None');
Przed wprowadzeniem tej funkcji były plany stworzenia nowego API do
wysyłania ciasteczek w Nette, ponieważ liczba argumentów dla
setCookie()
naprawdę rosła, a notacja pozycyjna była myląca.
Nie jest to już potrzebne, ponieważ nazwane argumenty są w tym przypadku
najwygodniejszym API. IDE będzie je podpowiadać i jest
bezpieczeństwo typu.
Idealnie nadają się nawet do wyjaśniania argumentów logicznych, gdzie ich
użycie nie jest konieczne, ale zwykły true
lub false
nie wycina tego:
// before
$db = $container->getService(Database::class, true);
// now
$db = $container->getService(Database::class, need: true);
Nazwy argumentów są teraz częścią publicznego API. Nie można już ich dowolnie zmieniać. Z tego powodu nawet Nette przechodzi audyt określający, czy wszystkie argumenty mają odpowiednią nazwę.
Nazwane argumenty mogą być również używane w połączeniu z variadics:
function variadics($a, ...$args) {
dump($args);
}
variadics(a: 1, b: 2, c: 3);
// $args will contain ['b' => 2, 'c' => 3]
Tablica $args
może teraz zawierać nawet klucze nienumeryczne,
co jest swego rodzaju przerwą BC. To samo dotyczy zachowania
call_user_func_array($func, $args)
, gdzie klucze w tablicy
$args
odgrywają teraz znacznie bardziej znaczącą rolę.
Przeciwnie, funkcje rodziny func_*()
są osłonięte przed
nazwanymi argumentami.
Nazwane argumenty są ściśle związane z faktem, że operator splat
...
może teraz rozszerzać tablice asocjacyjne:
variadics(...['b' => 2, 'c' => 3]);
Zaskakująco nie działa to obecnie wewnątrz tablic:
$arr = [ ...['a' => 1, 'b' => 2] ];
// Fatal error: Cannot unpack array with string keys
Połączenie named arguments i variadics daje możliwość, aby w końcu
mieć stałą składnię, na przykład dla metody link()
, do
której możemy przekazać argumenty named, jak i pozycyjne:
// before
$presenter->link('Product:detail', $id, 1, 2);
$presenter->link('Product:detail', [$id, 'page' => 1]); // had to be an array
// now
$presenter->link('Product:detail', $id, page: 1);
Składnia dla named arguments jest znacznie bardziej seksowna niż pisanie
tablic, dlatego Latte
natychmiast ją zaadoptowała, gdzie można ją wykorzystać np. w metodzie
{include}
i {link}
:
{include 'file.latte' arg1: 1, arg2: 2}
{link default page: 1}
Do named arguments wrócimy w trzeciej części serii w związku z atrybutami.
Wyrażenie może rzucić wyjątek
Rzucanie wyjątku jest teraz wyrażeniem. Możesz zawinąć go w nawiasy
i dodać do warunku if
. Hmmm, to nie brzmi zbyt praktycznie. Jest
to jednak o wiele bardziej interesujące:
// before
if (!isset($arr['value'])) {
throw new \InvalidArgumentException('value not set');
}
$value = $arr['value'];
// now, when throw is an expression
$value = $arr['value'] ?? throw new \InvalidArgumentException('value not set');
Ponieważ funkcje strzałkowe mogą do tej pory zawierać tylko jedno wyrażenie, dzięki tej właściwości mogą teraz rzucać wyjątki:
// only single expression
$fn = fn() => throw new \Exception('oops');
Dopasuj wyrażenie
Instrukcja switch-case
ma dwie główne wady:
- używa nierygorystycznego porównania
==
zamiast===
- musisz uważać, aby przez pomyłkę nie zapomnieć o
break
Z tego powodu, PHP posiada alternatywę w postaci nowego wyrażenia
match
, które używa ścisłego porównania i, odwrotnie, nie
używa break
.
switch
przykład kodu:
switch ($statusCode) {
case 200:
case 300:
$message = $this->formatMessage('ok');
break;
case 400:
$message = $this->formatMessage('not found');
break;
case 500:
$message = $this->formatMessage('server error');
break;
default:
$message = 'unknown status code';
break;
}
I ten sam (tylko ze ścisłym porównaniem) napisany przy użyciu
match
:
$message = match ($statusCode) {
200, 300 => $this->formatMessage('ok'),
400 => $this->formatMessage('not found'),
500 => $this->formatMessage('server error'),
default => 'unknown status code',
};
Zauważ, że match
nie jest strukturą sterującą jak
switch
, ale wyrażeniem. W przykładzie przypisujemy jego wartość
wynikową do zmiennej. Jednocześnie poszczególne “opcje” są również
wyrażeniami, więc nie ma możliwości napisania większej liczby kroków, jak
w przypadku switch
.
W przypadku braku dopasowania (a nie ma klauzuli default) rzucany jest
wyjątek UnhandledMatchError
.
Przy okazji, w Latte istnieją również tagi {switch}
,
{case}
i {default}
. Ich funkcja odpowiada dokładnie
nowej match
. Używają one ścisłego porównania, nie wymagają
break
i możliwe jest określenie wielu wartości oddzielonych
przecinkami w case
.
Operator Nullsafe
Opcjonalne łańcuchowanie pozwala na napisanie wyrażenia, którego ocena
zatrzyma się, jeśli napotka null. Jest to możliwe dzięki nowemu operatorowi
?->
. Zastępuje on wiele kodu, który w przeciwnym razie
musiałby wielokrotnie sprawdzać, czy nie ma wartości null:
$user?->getAddress()?->street
// approximately translates to
$user !== null && $user->getAddress() !== null
? $user->getAddress()->street
: null
Dlaczego “w przybliżeniu”? Ponieważ w rzeczywistości wyrażenie jest
oceniane bardziej pomysłowo, więc żaden krok nie jest powtarzany. Na
przykład $user->getAddress()
jest wywoływany tylko raz, więc
nie można napotkać problemu spowodowanego przez metodę zwracającą coś
innego za pierwszym i drugim razem.
Ta cecha została przyniesiona przez Latte rok temu. Teraz przyjmuje ją sam PHP. Świetnie.
Promocja właściwości konstruktora
Cukier syntaktyczny, który oszczędza pisanie typu dwa razy i zmiennej cztery razy. Szkoda, że nie pojawił się w czasach, gdy nie mieliśmy tak sprytnych IDE, które dziś piszą go 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,
) {}
}
Działa z Nette DI, można zacząć go używać.
Bardziej rygorystyczne sprawdzanie typów dla operatorów arytmetycznych i bitowych
To, co kiedyś pomogło dynamicznym językom skryptowym wznieść się na wyżyny, stało się ich najsłabszym punktem. Kiedyś PHP pozbył się “magicznych cudzysłowów”, rejestracji zmiennych globalnych, a teraz luźne zachowanie jest zastępowane przez rygorystyczność. Czasy, gdy w PHP można było dodawać, mnożyć itp. niemal dowolny typ danych, dla którego nie miało to sensu, dawno minęły. Począwszy od wersji 7.0, PHP staje się coraz bardziej rygorystyczny i od wersji 8.0, próba użycia jakichkolwiek operatorów arytmetycznych/bitewnych na tablicach, obiektach lub zasobach kończy się TypeError. Wyjątkiem jest dodawanie tablic.
// arithmetic and bitwise operators
+, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, --:
Bezpieczniejsze porównywanie ciągów do liczb
Albo spraw, by luźny operator był znowu wielki.
Wydawałoby się, że nie ma już miejsca dla operatora loose
==
, że to tylko literówka przy pisaniu ===
, ale ta
zmiana przywraca go ponownie na mapę. Jeśli już go mamy, to niech zachowuje
się rozsądnie. W wyniku wcześniejszego “nierozsądnego” porównania, na
przykład in_array()
może cię nieprzyjemnie trollować:
$validValues = ['foo', 'bar', 'baz'];
$value = 0;
dump(in_array($value, $validValues));
// surprisingly returned true
// since PHP 8.0 returns false
Zmiana zachowania ==
dotyczy porównywania liczb i ciągów
“numerycznych” i jest przedstawiona w poniższej tabeli:
Porównanie | Przed | 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 |
Co zaskakujące, jest to złamanie BC w samym rdzeniu języka, które zostało zatwierdzone bez oporu. I to jest dobre. JavaScript mógłby być bardzo zazdrosny pod tym względem.
Raportowanie błędów
Wiele wewnętrznych funkcji wywołuje teraz TypeError i ValueError zamiast
ostrzeżeń, które łatwo było zignorować. Wiele ostrzeżeń jądra zostało
przeklasyfikowanych. Operator zamykania @
nie wycisza teraz
błędów śmiertelnych. A PDO domyślnie rzuca wyjątki.
Nette zawsze starał się w jakiś sposób rozwiązać te sprawy. Tracy zmodyfikował zachowanie operatora shutup, Database przełączył zachowanie PDO, Utils zawiera zamienniki standardowych funkcji, które rzucają wyjątki zamiast łatwych do przeoczenia ostrzeżeń, itp. Miło jest zobaczyć, że ścisły kierunek, który Nette ma w swoim DNA, staje się natywnym kierunkiem języka.
Ujemne przyrosty kluczy tablicowych
$arr[-5] = 'first';
$arr[] = 'second';
Jaki będzie klucz drugiego elementu? Kiedyś było to 0
, since
PHP 8 it’s -4
.
Przecinek pośladkowy
Ostatnim miejscem, w którym nie mogło być przecinka trailingowego, była definicja argumentów funkcji. To już przeszłość:
public function __construct(
Nette\Database\Connection $db,
Nette\Mail\Mailer $mailer, // trailing comma
) {
....
}
$object::class
Magiczna stała ::class
działa również z obiektami
$object::class
, całkowicie zastępując funkcję
get_class()
.
Łapanie wyjątków tylko według typu
I na koniec: nie jest konieczne określanie zmiennej dla wyjątku w klauzuli catch:
try {
$container->getService(Database::class);
} catch (MissingServiceException) { // no $e
$logger->log('....');
}
*W kolejnych częściach zobaczymy główne nowości dotyczące typów danych, pokażemy, czym są atrybuty, jakie nowe funkcje i klasy pojawiły się w PHP oraz przedstawimy kompilator Just in Time.
Aby przesłać komentarz, proszę się zalogować