PHP 8.0: Kompletny przegląd nowości (1/4)

4 lata temu przez David Grudl  

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.