PHP 8.0: полный обзор новостей (1/4)
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..
Чтобы оставить комментарий, пожалуйста, войдите в систему