PHP 8.0: Пълен преглед на новостите (1/4)
Излезе PHP версия 8.0. Тя е толкова натъпкана с новости, колкото никоя версия преди нея. Представянето им изисква цели четири отделни статии. В тази първа ще разгледаме какво ново носи от гледна точка на езика.

Преди да се потопим в PHP, знайте, че актуалната версия на Nette е напълно готова за осмицата. Нещо повече, като подарък дори излезе и Nette 2.4, което е напълно съвместимо с нея, така че от гледна точка на framework-а нищо не ви пречи да започнете да използвате новата версия.
Именувани аргументи
И ще започнем направо с бомба, която смело може да се нарече 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');
// вместо безумното
$response->setCookie('lang', $lang, null, null, null, null, null, 'None');
Преди появата на тази функция беше
планирано да се създаде ново API в Nette за
изпращане на бисквитки, тъй като броят на
параметрите на setCookie()
наистина
нарасна и позиционният запис беше неясен.
Сега това вече не е необходимо, защото
именуваните аргументи в този случай са
най-практичното API. IDE ще ги подсказва и те
имат проверка на типовете.
Страхотно се вписват и за доуточняване на
логически параметри, където тяхното
използване не е необходимо, но самото
true
или false
не казва много:
// преди
$db = $container->getService(Database::class, true);
// сега
$db = $container->getService(Database::class, need: true);
Имената на параметрите сега стават част от публичното API. Не е възможно да се променят произволно както досега. Поради тази причина и Nette преминава през одит, дали всички параметри имат подходящи имена.
Именуваните аргументи могат да се използват и в комбинация с variadics:
function variadics($a, ...$args) {
dump($args);
}
variadics(a: 1, b: 2, c: 3);
// в $args ще бъде ['b' => 2, 'c' => 3]
Вече масивът $args
може да съдържа и
ненумерични ключове, което е известна BC break.
Същото се отнася и за поведението на
функцията 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
Комбинацията от именувани аргументи и
variadics дава възможност най-накрая да имаме
фиксирана синтаксис например за метода на
презентера link()
, на който сега можем
да предаваме именувани аргументи по същия
начин като позиционни:
// преди
$presenter->link('Product:detail', $id, 1, 2);
$presenter->link('Product:detail', [$id, 'page' => 1]); // трябваше да бъде масив
// сега
$presenter->link('Product:detail', $id, page: 1);
Синтаксисът за именувани аргументи е
много по-секси от писането на масиви, така
че Latte
веднага го прие, където може да се
използва например в таговете {include}
и
{link}
:
{include 'file.latte' arg1: 1, arg2: 2}
{link default page: 1}
Към именуваните параметри ще се върнем в третия дял във връзка с атрибутите.
Израз може да хвърли изключение
Хвърлянето на изключение вече е израз.
Можете например да го обвиете в скоби и да
го добавите към условие if
. Хммм, това
не звучи много практично. Но това вече е
по-интересно:
// преди
if (!isset($arr['value'])) {
throw new \InvalidArgumentException('стойността не е зададена');
}
$value = $arr['value'];
// сега, когато throw е израз
$value = $arr['value'] ?? throw new \InvalidArgumentException('стойността не е зададена');
Тъй като arrow функциите досега можеха да съдържат само един израз, благодарение на тази функция те могат да хвърлят изключения:
// само един израз
$fn = fn() => throw new \Exception('опа');
Match изрази
Конструкцията switch-case
има два
големи недостатъка:
- използва нестриктно сравнение
==
вместо===
- трябва да внимавате да не забравите
случайно
break
Затова PHP идва с алтернатива под формата
на нова конструкция match
, която
използва стриктно сравнение и обратно, не
използва break
.
Пример за код със switch
:
switch ($statusCode) {
case 200:
case 300:
$message = $this->formatMessage('ok');
break;
case 400:
$message = $this->formatMessage('не е намерено');
break;
case 500:
$message = $this->formatMessage('грешка на сървъра');
break;
default:
$message = 'неизвестен код на състоянието';
break;
}
И същото (само със стриктно сравнение),
записано с помощта на match
:
$message = match ($statusCode) {
200, 300 => $this->formatMessage('ok'),
400 => $this->formatMessage('не е намерено'),
500 => $this->formatMessage('грешка на сървъра'),
default => 'неизвестен код на състоянието',
};
Забележете, че match
не е контролна
структура като switch
, а израз. В
примера присвояваме резултантната му
стойност на променлива. Същевременно и
отделните “възможности” са изрази,
следователно не може да се запишат повече
стъпки, както в случая със switch
.
Ако не се постигне съвпадение с нито един
от вариантите (и не съществува клауза default),
се хвърля изключение UnhandledMatchError
.
Между другото, в Latte също съществуват
тагове {switch}
, {case}
и
{default}
. Тяхното функциониране
отговаря точно на новия match
. Те
използват стриктно сравнение, не изискват
break
и в case
е възможно да се
посочат повече стойности, разделени със
запетаи.
Nullsafe оператор
Опционалното верижно извикване (optional
chaining) позволява писането на израз, чието
изчисление спира, ако срещне null. И това
благодарение на новия оператор ?->
.
Той ще замени много код, който иначе би
проверявал многократно за null:
$user?->getAddress()?->street
// означава приблизително
$user !== null && $user->getAddress() !== null
? $user->getAddress()->street
: null
Защо “приблизително”? Защото в
действителност изразът се оценява
по-изобретателно, така че не се повтаря нито
една стъпка. Например $user->getAddress()
се
извиква само веднъж, така че проблемът,
предизвикан от това, че методът връща нещо
различно за първия и втория път, не може да
се срещне.
Тази функция беше внесена от “Лате преди: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 някога се отърва от “magic quotes”, регистрирането на глобални променливи, а сега разхлабеното поведение се заменя със стриктност. Времето, когато в PHP можехте да събирате, умножавате и т.н. почти всякакви типове данни, при които това нямаше абсолютно никакъв смисъл, отдавна отмина. Започвайки от версия 7.0, PHP става все по-стриктен и от версия 8.0 опитът за използване на какъвто и да е аритметичен/битов оператор върху масиви, обекти или ресурси завършва с TypeError. Изключение е събирането на масиви.
// аритметични и битови оператори
+, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, --:
По-разумно сравняване на низове и числа
Или make loose operator great again.
Изглеждаше, че за loose оператора ==
вече няма място, че е просто печатна грешка
при писане на ===
, но тази промяна го
връща отново на картата. Щом го имаме, нека
се държи разумно. Следствие от предишното
“неразумно” сравнение беше например
поведението на in_array()
, което можеше
неприятно да ви изненада:
$validValues = ['foo', 'bar', 'baz'];
$value = 0;
dump(in_array($value, $validValues));
// изненадващо връщаше true
// от PHP 8.0 връща 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 break в самата основа на езика, който беше одобрен без никаква съпротива. И това е добре. Тук JavaScript може много да завижда.
Докладване на грешки
Много вътрешни функции сега предизвикват
TypeError и ValueError вместо предупреждения, които
лесно можеха да бъдат пренебрегнати. Бяха
прекласифицирани редица предупреждения на
ядрото. Shutup операторът @
вече не
заглушава фатални грешки. А PDO в режим по
подразбиране хвърля изключения.
Nette винаги се е опитвало да решава тези неща по някакъв начин. Tracy променяше поведението на shutup оператора, Database превключваше поведението на PDO, Utils съдържа заместители на стандартни функции, които хвърлят изключения вместо незабележими предупреждения и т.н. Хубаво е да се види, че стриктната посока, която е в ДНК-то на Nette, става нативна посока на езика.
Масиви с отрицателен индекс
$arr[-5] = 'first';
$arr[] = 'second';
Какъв ще бъде ключът на втория елемент?
Преди беше 0
, от PHP 8 е -4
.
Завършваща запетая
Последното място, където не можеше да има завършваща запетая, беше дефиницията на параметрите на функция. Това вече е минало:
public function __construct(
Nette\Database\Connection $db,
Nette\Mail\Mailer $mailer, // завършваща запетая
) {
....
}
$object::class
Магическата константа ::class
работи
и с обекти $object::class
, с което напълно
замества функцията get_class()
.
catch без променлива
И накрая: в клаузата catch не е необходимо да се посочва променлива за изключението:
try {
$container->getService(Database::class);
} catch (MissingServiceException) { // няма $e
$logger->log('....');
}
В следващите части ни очакват съществени новости в типовете данни, ще покажем какво са атрибутите, какви нови функции и класове се появиха в PHP и ще представим Just in Time Compiler.
За да изпратите коментар, моля, влезте в системата