PHP 8.0: полный обзор новостей (1/4)

4 года назад от David Grudl  

PHP версии 8.0 выходит прямо сейчас. Она полна новых вещей, как ни одна другая версия до этого. Их введение заслуживает четырех отдельных статей. В первой из них мы рассмотрим, что она принесет на уровне языка.

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

Именованные аргументы

Начнем сразу с бомбы, которую можно смело обозначить как game changer. Аргументы теперь можно передавать функциям и методам не только позиционно, но и по имени. Что очень круто в случае, если у метода действительно слишком много параметров:

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 теперь может содержать даже нечисловые ключи, что является своего рода разрывом BC. То же самое относится и к поведению call_user_func_array($func, $args), где ключи в массиве $args теперь играют гораздо более значительную роль. Напротив, функции семейства func_*() ограждены от именованных аргументов.

Именованные аргументы тесно связаны с тем, что оператор splat ... теперь может расширять ассоциативные массивы:

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 немедленно взял его на вооружение, где он может быть использован, например, в тегах {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. Это возможно благодаря новому оператору ?->. Он заменяет много кода, который в противном случае должен был бы неоднократно проверять наличие null:

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

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

Почему “примерно”? Потому что в действительности выражение оценивается более хитроумно, так что ни один шаг не повторяется. Например, $user->getAddress() вызывается только один раз, поэтому проблема, вызванная тем, что метод возвращает что-то разное в первый и второй раз, не может возникнуть.

Эта возможность была предложена Latte год назад. Теперь 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 больше нет места ==, что это просто опечатка при написании ===, но это изменение снова возвращает его на карту. Если он уже есть, то пусть ведет себя разумно. В результате предыдущего “неразумного” сравнения, например, 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

Удивительно, но это разрыв BC в самом ядре языка, который был одобрен без какого-либо сопротивления. И это хорошо. JavaScript может быть очень завидным в этом отношении.

Отчет об ошибках

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

Nette всегда старался как-то решить эти проблемы. Tracy изменил поведение оператора 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..

Последние сообщения