PHP 8.0: повний огляд новин (1/4)

3 рік тому Від David Grudl  

Прямо зараз виходить версія PHP 8.0. У ній багато нового, як у жодній іншій версії. Їх представлення заслуговує на чотири окремі статті. У першій з них ми розглянемо, що нового з'явилося на рівні мови.

Перш ніж ми заглибимося в PHP, повідомляємо, що поточна версія Nette повністю готова до восьмої версії. Більше того, в якості подарунка було випущено повністю сумісну Nette 2.4, тому з точки зору фреймворку ніщо не заважає вам використовувати її.

Іменовані аргументи

Почнемо одразу з бомби, яку можна сміливо назвати зміною правил гри. Аргументи тепер можна передавати функціям і методам не тільки позиційно, але й за іменами. Що дуже круто у випадку, якщо метод має дуже багато параметрів:

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
	) {
		...
	}
}

Перші два аргументи передаються позиційно, інші за іменами: (імена повинні йти після позиційних)

$response->setCookie('lang', $lang, sameSite: 'None');

// instead of the horrible
$response->setCookie('lang', $lang, null, null, null, null, null, 'None');

Перед впровадженням цієї функції були плани створити новий API для відправки cookies в Nette, тому що кількість аргументів для setCookie() дійсно зростала, а позиційна нотація збивала з пантелику. Це більше не потрібно, тому що іменовані аргументи в даному випадку є найзручнішим API. IDE підкаже їх, і є безпека типів.

Вони ідеально підходять навіть для пояснення логічних аргументів, де їх використання не є обов'язковим, але звичайні true або false не підходять:

// before
$db = $container->getService(Database::class, true);

// now
$db = $container->getService(Database::class, need: true);

Імена аргументів тепер є частиною публічного API. Їх більше не можна змінювати за власним бажанням. Через це навіть Nette проходить аудит, визначаючи, чи всі аргументи мають відповідну назву.

Іменовані аргументи також можна використовувати у поєднанні з варіадами:

function variadics($a, ...$args) {
	dump($args);
}

variadics(a: 1, b: 2, c: 3);
// $args will contain ['b' => 2, 'c' => 3]

Масив $args тепер може містити навіть нечислові ключі, що є своєрідним порушенням БК. Те саме стосується і поведінки call_user_func_array($func, $args), де ключі в масиві $args тепер відіграють набагато важливішу роль. Функції сімейства func_*(), навпаки, захищені від іменованих аргументів.

Іменовані аргументи тісно пов'язані з тим, що оператор розбиття ... тепер може розширювати асоціативні масиви:

variadics(...['b' => 2, 'c' => 3]);

На диво, наразі він не працює всередині масивів:

$arr = [ ...['a' => 1, 'b' => 2] ];
// Fatal error: Cannot unpack array with string keys

Поєднання іменованих аргументів і варіадиків дає можливість нарешті мати фіксований синтаксис, наприклад, для методу link(), якому ми можемо передавати як іменовані аргументи, так і позиційні:

// 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);

Синтаксис іменованих аргументів набагато сексуальніший, ніж написання масивів, тому “Latte негайно взяла його:https://blog.nette.org/…ot-for-least#… на озброєння”, де його можна використовувати, наприклад, у тегах {include} та {link}:

{include 'file.latte' arg1: 1, arg2: 2}
{link default page: 1}

Ми повернемося до іменованих аргументів у третій частині статті у зв'язку з атрибутами.

Вираз може згенерувати виключення

Виняток тепер може згенерувати вираз. Ви можете взяти його у круглі дужки і додати до умови if. Звучить не дуже практично. Однак, це набагато цікавіше:

// 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');

Оскільки стрілочні функції досі могли містити лише один вираз, тепер вони можуть генерувати винятки завдяки цій можливості:

// only single expression
$fn = fn() => throw new \Exception('oops');

Вираз відповідності

Вираз switch-case має дві основні вади:

  • воно використовує нестроге порівняння == замість ===
  • потрібно бути уважним, щоб помилково не забути про break

Через це в PHP є альтернатива у вигляді нового виразу match, який використовує строге порівняння і, навпаки, не використовує break.

switch Приклад коду:

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;
}

І те ж саме (тільки при строгому порівнянні), написане з використанням match:

$message = match ($statusCode) {
    200, 300 => $this->formatMessage('ok'),
    400 => $this->formatMessage('not found'),
    500 => $this->formatMessage('server error'),
    default => 'unknown status code',
};

Зверніть увагу, що match – це не керуюча структура, як switch, а вираз. У прикладі ми присвоюємо його результуюче значення змінній. При цьому окремі “опції” також є виразами, тому немає можливості написати більше кроків, як у випадку з switch.

У разі відсутності збігу (а умови за замовчуванням немає) генерується виключення UnhandledMatchError.

До речі, в Latte також є теги {switch}, {case} і {default}. Їх функція точно відповідає новому match. Вони використовують строге порівняння, не вимагають break і в case можна вказати декілька значень, розділених комами.

Оператор Nullsafe

Необов'язковий ланцюжок дозволяє написати вираз, обчислення якого зупиняється, якщо зустрічається нуль. Це можливо завдяки новому оператору ?->. Він замінює багато коду, який інакше довелося б багаторазово перевіряти на null:

$user?->getAddress()?->street

// approximately translates to
$user !== null && $user->getAddress() !== null
	? $user->getAddress()->street
	: null

Чому “приблизно”? Тому що насправді вираз обчислюється більш хитромудро, тому жоден крок не повторюється. Наприклад, $user->getAddress() викликається лише один раз, тому проблема, пов'язана з тим, що метод повертає щось різне вперше і вдруге, не може виникнути.

Ця функція була привнесена “Latte рік тому:https://blog.nette.org/…om-functions”. Тепер її перейняв і сам PHP. Чудово.

Просування нерухомості конструктора

Синтаксичний цукор, який дозволяє не писати тип двічі, а змінну – чотири рази. Шкода, що він не з'явився в той час, коли у нас не було таких розумних IDE, які сьогодні пишуть його за нас 🙂

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,
	) {}
}

Він працює з Nette DI, можете починати користуватися.

Суворіші перевірки типів для арифметичних та бітових операторів

Те, що колись допомогло динамічним мовам сценаріїв піднятися на вершину популярності, стало їх найслабшим місцем. Колись PHP позбувся “магічних лапок”, реєстрації глобальних змінних, а тепер на зміну розкутості приходить суворість. Час, коли в PHP можна було додавати, множити і т.д. практично будь-які типи даних, для яких це не мало сенсу, давно минув. Починаючи з версії 7.0, PHP стає все більш і більш суворим, а починаючи з версії 8.0, спроба використовувати будь-які арифметичні/бітові оператори над масивами, об'єктами або ресурсами закінчується TypeError. Винятком є додавання масивів.

// arithmetic and bitwise operators
+, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, --:

Більш розумне порівняння рядка з числом

Або знову зробити оператор loose чудовим.

Здавалося б, для оператора loose більше немає місця ==, що це просто помилка при написанні ===, але ця зміна знову повертає його на карту. Якщо він у нас вже є, то нехай поводиться розумно. В результаті попереднього “нерозумного” порівняння, наприклад, in_array() може вас неприємно тролити:

$validValues = ['foo', 'bar', 'baz'];
$value = 0;

dump(in_array($value, $validValues));
// surprisingly returned true
// since PHP 8.0 returns false

Зміна поведінки == стосується порівняння чисел і “числових” рядків і показана в наступній таблиці:

Порівняння До 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

Дивно, але це розрив у самій основі мови, який був схвалений без жодного опору. І це добре. JavaScript міг би дуже позаздрити в цьому відношенні.

Повідомлення про помилки

Багато внутрішніх функцій тепер викликають TypeError і ValueError замість попереджень, які було легко ігнорувати. Ряд попереджень ядра було перекласифіковано. Оператор завершення роботи @ тепер не замовчує фатальні помилки. А PDO за замовчуванням генерує виключення.

Нетте завжди намагалася якось вирішити ці проблеми. Трейсі змінив поведінку оператора shutup, Database змінив поведінку PDO, Utils містить заміну стандартним функціям, які генерують виключення замість попереджень, які легко пропустити, і т.д. Приємно бачити, що суворий напрямок, закладений в ДНК Nette, стає рідним напрямком мови.

Від'ємні прирости ключів масиву

$arr[-5] = 'first';
$arr[] = 'second';

Яким буде ключ другого елементу? Раніше це був 0, since PHP 8 it’s -4.

Крапка з комою

Останнім місцем, де кома не могла бути, було визначення аргументів функції. Це вже в минулому:

	public function __construct(
		Nette\Database\Connection $db,
		Nette\Mail\Mailer $mailer, // trailing comma
	) {
		....
	}

$object::class

Магічна константа ::class також працює з об'єктами $object::class, повністю замінюючи функцію get_class().

Ловити виключення тільки за типом

І наостанок: не обов'язково вказувати змінну для виключення в операторі catch:

try {
	$container->getService(Database::class);

} catch (MissingServiceException) {  // no $e
	$logger->log('....');
}

У наступних частинах ми розглянемо основні нововведення щодо типів даних, покажемо, що таке атрибути, які нові функції і класи з'явилися в PHP, а також познайомимося з компілятором Just in Time.