PHP 8.0: Головні нововведення

З PHP 7.4: бенчмарки JIT, типи й типові breaking changes із практики застосунків.

Зміст (Оглавлення)


PHP 8.0 відкриває гілку 8.x: JIT у ядрі, ширша система типів (union, mixed, static), у синтаксисі — іменовані аргументи, match, nullsafe-оператор і атрибути, а рантайм менше терпить «тихі» помилки. Тут наголос на тому, що змінюється в реальних проєктах (фреймворки, легасі, розширення), а не лише перелік фіч.

Named Arguments (Іменовані аргументи)

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

// Раніше: доводилося вказувати всі параметри за порядком
setcookie('test', '', time() + 60 * 60 * 2, '/', '', false, true);

// PHP 8.0: вказуємо лише потрібне
setcookie(
    name: 'test',
    expires: time() + 60 * 60 * 2,
    httponly: true
);

Match Expression

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


$statusCode = 200;

$statusMessage = match ($statusCode) {
200, 300 => 'Успіх або редирект',
400, 404 => 'Помилка клієнта',
500 => 'Помилка сервера',
default => 'Невідомий статус',
};

Nullsafe Operator (?->)

Дозволяє читати властивості та викликати методи в ланцюжку. Якщо один з елементів null, увесь ланцюжок повертає null без викидання фатальної помилки.


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

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

Constructor Property Promotion (Просування властивостей у конструкторі)

Раніше для створення простих DTO (Data Transfer Objects) або Value Objects доводилося писати багато однотипного коду: оголошувати властивості, передавати їх у конструктор і присвоювати. PHP 8.0 об’єднує всі ці три кроки в один.

// PHP 7.4: Класичний (і багатослівний) підхід
class UserDTO 
{
    public string $name;
    public string $email;
    protected int $age;

    public function __construct(string $name, string $email, int $age) 
    {
        $this->name = $name;
        $this->email = $email;
        $this->age = $age;
    }
}

// PHP 8.0: Елегантно й лаконічно
class UserDTO 
{
    public function __construct(
        public string $name,
        public string $email,
        protected int $age,
    ) {}
}

Еволюція системи типів

У PHP 8.0 система типізації стала значно суворішою та виразнішою.

Union Types (Об’єднані типи)

До PHP 8.0, якщо змінна могла приймати кілька типів даних (наприклад, int або float), ми покладалися на PHPDoc. Тепер PHP підтримує це нативно.


class Calculator 
{
    private int|float $number;

    public function setNumber(int|float $number): void 
    {
        $this->number = $number;
    }
}

Псевдотип mixed

Новий тип mixed розв’язує проблему legacy-коду. Він еквівалентний array|bool|callable|int|float|null|object|resource|string. Зверніть увагу: mixed уже включає null, тому писати ?mixed або mixed|null не можна — це спричинить фатальну помилку.

Повернений тип static

Для реалізації патернів “Late Static Binding” і Fluent Interfaces додано тип поверненого значення static (раніше був лише self).


class BaseFactory {
    public function create(): static {
        return new static();
    }
}

Інтерфейс Stringable

Якщо клас реалізує магічний метод __toString(), PHP 8.0 автоматично (неявно) призначає йому інтерфейс Stringable. Це дозволяє використовувати string|Stringable у type hint-ах.

Attributes / Анотації (глибокий розбір)

Атрибути (PHP 8.0) — це структуровані метадані, які навішуються на класи, методи, властивості, параметри та константи. Рушій зберігає їх у байткоді; читаються через Reflection. Це не магія: нічого само не виконується, доки ваш фреймворк, роутер чи інструмент їх не прочитає — за схемою схоже на анотації в Java/C#, але нативно в PHP.

Атрибути vs PHPDoc

| | PHPDoc (@route, @deprecated) | Атрибути (#[Route]) | |---|-----------------------------------|------------------------| | Розбір | Рядок у коментарі; потрібні парсери | Синтаксис мови; без regex по коментарях | | Типізація | Неформальна; легко розійтися з кодом | Аргументи конструктора — справжні значення PHP | | Інструменти | Підтримка в IDE різна | Reflection API стабільний і швидкий |

PHPDoc залишайте для людської документації; атрибути — там, де метадані реально читає код (маршрутизація, підказки валідації, серіалізація, codegen).

Оголошення класу-атрибута

Будь-який клас може бути атрибутом, якщо позначений вбудованим #[\Attribute]. Бітова маска обмежує, де атрибут дозволений (Attribute::TARGET_*). Якщо цілі не вказати, за замовчуванням дозволені всі — зазвичай краще вказати явно.

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
final class Route
{
    public function __construct(
        public string $path,
        public array $methods = ['GET'],
    ) {}
}
  • Часто клас роблять final, щоб його не наслідували «випадково».
  • Параметри конструктора — це аргументи у місці застосування: #[Route('/x', methods: [...])].

Той самий елемент можна позначити кількома екземплярами одного атрибута, якщо додати Attribute::IS_REPEATABLE до маски — див. клас Attribute у мануалі.

Синтаксис застосування

Атрибути ставлять перед оголошенням; можна кілька підряд:

#[Route('/api/users', methods: ['GET', 'POST'])]
final class UserController {}

#[Route('/items')]
final class ItemController
{
    #[Route('/items/{id}', methods: ['GET'])]
    public function show(int $id): array
    {
        return ['id' => $id];
    }
}

Іменовані аргументи (PHP 8.0) природно відповідають параметрам конструктора атрибута.

Як це працює в runtime

  1. PHP розбирає атрибути під час компіляції й пов’язує їх із reflection-структурою.
  2. В runtime берете ReflectionClass, ReflectionMethod, ReflectionProperty тощо.
  3. getAttributes()ReflectionAttribute, далі newInstance() — екземпляр вашого класу з аргументами з коду.

Нічого «само» не викликається: bootstrap, DI-контейнер або роутер мають викликати Reflection (або бібліотеку-обгортку).

Читання атрибутів через Reflection

$reflection = new ReflectionClass(UserController::class);

foreach ($reflection->getAttributes(Route::class) as $attr) {
    /** @var Route $route */
    $route = $attr->newInstance();
}

foreach ($reflection->getAttributes() as $attr) {
    $name = $attr->getName();
    $args = $attr->getArguments();
}

Для маршрутів на методах — ReflectionMethod::getAttributes(), для валідації параметрів — ReflectionParameter::getAttributes().

Повний приклад (ідея мінімального «роутера»)

#[Route('/api/users', methods: ['GET', 'POST'])]
class UserController {}

$rc = new ReflectionClass(UserController::class);
$routeAttrs = $rc->getAttributes(Route::class);
if ($routeAttrs === []) {
    throw new RuntimeException('No Route on ' . UserController::class);
}
$route = $routeAttrs[0]->newInstance();
// зареєструвати $route->path у диспетчері...

Де атрибути доречні

  • HTTP routing / middleware — шлях і методи до контролерів (як метадані у стилі Laravel/Symfony).
  • Валідація та серіалізація — правила на властивостях для гідратора чи серіалізатора.
  • DI — підказки для автопідстановки залежностей (часто разом з іншими атрибутами).
  • Тести та внутрішні інструменти — групи фікстур, власні маркери deprecation, які читає CI.

Не варто вміщувати бізнес-логіку лише в атрибути: тримайте їх декларативними; складну логіку — у звичайних класах і сервісах.

Строге порівняння рядків і чисел

Це одна з найпідступніших змін, що ламають зворотну сумісність. Раніше при нестрогому порівнянні числа з рядком PHP конвертував рядок у число. У PHP 8.0, якщо рядок не є числовим, числа порівнюються як рядки[cite: 227, 228].


// PHP 7.4
0 == 'foobar' // true [cite: 224]

// PHP 8.0
0 == 'foobar' // false [cite: 226]

Синтаксичні покращення

Дозвіл ::class на об’єктах

Тепер можна отримати ім’я класу прямо зі змінної-об’єкта за допомогою ::class. Раніше для цього використовувалася функція get_class().


$object = new \App\Models\User();
// PHP 7: get_class($object);
echo $object::class; // Виведе "App\Models\User"

Висячі коми (Trailing commas)

Дозволено залишати висячу кому у списках параметрів функцій, методів і замикань. Це робить коміти в Git більш чистими.


public function makeRequest(
    string $url,
    array $data,
    array $headers, // <-- висяча кома тепер легальна [cite: 286]
) { ... }

Нові рядкові функції

Замість strpos() і перевірок на !== false додано три функції, що повертають строгий bool:


$str = "DevSense is awesome";

str_starts_with($str, "DevSense"); // true
str_contains($str, "awesome");     // true
str_ends_with($str, "awesome");    // true



Практичний рецепт: префікс/суфікс без strpos

if (!str_starts_with($path, '/var/www/app/')) {
    throw new InvalidArgumentException('Path escapes allowed root');
}
if (str_ends_with($filename, '.php')) {
    // обробка завантажених скриптів тощо
}

Функція get_debug_type()

Нова функція get_debug_type() повертає корисний тип змінної (наприклад, App\Models\User замість просто object, або int замість integer). Вона ідеально підходить для складання зрозумілих повідомлень про помилки.

Weak Maps (Слабкі карти)

Архітектурний прорив для ORM і кешування. WeakMap дозволяє створювати зв’язки між об’єктами так, щоб ці зв’язки не заважали Garbage Collector видаляти об’єкти з пам’яті.


class Cache {
    private WeakMap $cache;
    
    public function __construct() {
        $this->cache = new WeakMap();
    }
    
    public function getMetadata(object $obj): array {
        if (!isset($this->cache[$obj])) {
            $this->cache[$obj] = $this->computeExpensiveData($obj);
        }
        return $this->cache[$obj];
    }
}

Щойно об’єкт $obj знищується в застосунку, він автоматично зникає і з WeakMap, звільняючи пам’ять.

Throw Expression та обробка помилок

Оператор throw із інструкції (statement) перетворився на вираз (expression).


// Тепер можна викидати винятки прямо в тернарних операторах або при злитті
$user = $request->get('user') ?? throw new InvalidArgumentException('User is required');

$callable = fn() => throw new Exception('This should not run');

Глобальна зміна суворості: у PHP 8.0 оператор придушення помилок @ більше не приховує фатальні помилки. Крім того, більшість попереджень ядра (Engine Warnings) були перетворені на Error винятки (наприклад, спроба отримати властивість не-об’єкта тепер “роняє” скрипт, а не просто пише Warning у логи).

Non-capturing catches (анонімне перехоплення винятків)

Якщо вам потрібно перехопити виняток, але сам об’єкт винятку вам не потрібен, змінну тепер можна опустити.


// Раніше: ми були зобов’язані оголосити змінну $e
try {
    // код
} catch (Exception $e) {
    Log::error('Щось пішло не так');
}

// PHP 8.0:
try {
    // код
} catch (Exception) {
    Log::error('Щось пішло не так');
}

Помилки типів для вбудованих функцій

Більшість вбудованих функцій PHP тепер викидають суворі винятки TypeError або ValueError у разі передавання некоректних параметрів, замість того щоб видавати Warning і повертати null.


// PHP 7.4
strlen([]); // Warning: strlen() expects parameter 1 to be string, array given [cite: 231]

// PHP 8.0
strlen([]); // TypeError: strlen(): Argument #1 ($str) must be of type string, array given [cite: 237]
array_chunk([], -1); // ValueError: array_chunk(): Argument #2 ($length) must be greater than 0

JIT-компілятор і продуктивність

Найфундаментальніша архітектурна зміна “під капотом” PHP 8.0 — впровадження JIT (Just-In-Time) компілятора.

OPcache (До PHP 8): вихідний код парсився в OpCodes, а віртуальна машина Zend виконувала їх построково.

JIT (PHP 8.0+): аналізує OpCodes і компілює "гарячі ділянки" напряму в машинний код процесора (x86/ARM).

Для типових I/O-залежних вебзастосунків приріст становить приблизно 1–5%. Але для обчислень (CPU Bound) — від 300% до 500%.

Зміни в модулях і ресурсах

Триває очищення ядра від непрозорих типів resource. Їх замінюють на об’єкти:

cURL: curl_init() повертає клас CurlHandle.

GD: imagecreate() повертає GdImage.

Sockets: socket_create() повертає Socket.

Очищення пам’яті: нові об’єкти автоматично знищуються збирачем сміття. Функції на кшталт curl_close() більше не мають смислового навантаження.

Інші зміни розширень:

JSON тепер жорстко вбудований у ядро (його не можна вимкнути ключем --disable-json).

XML-RPC перенесено з ядра в PECL.

Додано математичну функцію fdiv(), яка дозволяє ділення на нуль (повертає INF, -INF або NAN замість помилки).

Триває очищення ядра від непрозорих типів resource. Їх замінюють на об’єкти:

  • cURL: curl_init() повертає CurlHandle.
  • GD: imagecreate() повертає GdImage.
  • Sockets: socket_create() повертає Socket.
  • Також зачеплено розширення OpenSSL, XMLWriter і функції XML.

Зворотно несумісні зміни (нотатки для міграції)

Навіть якщо ви не використовуєте новий синтаксис, апґрейд до PHP 8.0 може “зламати” застосунок через суворішу поведінку в runtime та видалення. Нижче — компактний чеклист того, що варто прогнати в CI та на staging.

Мова / ключові слова / видалення

  • match тепер reserved keyword.
  • mixed тепер reserved word (не можна використовувати для назв class/interface/trait і заборонено в namespaces).
  • __autoload() видалено. Використовуйте spl_autoload_register().
  • create_function() видалено. Використовуйте anonymous functions / closures.
  • each() видалено. Використовуйте foreach або ArrayIterator.
  • Видалено визначення констант без урахування регістру (define('FOO', 'bar', true) більше не підтримується).
  • Методи з таким самим іменем, як у класу, більше не трактуються як конструктори (використовуйте __construct()).
  • Більше не дозволено викликати non-static методи статично (це також впливає на перевірки на кшталт is_callable() при використанні імені класу).
  • Приведення (real) і (unset) видалено.

Помилки та діагностика (суворіше в runtime)

  • Assertion failures тепер за замовчуванням кидають виняток (перегляньте assert.*, напр. assert.exception).
  • ini-директиву track_errors видалено (тобто php_errormsg більше недоступна; використовуйте error_get_last()).
  • Оператор @ більше не “глушить” фатальні помилки. Error handlers не мають покладатися на error_reporting() == 0 для визначення suppression.
  • error_reporting за замовчуванням тепер E_ALL (включає E_NOTICE і E_DEPRECATED).
  • display_startup_errors увімкнено за замовчуванням.
  • Багато warning-ів стали Error exceptions (наприклад: запис у property не-об’єкта, невалідні типи ключів масиву/рядкових offset-ів, unpack не-array/Traversable, доступ до undefined unqualified constants).
  • Багато notice-ів стали warnings (undefined variables/properties/array keys, array-to-string conversion, невалідні string offsets тощо).

Числові рядки та приведення типів

  • Нестрогі порівняння чисел з нечисловими рядками змінилися (див. розділ “Строге порівняння рядків і чисел” вище).
  • “Saner numeric strings”: операції, які раніше давали warning/notice, тепер дають warning або кидають TypeError (особливо арифметичні/бітові операції з нечисловими рядками; поведінка стала суворішою і більш консистентною).
  • Приведення float → string тепер не залежить від locale.

Масиви / ключі / виклики / reflection

  • array_key_exists() більше не працює з об’єктами (використовуйте isset() або property_exists()).
  • Невалідні типи ключів для array_key_exists() обробляються суворіше та можуть кидати TypeError.
  • Ключі масиву в call_user_func_array() тепер інтерпретуються як імена параметрів (поведінка може змінитися, якщо ви ненавмисно передаєте асоціативні масиви).
  • debug_backtrace() і Exception::getTrace() більше не надають references до аргументів.

OOP edge cases

  • Використання parent у класі без батьківського класу тепер є фатальною compile-time помилкою.
  • Сигнатури magic methods тепер валідовуються, якщо оголошені (невідповідності можуть ламати код).

Extensions: notable BC breaks

  • cURL: CURLOPT_POSTFIELDS (та інші опції, що приймають масиви) більше не приймає об’єкти “як масив”. Якщо ви на це спиралися — робіть явний каст (array).
  • Date/Time: mktime() і gmmktime() тепер вимагають принаймні один аргумент.
  • DOM: прибрано низку нереалізованих DOM класів і методів (якщо ви використовували їх як заглушки/тестові сутності — код зламається).
  • Exif: read_exif_data() видалено; використовуйте exif_read_data().
  • Filter:
    • FILTER_FLAG_SCHEME_REQUIRED і FILTER_FLAG_HOST_REQUIRED видалено (для FILTER_VALIDATE_URL scheme/host і так завжди обов’язкові).
    • INPUT_REQUEST і INPUT_SESSION видалено як джерела для filter_input().
  • mbstring:
    • mbstring.func_overload видалено (а також пов’язані константи MB_OVERLOAD_* / записи в mb_get_info()).
    • mb_parse_str() більше не можна викликати без передавання результуючого масиву.
    • модифікатор e для mb_ereg_replace() видалено; використовуйте mb_ereg_replace_callback().
  • OpenSSL: openssl_seal() і openssl_open() тепер вимагають аргумент method (старий default "RC4" вважається небезпечним).
  • PCRE (Regular Expressions): невалідні escape-послідовності більше не інтерпретуються як літерали; модифікатор X тепер ігнорується.
  • PDO:
    • default error mode змінено з silent на exceptions.
    • змінилися сигнатури деяких методів (зокрема PDO::query() і PDOStatement::setFetchMode()).
  • Phar: metadata у phar більше не unserialize’иться автоматично (security hardening; код, що розраховує на неявний unserialize, треба змінювати).
  • Reflection:
    • ReflectionClass::newInstance(), ReflectionFunction::invoke() і ReflectionMethod::invoke() перейшли на variadics (...$args).
    • методи Reflection*::export() видалено (кастуйте reflection-об’єкти в string).
  • SPL:
    • SplFixedArray тепер IteratorAggregate (а не Iterator); низку методів ітерації видалено на користь getIterator().
    • spl_autoload_register() тепер завжди кидає TypeError на невалідних аргументах (параметр do_throw фактично ігнорується).
  • Standard library:
    • assert() більше не обчислює string-аргументи (використовуйте assert($a == $b), а не assert('$a == $b')); assert.quiet_eval / ASSERT_QUIET_EVAL видалено.
    • parse_str() більше не можна використовувати без передавання результуючого масиву.
    • опція 'salt' у password_hash() більше не підтримується (ігнорується з warning).

Підсумки (резюме)

Сприймайте PHP 8.0 як оновлення платформи: менше сюрпризів від нестрогих порівнянь, зрозуміліші відмови вбудованих API і типізація, яка нарешті відповідає великим кодовим базам. Виграш — менше багів лише на production і рівніший шлях до 8.1+, особливо якщо прибирати deprecations, доки ще є тести проти 7.4.