Нові функції та покращення в PHP 8.3: Повний технічний довідник
PHP 8.3 — це значний мінорний реліз мови PHP, який забезпечує суттєві покращення JIT-компілятора, системи типів, readonly-властивостей та основних функцій для роботи з масивами та рядками. Випущений 23 листопада 2023 року, він вводить типізовані константи класів, json_validate(), вдосконалення array_is_list(), доповнення Randomizer та глибоке клонування readonly-властивостей — зміни, які безпосередньо впливають на продуктивність застосунків, коректність коду та зручність підтримки на виробничих серверах.
Якщо ви запускаєте PHP-навантаження у середовищі VPS Хостингу або на Виділеному Сервері, розуміння кожної зміни PHP 8.3 не є необов’язковим — це обов’язкова умова для прийняття обґрунтованих рішень щодо оновлення, уникнення прихованих регресій та отримання вимірюваних переваг у продуктивності.
Що змінилося між PHP 8.2 та PHP 8.3
Перш ніж заглиблюватися в окремі функції, варто визначити масштаб цього релізу. PHP 8.3 — це не кардинальна переробка. Це точне оновлення, яке усуває давні прогалини в системі типів, зміцнює JIT-конвеєр та додає службові функції, для яких раніше потрібні були обхідні рішення на рівні користувацького коду. Таблиця нижче відображає найбільш значущі зміни порівняно з їхніми еквівалентами в PHP 8.2.
| Функція / Поведінка | PHP 8.2 | PHP 8.3 |
|---|---|---|
| Типізовані константи класів | Не підтримується | Повністю підтримується |
json_validate() | Недоступно | Доступно нативно |
| Клонування readonly-властивостей | Неможливо | Підтримується через clone |
array_is_list() | Доступно | Поведінка незмінна, але ширші патерни використання |
| Динамічне отримання константи класу | Синтаксична помилка | Підтримується через ClassName::{$const} |
Randomizer::getBytesFromString() | Недоступно | Доступно |
Randomizer::getFloat() / nextFloat() | Недоступно | Доступно |
Атрибут #[Override] | Недоступно | Доступно |
Застаріле: неявне ініціювання mt_rand | Не застаріле | Застаріле |
| Директива ini для розміру стека Fiber | Не налаштовується | Додано fiber.stack_size |
| Покращення JIT-трасування | Базове трасування | Покращена IR та обробка циклів |
str_contains з масивами | Не підтримується | Досі не підтримується (помилка у вихідній статті — див. нижче) |
> Критичне виправлення: У вихідній статті помилково стверджується, що str_contains() приймає масив рядків у PHP 8.3. Це фактично невірно. str_contains() приймає лише два рядкові аргументи. Передача масиву викликає TypeError. Правильний підхід для пошуку підрядка в кількох рядках — це array_filter() у поєднанні з str_contains(), або in_array() для точних збігів.
JIT-компіляція в PHP 8.3: що насправді змінилося
Передумови: як працює PHP JIT
JIT-компілятор PHP, представлений експериментально в PHP 8.0, працює як розширення підсистеми OPcache. Він компілює гарячі шляхи байткоду в нативний машинний код під час виконання, обходячи інтерпретатор Zend VM для цих шляхів. PHP 8.3 постачається зі суттєво переробленим JIT-бекендом, який покращує проміжне представлення (IR), що використовується під час компіляції.
Новий JIT на основі IR у PHP 8.3 (розроблений Дмитром Стоговим) замінює шар генерації коду старішого трасуючого JIT на належне проміжне представлення у формі SSA. Це забезпечує кращий розподіл регістрів, усунення мертвого коду та підйом інваріантів циклу — оптимізації, які були структурно неможливі в попередній архітектурі.
Правильне увімкнення JIT
У вихідній статті показано php -d jit=on script.php, що є неповним. JIT вимагає активного OPcache. Правильна мінімальна конфігурація для CLI-бенчмарку або виробничого php.ini:
; php.ini
opcache.enable=1
opcache.enable_cli=1
opcache.jit_buffer_size=128M
opcache.jit=tracingУ контексті веб-сервера (FPM або Apache mod_php) opcache.enable_cli не є релевантним, але opcache.jit_buffer_size має бути ненульовим, інакше JIT тихо вимикає себе. Поширена виробнича пастка — встановлення jit_buffer_size=0 у спільному php.ini та здивування, чому JIT не має ефекту.
Коли JIT забезпечує вимірювані переваги
JIT не є універсально корисним. Його переваги зосереджені в навантаженнях, обмежених CPU:
- Високоефективні цілі: Математичні обчислення, обробка зображень, інференс машинного навчання, ігрова логіка, цикли розбору CSV/даних, криптографічні операції в користувацькому коді.
- Малоефективні цілі: Типові CRUD веб-застосунки, де вузьким місцем є I/O (запити до бази даних, файлова система, мережа). У таких випадках накладні витрати JIT від компіляції можуть незначно збільшити використання пам’яті з незначним покращенням пропускної здатності.
- Негативні випадки: Застосунки з надзвичайно різноманітними шляхами коду (великі фреймворки з інтенсивним використанням рефлексії) можуть спричинити перевантаження буфера JIT, що призводить до накладних витрат на деоптимізацію.
Практичне правило: спочатку виконайте бенчмарк з opcache.jit=tracing. Якщо ви бачите менше 3% покращення на вашому реальному навантаженні, вимкніть JIT, щоб звільнити пам’ять буфера для кешу опкодів OPcache, який рівномірно покращує всі PHP-застосунки.
Типізовані константи класів
Це, мабуть, найбільш значуще доповнення системи типів у PHP 8.3 для великих кодових баз.
Яку проблему це вирішує
До PHP 8.3 константи класів не мали примусового типу. Дочірній клас міг перевизначити константу з повністю несумісним типом, і PHP не генерував помилку до часу виконання — або взагалі не генерував, залежно від того, як використовувалася константа.
// PHP 8.2 — no type enforcement
interface StatusCode {
const SUCCESS = 200; // implicitly int
}
class BrokenStatus implements StatusCode {
const SUCCESS = "two hundred"; // silently accepted — a maintenance nightmare
}Рішення PHP 8.3
// PHP 8.3 — type is enforced at definition and inheritance
interface StatusCode {
const int SUCCESS = 200;
}
class BrokenStatus implements StatusCode {
const int SUCCESS = "two hundred";
// Fatal error: Cannot use string as value for typed class constant
// BrokenStatus::SUCCESS of type int
}Усі скалярні типи (int, float, string, bool), array, null, об’єднані типи та типи перетину є дійсними для оголошень типів констант. Типи never та void не дозволені. Ця функція чисто інтегрується зі статичними аналізаторами, такими як PHPStan та Psalm, забезпечуючи суворіші контракти інтерфейсів без накладних витрат під час виконання.
Динамічне отримання константи класу та члена enum
PHP 8.3 дозволяє отримувати константи класів та члени enum за допомогою виразу часу виконання у синтаксисі ::{}.
class Direction {
const string NORTH = 'north';
const string SOUTH = 'south';
}
$direction = 'NORTH';
echo Direction::{$direction}; // outputs: northРаніше це вимагало constant() або виразу match, обидва з яких є багатослівними та схильними до помилок. Новий синтаксис також працює з enum:
enum Color {
case Red;
case Blue;
}
$name = 'Red';
$color = Color::{$name}; // Color::RedГраничний випадок, на який слід звернути увагу: Якщо змінна містить ім’я, яке не відповідає визначеній константі або варіанту enum, PHP генерує виняток Error — не попередження. Обгортайте динамічні отримання в блок try/catch або перевіряйте за допомогою defined() / enum_exists() перед використанням.
Функція json_validate()
Чому це важливо у виробництві
До PHP 8.3 ідіоматичний спосіб перевірки рядка JSON без його декодування був таким:
json_decode($input);
$isValid = json_last_error() === JSON_ERROR_NONE;Цей підхід повністю декодує JSON у структуру PHP, виділяючи пам’ять пропорційно розміру корисного навантаження. Для конвеєрів лише з перевіркою — API-шлюзів, споживачів черги повідомлень, приймачів вебхуків — це марнотратно.
Нативна перевірка PHP 8.3
$payload = '{"user": "alex", "role": "admin"}';
if (json_validate($payload)) {
// safe to decode
$data = json_decode($payload, true);
}
// Invalid JSON
var_dump(json_validate('{invalid}')); // bool(false)json_validate() розбирає структуру JSON без побудови дерева значень PHP. Споживання пам’яті становить O(глибина) замість O(розмір), що робить його значно ефективнішим для великих корисних навантажень. Він також приймає параметри $depth та $flags, узгоджені з json_decode().
Реальний випадок використання: Приймач вебхуків, що обробляє 50 000 запитів на хвилину, може використовувати json_validate() для відхилення некоректних корисних навантажень на межі до будь-якої десеріалізації, суттєво знижуючи навантаження на CPU та пам’ять.
Readonly-властивості: підтримка глибокого клонування
PHP 8.2 представив readonly-властивості, але зробив їх неможливими для зміни навіть під час клонування об’єктів. Це змушувало розробників вдаватися до незручних обхідних рішень — фабричних методів, хаків із серіалізацією або повної відмови від readonly для об’єктів-значень.
PHP 8.3 вирішує це, дозволяючи магічному методу __clone() перепризначати readonly-властивості в контексті клонування.
class ImmutablePoint {
public function __construct(
public readonly float $x,
public readonly float $y,
) {}
public function withX(float $x): static {
$clone = clone $this;
$clone->x = $x; // Legal in PHP 8.3 within __clone context
return $clone;
}
}
$point = new ImmutablePoint(1.0, 2.0);
$moved = $point->withX(5.0);
echo $moved->x; // 5.0
echo $point->x; // 1.0 — original unchangedЦей патерн є основоположним для незмінних об’єктів-значень у Domain-Driven Design. Без нього readonly був значною мірою декоративним для складних доменних моделей.
Атрибут #[Override]
Атрибут #[Override] сигналізує PHP (та інструментам статичного аналізу), що метод призначений для перевизначення методу батьківського класу або інтерфейсу. Якщо батьківський метод не існує, PHP генерує помилку під час компіляції.
class Base {
public function process(): void {}
}
class Child extends Base {
#[Override]
public function process(): void {
// If Base::process() is renamed or removed, this becomes a fatal error
}
}Це особливо цінно у великих командах, де рефакторинг базового класу може непомітно зламати перевизначення методів у дочірніх класах. Атрибут діє як контракт під час компіляції, виявляючи категорію помилок, які раніше виявлялися лише під час виконання або через статичний аналіз.
array_is_list() та правильна класифікація масивів
array_is_list() був введений у PHP 8.1, а не 8.3. Однак правильні патерни його використання заслуговують на точну документацію, оскільки функція часто неправильно розуміється.
Масив PHP є списком тоді і тільки тоді, коли:
- Він порожній, або
- Його ключі є послідовними цілими числами, починаючи з
0без пропусків.
var_dump(array_is_list([])); // bool(true)
var_dump(array_is_list([0 => 'a', 1 => 'b'])); // bool(true)
var_dump(array_is_list(['a', 'b', 'c'])); // bool(true)
var_dump(array_is_list([1 => 'a', 0 => 'b'])); // bool(false) — wrong order
var_dump(array_is_list([0 => 'a', 2 => 'b'])); // bool(false) — gap at index 1
var_dump(array_is_list(['key' => 'value'])); // bool(false) — string keyПрактичне застосування: При серіалізації даних у JSON array_is_list() визначає, чи має вивід бути масивом JSON ([]) або об’єктом JSON ({}). Використання його перед json_encode() запобігає випадковій серіалізації об’єктів числово-індексованих масивів, з яких були видалені елементи.
Нові методи Randomizer у PHP 8.3
Клас RandomRandomizer, введений у PHP 8.2, отримує три важливі доповнення:
getBytesFromString()
$randomizer = new RandomRandomizer();
$token = $randomizer->getBytesFromString('abcdefghijklmnopqrstuvwxyz0123456789', 16);
echo $token; // e.g., "k3mz9xqp1wvn7yt2"Це генерує криптографічно безпечний випадковий рядок, взятий із зазначеного алфавіту — патерн, необхідний для генерації токенів, OTP-кодів та створення slug. Раніше це вимагало ручного циклу з random_int().
getFloat() та nextFloat()
$randomizer = new RandomRandomizer();
// Returns a float in [0.0, 1.0)
$value = $randomizer->nextFloat();
// Returns a float in a specified closed or half-open interval
$scaled = $randomizer->getFloat(1.5, 9.5, RandomIntervalBoundary::ClosedOpen);getFloat() використовує алгоритм γ-перерізу для отримання рівномірно розподілених чисел з плаваючою комою без зміщення за модулем, яке впливає на наївні реалізації. Це критично для симуляцій, імовірнісних алгоритмів та фреймворків A/B-тестування, де важлива рівномірність розподілу.
Застарілі функції та видалення в PHP 8.3
Розуміння того, що виводиться з використання, так само важливо, як знання того, що додається. Ігнорування застарілих функцій зараз означає фатальні помилки в PHP 9.0.
| Застаріла функція | Причина | Шлях міграції |
|---|---|---|
Виклик mt_rand() без явного ініціювання в деяких контекстах | Непослідовність поведінки неявного ініціювання | Використовуйте RandomRandomizer |
ReflectionProperty::setValue() без об’єкта для нестатичного | Неоднозначна поведінка | Передавайте цільовий об’єкт явно |
Передача від’ємного $widths до mb_strimwidth() | Невизначена поведінка | Перевіряйте вхідні дані перед викликом |
ldap_connect() з окремими аргументами хоста/порту | Застаріло на користь форми URI | Використовуйте рядок URI ldap://host:port |
range() з нецілим кроком, що генерує числа з плаваючою комою | Несподівана неявна приведення типів | Явно приводьте крок до float |
Бенчмарки продуктивності: PHP 8.3 порівняно з попередніми версіями
На основі опублікованих бенчмарків від Kinsta, Phoronix та команди PHP internals з використанням Symfony Demo, WordPress та чистих навантажень Fibonacci/сортування:
| Бенчмарк | PHP 8.1 | PHP 8.2 | PHP 8.3 |
|---|---|---|---|
| Symfony Demo (запит/сек) | ~1 450 | ~1 520 | ~1 610 |
| WordPress (запит/сек) | ~1 180 | ~1 240 | ~1 290 |
| Fibonacci (JIT, мс) | ~48 | ~44 | ~38 |
| Mandelbrot (JIT, мс) | ~210 | ~195 | ~170 |
| Лише OPcache (без JIT) | Базовий рівень | +5% | +8% |
Переваги є стабільними, але не драматичними для застосунків, обмежених I/O. Покращення JIT у PHP 8.3 показують найбільш значущу різницю в навантаженнях чистих обчислень — до 18% швидше, ніж PHP 8.2, на бенчмарку Mandelbrot.
Оновлення до PHP 8.3: практичний контрольний список на стороні сервера
Якщо ви керуєте власною серверною інфраструктурою — чи то на VPS з cPanel, чи то на фізичному Виділеному Сервері — дотримуйтесь цієї послідовності перед оновленням виробничих середовищ.
Кроки перед оновленням
- Запустіть
composer outdatedта оновіть усі залежності до версій із задекларованою сумісністю з PHP 8.3. - Виконайте
php -d error_reporting=E_ALL your_app_entrypoint.phpпід PHP 8.3 CLI, щоб виявити повідомлення про застарілі функції до того, як вони стануть фатальними помилками. - Перевірте будь-який код, який викликає
str_contains(),str_starts_with()абоstr_ends_with()з нерядковими аргументами — тепер вони генеруютьTypeErrorу суворих контекстах. - Перегляньте будь-які константи класів, які дочірні класи перевизначають — типізовані константи спричинять фатальні помилки, якщо типи несумісні.
- Перевірте вивід
phpinfo()після оновлення, щоб підтвердити, що OPcache та JIT активні з очікуваною конфігурацією.
Налаштування php.ini для виробництва PHP 8.3
; Recommended production baseline for PHP 8.3
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.jit=tracing
opcache.jit_buffer_size=64MВстановлення validate_timestamps=0 вимикає перевірку модифікації файлів при кожному запиті — це необхідно для розгортань з високим трафіком, де накладні витрати на інвалідацію OPcache є вимірюваними. Замість цього використовуйте хуки розгортання для виклику opcache_reset() після розгортання коду.
Перевірка після оновлення
# Verify active PHP version
php -v
# Confirm JIT is compiled in and active
php -r "var_dump(opcache_get_status()['jit']);"
# Check for deprecation notices in error log
tail -f /var/log/php-fpm/error.log | grep -i deprecatPHP 8.3 та міркування щодо безпеки веб-застосунків
PHP 8.3 не вводить нових примітивів безпеки, але деякі зміни мають непрямі наслідки для безпеки:
json_validate()зменшує поверхню атаки в конвеєрах перевірки вхідних даних, запобігаючи потраплянню некоректного JSON до логіки десеріалізації.- Типізовані константи класів запобігають атакам на плутанину типів, де підклас підставляє несподіваний тип для константи, що стосується безпеки (наприклад, рівень дозволу або значення тайм-ауту).
- Атрибут
#[Override]запобігає тихому затіненню методів у базових класах, критичних для безпеки, — вектор для тонких помилок підвищення привілеїв в архітектурах плагінів. - Доповнення
RandomRandomizerзамінюють небезпечні патерни, такі якsubstr(str_shuffle(implode(range('a','z'))), 0, 16), для генерації токенів.
Для застосунків, що обробляють конфіденційні дані, поєднання PHP 8.3 з правильно налаштованим стеком TLS є обов’язковим. Якщо ваше хостингове середовище ще не має розгорнутих актуальних SSL-сертифікатів, вирішіть це питання перед будь-яким оновленням PHP.
Вибір правильного хостингового середовища для PHP 8.3
JIT-компілятор PHP 8.3 та збільшені вимоги до пам’яті для розв’язання типізованих констант означають, що середовища з обмеженими ресурсами можуть не отримати повної вигоди від оновлення.
- Спільний хостинг: Доступність версії PHP повністю залежить від провайдера. Якщо вам потрібен PHP 8.3 негайно, плани Спільного Веб-хостингу з перемиканням версій PHP дають вам гнучкість без накладних витрат на управління сервером.
- VPS: Повний контроль над
php.ini, конфігурацією пулу PHP-FPM, налаштуванням OPcache та розміром буфера JIT. Це мінімально рекомендоване середовище для виробничих розгортань PHP 8.3 з увімкненим JIT. - Виділені сервери: Необхідні для застосунків з високим трафіком, де конкуренція за буфер JIT між кількома воркерами PHP-FPM стає вузьким місцем. Виділене середовище також дозволяє NUMA-орієнтоване виділення пам’яті для OPcache.
- GPU-хостинг: Актуально, якщо ваш PHP-застосунок оркеструє навантаження з GPU-прискоренням (наприклад, виклик сервісів інференсу Python ML). Середовища GPU-хостингу отримують вигоду від покращеного FFI та управління процесами в PHP 8.3.
Ключові технічні висновки та матриця рішень
Використовуйте типізовані константи класів негайно, якщо:
- Ваша кодова база використовує інтерфейси або абстрактні класи з константами, які дочірні класи перевизначають.
- Ви використовуєте PHPStan рівня 8 або Psalm — типізовані константи відкривають суворіший аналіз.
Увімкніть JIT, якщо:
- Ваш профайлер показує, що час CPU перевищує 40% часу запиту.
- Ви запускаєте пакетну обробку, перетворення даних або математичні навантаження в PHP.
- У вас є щонайменше 64 MB виділеного буфера OPcache JIT на пул PHP-FPM.
Не вмикайте JIT, якщо:
- Ваш застосунок обмежений I/O (база даних, кеш, файлова система, API-виклики домінують у затримці).
- Ви перебуваєте на спільному хостингу з обмеженою пам’яттю OPcache.
- Ви не виконували бенчмарк вашого конкретного навантаження — нічого не припускайте.
Використовуйте json_validate(), якщо:
- Ви перевіряєте JSON перед декодуванням будь-де у вашій кодовій базі.
- Ви обробляєте вебхуки або корисні навантаження черги повідомлень з великим обсягом.
Додайте #[Override] до:
- Кожного методу в дочірньому класі, який навмисно перевизначає батьківський метод.
- Перевизначень методів, критичних для безпеки, в архітектурах плагінів або розширень.
Перейдіть на RandomRandomizer, якщо:
- Будь-яка частина вашого коду використовує
rand(),mt_rand(),array_rand()абоstr_shuffle()для генерації токенів або ключів, чутливих до безпеки.
FAQ
Чи порушує PHP 8.3 зворотну сумісність з кодом PHP 8.2?
У більшості випадків ні. PHP 8.3 є мінорним релізом без видалених функцій зі стандартної бібліотеки, які не були вже застарілими в 8.2. Однак нові застарілі поведінки генеруватимуть повідомлення E_DEPRECATED, і будь-який код, що покладається на неявне приведення типів у константах або range() з кроками у вигляді чисел з плаваючою комою, може поводитися інакше. Завжди запускайте свій набір тестів під PHP 8.3 перед розгортанням.
Чи увімкнено JIT за замовчуванням у PHP 8.3?
Ні. JIT вимагає увімкненого OPcache та встановлення opcache.jit_buffer_size у ненульове значення. Стандартний php.ini, що постачається з більшістю дистрибутивів, встановлює opcache.jit_buffer_size=0, що фактично вимикає JIT. Ви повинні явно налаштувати його.
Чи можу я використовувати типізовані константи класів з об’єднаними типами в PHP 8.3?
Так. const int|string VERSION = 8; є дійсним. Типи перетину також дозволені для констант об’єктного типу. Єдині заборонені типи — void та never.
У чому різниця між json_validate() та json_decode() для цілей перевірки?
json_validate() розбирає структуру JSON без побудови значення PHP в пам’яті. Він значно ефективніший за пам’яттю для великих корисних навантажень та швидший, коли вам потрібно лише підтвердити структурну дійсність. json_decode() слід використовувати, коли вам дійсно потрібні декодовані дані — не викликайте обидва послідовно; викликайте json_validate() лише тоді, коли маєте намір відкинути результат.
Чи підтримує PHP 8.3 readonly-класи, введені в PHP 8.2?
Так, і розширює їх. PHP 8.3 дозволяє перепризначати readonly-властивості всередині __clone(), що було основним обмеженням readonly-класів у PHP 8.2. Це робить патерни незмінних об’єктів-значень повністю життєздатними без обхідних рішень.
