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

3 lata temu Ze strony David Grudl  

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.