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 за изпращане на бисквитки в 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_*() са защитени от именувани аргументи.

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

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

Изненадващо е, че в момента той не работи вътре в масивите:

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

Комбинацията от именувани аргументи и variadics дава възможност най-накрая да имаме фиксиран синтаксис, например за метода 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() се извиква само веднъж, така че проблемът, предизвикан от това, че методът връща нещо различно за първия и втория път, не може да се срещне.

Тази функция беше внесена от “Лате преди: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 ==, че той е просто печатна грешка при писането на ===, но тази промяна го връща отново на картата. Ако вече го имаме, нека се държи разумно. В резултат на предишното “неразумно” сравнение например 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 хвърля изключения по подразбиране.

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.

Последни публикации