PHP Manual
/
Нові версії

PHP 8 вийшов - повний огляд

26. 11. 2020

Obsah článku

Сьогодні, 26 листопада 2020 року, через кілька років вийшла нова основна версія PHP 8, яка включає в себе сміливий набір нових функцій. Це одне з найбільших оновлень за довгий час і заслуговує на окрему статтю.

У цій статті ми підсумуємо всі основні нові функції та відмінності в синтаксисі та опціях у порівнянні зі старою версією. Більшість нових функцій є сумісними з попередніми версіями і приносять поведінкові покращення, які вам сподобаються.

Важлива інформація:** PHP 8 зараз знаходиться у фазі "заморожування функцій", що означає, що більше не можна додавати нову поведінку, а виправляються тільки помилки. Таким чином, ви можете розраховувати на сумісність і повноцінне налагодження ваших додатків.

Типи об'єднань

PHP в цілому в останні роки переходить від суто динамічної мови, де будь-яка змінна може містити що завгодно, до суворої форми, де ми заздалегідь знаємо, який тип даних буде в якій змінній, параметрі, аргументі або властивості. Використання data-types все ще є необов'язковим, але я рекомендую використовувати сильну типізацію і сам використовую її на всіх проектах.

Об'єднані типи виражають сукупність декількох типів, приймаючи в них будь-який аргумент або властивість.

Наприклад:

function validatePsc(string|int $psc): bool
{
// імплементація
}

Функція validatePsc() у змінній $psc приймає типи даних string (рядок) та int (ціле число).

У попередній версії PHP 7.4 така нотація була неможлива і зазвичай обходилася за допомогою comment:

/**
* @param string|int $psc
*/
function validatePsc($psc): bool
{
// імплементація
}

Однак, цей коментар анотації ігнорується PHP (це ж коментар) і нам довелося виконувати додаткову перевірку за допомогою зовнішнього інструменту, такого як PhpStan, який багато розробників ігнорують. Тепер перевірка здійснюється безпосередньо під час виконання (під час роботи програми) і не може бути обійдена.

Однак, PHP відомий певний тип об'єднання типів, починаючи з версії 7, коли можна було сказати, що основний тип також може бути nullable, тобто він приймає основний тип даних плюс значення null.

Це було написано у двох варіантах, кожен з яких мав різний зміст:

function setPhone(?string $phone): void
{
// імплементація
}
// або
function setPhone(string $phone = null): void
{
// імплементація
}
// або комбінацію
function setPhone(?string $phone = null): void
{
// імплементація
}

У всіх записах зазначено, що телефон int (ціле число) є або рядком, або нулем.

  • Перший запис завжди вимагає передачі значення
  • Другий запис не вимагає передачі будь-якого значення; якщо нічого не передається, значенням за замовчуванням є null (це необов'язковий аргумент)
  • Третій запис є комбінацією опцій і поводиться так само, як і другий запис

При використанні об'єднаних типів ми більше не зможемо використовувати позначення зі знаком питання і повинні строго визначати, наприклад, тип даних null:

function setPhone(string|int|null $phone = null): void
{
// імплементація
}

Номер телефону тепер повинен бути string, int або null.

Об'єднані типи ще мають ряд застосувань, про які досвідчені розробники прочитають у документації або при реалізації конкретних бібліотек.

JIT - прискорення обробки сценаріїв

Компілятор JIT (just in time) забезпечує значне покращення продуктивності при ускладненні (розборі та розумінні) скриптів. Однак така поведінка може змінюватися в контексті веб-запитів.

Тепер ви можете побачити, чи увімкнено JIT на панелі Tracy в рамках Nette, і подивитися [окрему статтю] (https://stitcher.io/blog/php-jit) для отримання більш детальної інформації.

Що можна сказати про компіляцію в цілому, так це те, що PHP намагається обробити код заздалегідь, щоб при обробці конкретного запиту йому не довелося переглядати фізичний файл скрипта, розбирати його і інтерпретувати. Раніше це вирішувалося за допомогою розширення OPCache (яке за замовчуванням було доступне на серверах і хостах), і це підвищувало швидкість обробки приблизно вдвічі.

Як правило, якщо у вас повільний додаток, завжди краще вибрати відповідний алгоритм для вирішення конкретної задачі, ніж робити мікро-оптимізації в коді. Зазвичай великі затримки викликані очікуванням бази даних та її повільними запитами, збереженням сесій, очікуванням звільнення місця на жорсткому диску та іншими апаратними операціями.

Оператор Nullsafe (опціональний ланцюг)

Дуже часто в реальній програмі виникає необхідність перевірити наявність значення, що повертається (щоб воно не було null) з одного методу, а потім умовно викликати інший. Для цього чудово підходять [тернарні оператори] (/ternary-operator), але вони працюють тільки з однією умовою і не можуть бути вкладеними. Оператор nullsafe дозволяє створювати гнізда нативно.

Порада: Практично така ж поведінка вже підтримується системою шаблонів Latte, але вона перевизначає цей тип синтаксису в рідному PHP-коді, тому ви можете використовувати оператор nullsafe на старих версіях PHP (починаючи з PHP 7 і вище). Похвала Девіду за цю модифікацію!

Це робить його простим у використанні:

$orderId = $order?->getId();

Змінна $orderId містить або значення, що повертається методом getId(), або null, якщо змінна $order містить значення null і метод getId() не вдалося викликати.

У PHP 7 цей тип проблем був обійдений наступним синтаксисом через тернарний оператор:

$orderId = isset($order) ? $order->getId() : null;

Можливо, умова:

if (isset($order)) {
$orderId = $order->getId();
} else {
$orderId = null;
}

Запис може бути зроблений додатково до дзвінка. Я взяв зразок з [Latte documentation] (https://latte.nette.org/cs/syntax#toc-volitelne…), який чудово його описує:

$orderName = $order->item?->name;
// те ж саме, що і..:
$orderName = isset($order->item) ? $order->item->name : null;

Як правило, використовується при перерахуванні більш складних структур в шаблоні, наприклад, в Latte це виглядає так (приклад взято з документації):

{$user?->address?->street}
// означає приблизно ($user !== null) && ($user->address !== null) ? $user->address->street : null
{$items[2]?->count}
// замінити приблизно ($items[2] !== null) ? $items[2]->count : null
{$user->getIdentity()?->name}
// замінити приблизно $user->getIdentity() !== null ? $user->getIdentity()->name : null

У реальному коді це може виглядати так, наприклад, що ми хочемо дізнатися країну замовника, прочитавши його профіль (а у вас дані в базі даних зберігаються красиво через сесії, як і належить), то в старому PHP це виглядало так:

$country = null;
if ($session !== null) {
$user = $session->user;
if ($user !== null) {
$address = $user->getAddress();
if ($address !== null) {
$country = $address->country;
}
}
}

Тепер його можна скоротити до одного рядка:

$country = $session?->user?->getAddress()?->country;

Використання оператора nullsafe також запобігає різним помилкам, які в PHP 7 недосвідченому розробнику було б нелегко виявити.

Наприклад, цей запис згенерує фатальну помилку:

var_dump($invoice->getDate()->format('Д-д-д') ?? null);
// return: fatal error: uncaught Помилка: виклик функції-члена format() на null

Правильний синтаксис виглядає наступним чином:

var_dump($invoice->getDate()?->format('Д-д-д'));
// повертає: null

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

У старому доброму PHP виклики функцій з аргументами потрібно було писати, передаючи аргументи в точному порядку, визначеному цільовою функцією. В цьому немає нічого поганого, однак, при використанні ряду параметрів з подібними значеннями, це може призвести до погіршення читабельності. Або якщо ми хотіли передати до n-го параметра в порядку, то перед цим потрібно було передати всі необов'язкові параметри, що могло негативно вплинути на читабельність і пряму сумісність.

Уявіть собі, наприклад, функцію setCookie() в Nette, яка має багато аргументів:

public function setCookie(
string $name,
string $value,
$time,
string $path = null,
string $domain = null,
bool $secure = null,
bool $httpOnly = null,
string $sameSite = null
)

Перші три аргументи ($name, $value і $time) є обов'язковими, але якщо ми хотіли передати аргумент $httpOnly, то повинні були передати всі попередні і правильно розрахувати замовлення:

$http->setCookie(
'myCookie',
'Девід любить коней',
'зараз',
null, // шлях
null, // домен
null, // безпечно
true
);

Чого просто не хочеться робити без необхідності.

Витончене письмо тоді виглядає як:

$http->setCookie(
name: 'myCookie',
value: 'Девід любить коней',
time: 'зараз',
httpOnly: true
);

Цей тип синтаксису вимагає, щоб імена аргументів у цільовій функції ніколи не змінювались, оскільки вони все одно будуть вводитись при виклику. Принаймні, розробники зможуть їх краще назвати.

Якщо ми хочемо використовувати тільки один з аргументів, синтаксис може бути об'єднаний і стиснутий до одного рядка:

$http->setCookie('myCookie', 'Девід любить коней', 'зараз', httpOnly: true);

Перші 3 аргументи передаються оригінальним способом, потім передається необов'язковий аргумент httpOnly (тому що він іменований).

Атрибути

Більшість основних мов, таких як Java або C#, вже за замовчуванням включають так звані анотації, що є синтаксисом рідної мови, який дозволяє додавати метаінформацію до інших мовних конструкцій.

У PHP цей тип синтаксису вже давно відсутній, і його обходили за допомогою використання DOC-коментарів, які є класичним коментарем над методом, тільки з двома зірочками /**.

Ці коментарі ігноруються під час обробки сценарію, і для їх розбору та інтерпретації під час виконання необхідно додати спеціальну логіку користувача за допомогою рефлексії. Ви, напевно, розумієте, який вплив це може мати на продуктивність, плюс синтаксис коментарів не може бути обов'язковим, і його дуже важко перевірити під час компіляції (коли скрипт обробляється перед виконанням), і знову ж таки, для цього вам доведеться використовувати додаткові інструменти за межами звичайного набору інструментів PHP.

Для збереження зворотної сумісності PHP надає атрибути з синтаксисом, подібним до альтернативної нотації коментарів, що не порушує роботу скрипта на застарілих версіях PHP.

Оригінальна нотація (використовується, наприклад, для Inject залежностей в Nette Presenter):

final class HomepagePresenter extends BasePresenter
{
/** @inject */
public EntityManager $entityManager;
}

Тепер можна видалити коментар і використовувати атрибут native:

use App\Attributes\Inject;
final class HomepagePresenter extends BasePresenter
{
#[Inject]
public EntityManager $entityManager;
}

Дуже важливо, що атрибут - це вже не просто шматок рядка в коментарі, а фізичний клас, який є коректним PHP-кодом.

Це чудово, адже тепер можна безпечно перевіряти вхідні дані атрибуту, а використання атрибуту фактично стає викликом його конструктора, де можна використовувати іншу логіку. Я з нетерпінням чекаю, коли це буде підтримано "Доктриною", яка використовує анотації для всього.

Реалізація самого атрибуту тоді може виглядати приблизно так:

#[Attribute]
class Inject
{
public string $value;
public function __construct(string $value)
{
$this->value = $value;
}
}

Сувора логіка може бути використана в межах атрибуту знову ж таки, наприклад, перевірка типів даних аргументів, типів об'єднань та інших мовних особливостей.

Вираз збігу

Нова мовна конструкція match() є модернізованим поліпшенням старого доброго witch() (який я намагаюся не використовувати), і приносить ряд класних можливостей (які змусять мене почати використовувати його знову).

Наприклад, ми хочемо змінити значення змінної на основі вхідних даних:

$pozdrav = match(bool $formal) {
true => 'Доброго дня',
false => 'Привіт',
};

Важливою новою особливістю синтаксису є те, що нам не потрібно використовувати break (як у старому witch), і синтаксис в цілому набагато економніший.

У той же час, ми можемо перевіряти декілька вхідних даних одночасно в межах умови (через кому) і, можливо, повертати значення за замовчуванням (коли жоден з них не задовольняє умовам).

Це стане в нагоді, наприклад, при переписуванні коду умови HTTP в повідомлення про помилку (ви обов'язково оціните це при роботі з кодами винятків):

$message = match ($statusCode) {
200, 300 => null,
400 => 'не знайдено',
500 => 'помилка сервера',
default => 'невідомий код стану',
};

Порівняння значень виконується строго оператором === (switch використовує тільки ==), що ще раз показує, що PHP йде по строгому шляху проектування. Тому вхід '200' (рядок, що містить число) не буде прийнятий, як і в попередньому випадку.

Якщо не вказано значення для default і немає збігу, то виникає помилка UnhandledMatchError.

Новий синтаксис також дозволяє використовувати вираз або виклик функції для зіставлення (це діє як умова). У разі помилки ми можемо згенерувати виключення (оскільки лексема throw тепер є виразом і може бути використана таким чином):

$message = match ($statusCode) {
200 => null,
$this->checkServerError($statusCode) => throw new ServerError(),
default => 'невідомий код стану',
};

Розповсюдження властивостей в конструктор

Це просто синтаксичний цукор, який стане в нагоді для швидкого та зручного визначення сутності та її властивостей безпосередньо в конструкторі.

Наприклад, первісний суб'єкт господарювання:

final class User
{
public string $name;
public function __construct(
string $name,
) {
$this->name = $name;
}
}

Його можна лише скоротити до:

final class User
{
public function __construct(
public string $name
) {}
}

Властивість $name перевіряється на відповідність типу даних string і її значення можна прочитати безпосередньо з екземпляру, оскільки вона є загальнодоступною властивістю. Якщо ви використовуєте додатковий SmartObject в Nette (що я не рекомендую для PHP 8), ви також можете отримати доступ до приватних властивостей, викликавши спочатку їх метод getter, і цей синтаксис знову спрощує речі.

Тип повернення static

Раніше ми могли використовувати тип даних self в якості значення, що повертається методом, але він повертає екземпляр того самого класу, в якому він визначений. Тип даних static взагалі може робити це навіть у випадку успадкування, і повертатиме тип даних класу, з якого був виконаний екземпляр, а не його предка.

Наприклад:

class BaseEndpoint
{
public function getInstance(): static
{
return new static();
}
}

Тип даних змішаний

Змішаний тип тепер можна використовувати як аргумент функції або методу. Це означає, що метод завжди повинен приймати деякий вхід (і тому є обов'язковим аргументом).

Якщо ви можете хоч трохи, завжди використовуйте прямий тип даних, або хоча б об'єднання. Змішаний корисний лише тоді, коли функція приймає дійсно будь-що. На практиці використання корисне, наприклад, для різних функцій дампа, які приймають довільні вхідні дані і повинні мати можливість їх виводити.

Тип mixed приймає наступні типи: string, int, float, null, bool, array, callable, object, resource.

Потім Девід буде використовувати змішаний тип для виконання своїх функцій:

function bdump(mixed $var): mixed
{
Tracy\Debugger::barDump($var);
return $var;
}

Кидання жетонів як вираз

Токен throw тепер став виразом, це означає на практиці, що виключення може бути згенероване при усіченні лямбда-функції fn(), або при перевірці тернарного оператора:

$error = fn () => throw new \InvalidArgumentException('Це завжди призводить до помилки.');
$userName = $user['ім'я'] ?? throw new \LogicException('Користувач повинен мати ім'я.');

Функція str_contains()

PHP нарешті включає власну функцію для перевірки того, що рядок за замовчуванням містить підрядок.

Наприклад:

if (str_contains('Гонзік любить котів.', 'коти')) {
echo 'Функція справляється з котами.';
}

Раніше наявність підрядка перевірялася функцією strpos:

if (strpos('Гонзік любить котів.', 'коти') !== false) {
echo 'Функція справляється з котами.';
}

Функції str_starts_with() та str_ends_with()

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

str_starts_with('Гонзік любить котів.', 'Хонзік'); // true
str_ends_with('Гонзік любить котів.', 'коти.'); // true

Функція get_debug_type()

Розширити вивід існуючої функції gettype, яка повертала лише узагальнений тип переданої змінної. Функція використовується, наприклад, при виникненні виключення, коли ми отримуємо невірний ввід і хочемо повідомити користувачеві, що він насправді передав.

Коли ми викликаємо функцію gettype() зі змінною, що містить екземпляр класу \App\User, функція повертає об'єкт, тому ми не знаємо, що це за клас. Нова функція get_debug_type() повертає ім'я класу.

Функція get_resource_id()

Ця функція повертає ідентифікатор зовнішнього ресурсу зі змінної.

Наприклад, підключення до бази даних MySql обробляється PHP за допомогою спеціального типу даних resource, тепер з'явилася можливість дізнатися, який ID їй присвоєно.

Історична довідка:.

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

Розширення ext-json завжди доступне

Раніше PHP можна було компілювати без підтримки json. Тепер json буде завжди доступний, тому ви можете видалити залежність ext-json з ваших файлів composer.json і завжди знати, що json можна використовувати.

Пріоритет конкатенації

Уявіть собі щось на кшталт:

echo 'Підсумок:' . $a + $b;

Чи відбувається спочатку додавання чисел, чи спочатку до рядка додається змінна $a, а потім весь новий рядок додається до $b?

Можна було б очікувати, що спочатку буде зроблено доповнення, але це гарне припущення. PHP насправді робить щось подібне:

echo ('Підсумок:' . $a) + $b;

PHP 8 тепер буде вести себе передбачувано:

echo 'Підсумок:' . ($a + $b);

В цілому, однак, завжди краще використовувати круглі дужки, щоб обмежити вираз.

Стабільне замовлення

До версії PHP 8 сортування рядків виконувалося з використанням так званого нестабільного алгоритму, що означає, що PHP не гарантував, що елементи з однаковим (або іншим чином еквівалентним) значенням не будуть поміняні місцями. У новій версії поведінка всіх функцій сортування змінена на стабільну, тому сортування завжди виконується детерміновано і ви завжди отримуєте однаковий результат.

Це вирішує, наприклад, випадки, коли ми ранжували рейтинги користувачів за релевантністю, але деякі рейтинги мали однаковий бал. Тепер вони будуть з'являтися в тому ж порядку при кожному сортуванні і не будуть постійно пропускатися.

Інші нові функції

PHP має багато інших незначних нововведень і поліпшень. Наприклад, по-іншому будуть викидатися помилки (але це ж не турбує нас, які пишуть безпомилковий код, чи не так?).

З повним переліком змін ви завжди можете ознайомитися в офіційній документації та на сторінці RFC.

Чого мені не вистачає в новому PHP

Хотілося б, щоб PHP нарешті підтримував складені типи масивів, наприклад, коли метод повертає масив ідентифікаторів, нам все ще доводиться вказувати просто getIds(): array, а щось на кшталт getIds(): int[] було б набагато краще. Можливо, ми скоро це побачимо, і сильна перевірка типу буде завершена.

Більше ресурсів

Девід Грудл розповів про новинки Posobot. Рекомендую переглянути запис:

Я хочу подякувати Девіду за його лекцію, оскільки я почерпнув з неї певну інформацію для цієї статті. Зокрема, про перехід Nette на PHP 8 та інші кулуарні поради щодо PHP.

Jan Barášek   Více o autorovi

Autor článku pracuje jako seniorní vývojář a software architekt v Praze. Navrhuje a spravuje velké webové aplikace, které znáte a používáte. Od roku 2009 nabral bohaté zkušenosti, které tímto webem předává dál.

Rád vám pomůžu:

V jiných jazycích

1.
12.
Status:
All systems normal.
2024