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 в тайпхинтах.

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) естественно читаются: methods: ['GET'] попадает в параметр конструктора атрибута.

Как это работает в рантайме

  1. PHP разбирает атрибуты при компиляции и связывает их с reflection-структурой.
  2. В рантайме берёте 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();
    // $route->path, $route->methods
}

// Все атрибуты класса (фильтруете сами)
foreach ($reflection->getAttributes() as $attr) {
    $name = $attr->getName();       // например Route::class
    $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 может “сломать” приложение из‑за повышенной строгости рантайма и удалений. Ниже — компактный чеклист того, что стоит явно прогнать в CI и на staging.

Язык / ключевые слова / удаления

  • match теперь зарезервированное ключевое слово.
  • mixed теперь зарезервированное слово (нельзя использовать для имен классов/интерфейсов/trait и запрещено в namespaces).
  • __autoload() удален. Используйте spl_autoload_register().
  • create_function() удалена. Используйте анонимные функции / closures.
  • each() удалена. Используйте foreach или ArrayIterator.
  • Удалены определения констант без учета регистра (define('FOO', 'bar', true) больше не поддерживается).
  • Методы с именем, совпадающим с именем класса, больше не считаются конструкторами (используйте __construct()).
  • Больше нельзя вызывать non-static методы статически (это также влияет на проверки вроде is_callable() при использовании имени класса).
  • Касты (real) и (unset) удалены.

Обработка ошибок и диагностика (строже в runtime)

  • Ошибки assert теперь по умолчанию бросают исключение (проверьте assert.*, например assert.exception).
  • ini-директива track_errors удалена (значит php_errormsg больше недоступна; используйте error_get_last()).
  • Оператор @ больше не скрывает фатальные ошибки. Обработчики ошибок не должны полагаться на error_reporting() == 0 для определения подавления.
  • Уровень error_reporting по умолчанию теперь E_ALL (включает E_NOTICE и E_DEPRECATED).
  • display_startup_errors включен по умолчанию.
  • Многие warning стали Error-исключениями (например: запись в свойство не-объекта, невалидные типы ключей массива/строковых оффсетов, unpack не-массива/Traversable, доступ к неопределенным не квалифицированным константам).
  • Многие notice стали warnings (неопределенные переменные/свойства/ключи массива, преобразование массива в строку, невалидные строковые оффсеты и т. п.).

Числовые строки и приведение типов

  • Нестрогие сравнения чисел с нечисловыми строками изменились (см. раздел «Строгое сравнение строк и чисел» выше).
  • “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() больше не предоставляют ссылки на аргументы.

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’ится автоматически (hardening безопасности; код, завязанный на неявный unserialize, нужно менять).
  • Reflection:
    • ReflectionClass::newInstance(), ReflectionFunction::invoke() и ReflectionMethod::invoke() перешли на variadics (...$args).
    • методы Reflection*::export() удалены (используйте приведение reflection-объектов к строке).
  • SPL:
    • SplFixedArray теперь IteratorAggregate (а не Iterator); ряд методов итерации удален в пользу getIterator().
    • spl_autoload_register() теперь всегда бросает TypeError на невалидных аргументах (параметр do_throw по сути игнорируется).
  • Standard library:
    • assert() больше не вычисляет строковые аргументы (используйте assert($a == $b), а не assert('$a == $b')); assert.quiet_eval / ASSERT_QUIET_EVAL удалены.
    • parse_str() больше нельзя вызывать без указания результирующего массива.
    • опция 'salt' у password_hash() больше не поддерживается (игнорируется с warning).

Итоги (Резюме)

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