PHP 8: отличия от PHP 7, что нового и что сломается при переходе

PHP 8: отличия от PHP 7, что нового и что сломается при переходе Про IT

PHP 8 — мажорная линейка языка, которая началась с версии 8.0 (ноябрь 2020). Главные отличия от PHP 7 — новый синтаксис (именованные аргументы, match, оператор ?->, union types, атрибуты, свойства в конструкторе), JIT-компилятор и более строгое поведение: другие правила сравнения строк с числами и исключения TypeError там, где PHP 7 выдавал предупреждение и продолжал работу.

Важно различать три вещи. PHP 8.0 — релиз, в котором сосредоточены почти все несовместимости с 7.x. PHP 8.1-8.5 — минорные версии той же линейки, они добавляют возможности и точечные изменения поверх 8.0. Переход с PHP 7 на практике — это переход с 7.4 сразу на одну из поддерживаемых версий 8.x: ветки 7.x и 8.0-8.1 уже не получают обновлений безопасности.

Все примеры ниже прогнаны 24.09.2026 в официальных docker-образах: php:7.4-cli-alpine, php:8.0-cli-alpine и php:8.4-cli (PHP 8.4.25).

PHP 7.4 и PHP 8: сводная таблица отличий

Область PHP 7.4 PHP 8.0 и новее
Передача аргументов только по позиции можно по имени: f(id: 7)
Ветвление по значению switch, нестрогое == match, строгое ===, возвращает значение
Цепочка с возможным null проверки if ($x !== null) $a?->b?->c
Типы один тип или ?T union types int|float, тип mixed, static в возврате
Метаданные PHPDoc-комментарии атрибуты #[Name(...)]
Свойства класса объявить + присвоить в конструкторе объявить прямо в параметрах конструктора
0 == 'abc' true false
Неверный тип во встроенной функции Warning и NULL TypeError
Деление на ноль / Warning и INF DivisionByZeroError
JIT нет есть, в составе OPcache, по умолчанию выключен

Новый синтаксис одним примером

Скрипт ниже собирает основные нововведения 8.0 в один работающий файл. Скопируйте его в features.php и запустите php features.php.

<?php
declare(strict_types=1);

#[Attribute(Attribute::TARGET_CLASS)]
final class Table
{
    public function __construct(public string $name) {}
}

#[Table(name: 'orders')]
final class Order
{
    // PHP 8.0: свойства объявляются прямо в конструкторе
    public function __construct(
        public int $id,
        public int|float $total,          // union type
        public ?Customer $customer = null,
    ) {}
}

final class Customer
{
    public function __construct(public string $email) {}
}

function status(int $code): string
{
    // match: строгое сравнение (===), возвращает значение
    return match ($code) {
        200, 201 => 'ok',
        404      => 'not found',
        default  => throw new InvalidArgumentException("unknown code $code"),
    };
}

$order = new Order(id: 7, total: 1990.5);  // именованные аргументы

var_dump($order->customer?->email);        // nullsafe: null, без ошибки
echo status(201), PHP_EOL;
echo str_contains('php-8-release', '8') ? 'yes' : 'no', PHP_EOL;
echo $order::class, PHP_EOL;               // ::class на объекте

$attr = (new ReflectionClass(Order::class))->getAttributes(Table::class)[0];
echo $attr->newInstance()->name, PHP_EOL;

try {
    status(500);
} catch (InvalidArgumentException) {       // catch без переменной
    echo 'caught', PHP_EOL;
}

На PHP 8.0 и 8.4 вывод одинаковый:

NULL
ok
yes
Order
orders
caught

На PHP 7.4 тот же файл не запускается совсем:

Parse error: syntax error, unexpected 'public' (T_PUBLIC), expecting variable (T_VARIABLE) in /app/features.php on line 7

Обратите внимание: ошибка указывает на свойство в конструкторе, а не на строку с #[...]. В PHP 7 последовательность #[ начинается с #, то есть это однострочный комментарий, и атрибут просто игнорируется.

Что делает каждая строка

  • Свойства в конструкторе (constructor property promotion): public int $id в параметрах одновременно объявляет свойство и присваивает ему аргумент. Удобно для DTO и value-объектов; в классах с логикой в конструкторе список свойств теперь читается из его сигнатуры.
  • Union types: int|float — значение одного из перечисленных типов. ?Customer — короткая запись Customer|null.
  • Именованные аргументы: new Order(id: 7, total: 1990.5) — порядок можно менять, необязательные параметры пропускать. Побочный эффект: имя параметра становится частью публичного API, и его переименование в библиотеке ломает вызывающий код.
  • match возвращает значение, сравнивает строго, не требует break, а throw в нем допустим, потому что с 8.0 throw — выражение.
  • ?-> (nullsafe): если слева null, вся цепочка возвращает null без ошибки. Для записи ($a?->b = 1) оператор не работает.
  • Атрибуты — структурированные метаданные, которые читаются через Reflection. Сами по себе они ничего не выполняют: смысл им придает код фреймворка (маршруты в Symfony, правила валидации, маппинг ORM).
  • str_contains(), а также str_starts_with() и str_ends_with() заменяют конструкции вида strpos($s, '8') !== false.

switch и match: разница на одном входе

<?php
$code = '200';
switch ($code) {
    case 200: echo "switch: int 200\n"; break;
    default:  echo "switch: default\n";
}
echo match ($code) {
    200     => "match: int 200\n",
    default => "match: default\n",
};
switch: int 200
match: default

switch сравнивает нестрого, и строка '200' совпала с числом 200. match сравнивает через ===, поэтому ушел в default. Если в match нет подходящей ветки и нет default, PHP 8 бросает исключение UnhandledMatchError. Текст зависит от версии: для match (5.0) без подходящей ветки PHP 8.0 пишет Unhandled match value of type float, а PHP 8.4 — Unhandled match case 5.0, то есть показывает само значение.

Что сломается при переходе с PHP 7.4 на PHP 8

Сравнение чисел со строками

Один и тот же файл на двух версиях:

<?php
var_dump(0 == 'abc');     // строка без числа
var_dump(0 == '');        // пустая строка
var_dump('1' == '01');    // две числовые строки
var_dump(100 == '1e2');   // числовая строка в экспоненте
var_dump(42 == '42abc');  // число с хвостом
var_dump(null == false);
Выражение PHP 7.4 PHP 8.4
0 == 'abc' true false
0 == '' true false
'1' == '01' true true
100 == '1e2' true true
42 == '42abc' true false
null == false true true

Правило PHP 8: если строка числовая, число и строка сравниваются как числа; если нет — число приводится к строке и сравниваются строки. Изменилось только сравнение числа с нечисловой строкой. Опасные места — проверки вида if ($value == 0) для данных из формы или БД и in_array() без третьего аргумента true. Надежный вариант в обеих версиях — строгое === и явное приведение типа.

Ошибки вместо предупреждений

<?php
try {
    var_dump(strlen([]));
} catch (TypeError $e) {
    echo get_class($e), ': ', $e->getMessage(), PHP_EOL;
}
try {
    var_dump(10 / 0);
} catch (DivisionByZeroError $e) {
    echo get_class($e), ': ', $e->getMessage(), PHP_EOL;
}
echo 'Sum: ' . 1 + 2, PHP_EOL;
echo $undefined ?? 'default', PHP_EOL;
echo 'end', PHP_EOL;

PHP 7.4 (с error_reporting=-1) продолжает работу с мусорными значениями:

Deprecated: The behavior of unparenthesized expressions containing both '.' and '+'/'-' will change in PHP 8: '+'/'-' will take a higher precedence in /app/types.php on line 12

Warning: strlen() expects parameter 1 to be string, array given in /app/types.php on line 3
NULL

Warning: Division by zero in /app/types.php on line 8
float(INF)

Warning: A non-numeric value encountered in /app/types.php on line 12
2
default
end

PHP 8.4 бросает исключения, а конкатенация считает иначе:

TypeError: strlen(): Argument #1 ($string) must be of type string, array given
DivisionByZeroError: Division by zero
Sum: 3
default
end

На PHP 8.0 вывод тот же, отличается только имя параметра в тексте TypeError: Argument #1 ($str). Сообщения встроенных функций менялись между минорными версиями, поэтому в тестах проверяйте класс исключения, а не полный текст.

Без try/catch каждое из этих исключений остановит скрипт. Код, который раньше «тихо» работал с NULL вместо длины строки, на PHP 8 упадет — и это полезно: ошибка видна сразу, а не в отчете через месяц.

Другие изменения, которые чаще всего всплывают

Что было в PHP 7 Что в PHP 8 Как исправить
$a ? 'x' : $b ? 'y' : 'z' — Deprecated в 7.4 Fatal error: вложенный тернарный оператор без скобок не поддерживается расставить скобки или заменить на match
Notice: Undefined index / variable Warning: Undefined array key "id", Undefined variable $x ??, isset(), array_key_exists()
each(), create_function() удалены: Call to undefined function each() foreach, анонимные функции
ресурсы cURL, GD и др. объекты (CurlHandle, GdImage) заменить проверки is_resource() на !== false или instanceof
@ глушил и фатальные ошибки @ больше не скрывает фатальные ошибки не полагаться на @, обрабатывать исключения
сигнатура абстрактного метода трейта не проверялась проверяется при использовании трейта привести сигнатуру в классе к трейту

JIT в PHP 8: когда он ускоряет, а когда нет

JIT (Just-In-Time) — часть расширения OPcache, которая компилирует горячие участки байткода PHP в машинный код во время работы. Обычный путь — исходник, затем опкоды, затем их интерпретация виртуальной машиной Zend VM; JIT позволяет части опкодов выполняться как машинные инструкции без интерпретатора.

Проверка на вычислительной задаче (подсчет точек множества Мандельброта, сетка 600×600):

<?php
function mandel(int $size): int
{
    $inside = 0;
    for ($y = 0; $y < $size; $y++) {
        for ($x = 0; $x < $size; $x++) {
            $cr = 2.0 * $x / $size - 1.5;
            $ci = 2.0 * $y / $size - 1.0;
            $zr = $zi = 0.0;
            $i = 0;
            while ($i < 100 && $zr * $zr + $zi * $zi < 4.0) {
                $t = $zr * $zr - $zi * $zi + $cr;
                $zi = 2.0 * $zr * $zi + $ci;
                $zr = $t;
                $i++;
            }
            if ($i === 100) {
                $inside++;
            }
        }
    }
    return $inside;
}

$start = hrtime(true);
$result = mandel(600);
$ms = (hrtime(true) - $start) / 1e6;

$jit = function_exists('opcache_get_status')
    ? (opcache_get_status(false)['jit']['on'] ?? false)
    : false;
printf("result=%d time=%.0f ms jit=%s\n", $result, $ms, $jit ? 'on' : 'off');
php bench.php
php -d opcache.enable_cli=1 -d opcache.jit=tracing bench.php
result=139215 time=299 ms jit=off
result=139215 time=37 ms jit=on

Цифры — один прогон PHP 8.4.25 в docker на ARM64, на другой машине они будут другими; устойчиво только соотношение порядка «в несколько раз» для такого типа кода. На типичном веб-приложении (запрос в БД, шаблон, ответ) основное время уходит на ввод-вывод, и JIT дает заметно меньший выигрыш или не дает его вовсе. Ускорение веб-приложений при переходе с 7.4 обычно объясняется оптимизациями движка и OPcache, а не JIT.

Для PHP-FPM JIT включается в php.ini:

opcache.enable=1
opcache.jit=tracing
opcache.jit_buffer_size=64M

Умолчания зависят от версии: в PHP 8.4 opcache.jit=disable и opcache.jit_buffer_size=64M, а в 8.0-8.3 режим tracing задан, но буфер равен 0, поэтому JIT фактически выключен. Проверяйте фактическое состояние через opcache_get_status()['jit'], а не по конфигу. Поддержка ARM64 в JIT появилась в 8.1; в 8.0 он работал только на x86 и x86-64.

Как перейти с PHP 7.4 на PHP 8: порядок работ

  1. Сначала 7.4 без предупреждений. Включите error_reporting=E_ALL на тестовом стенде и исправьте все Deprecated: 7.4 заранее сообщает о вложенных тернарниках и приоритете конкатенации.
  2. Статический анализ. PHP_CodeSniffer с набором PHPCompatibility находит несовместимый синтаксис, Rector автоматически переписывает часть кода, PHPStan или Psalm ловят ошибки типов.
  3. Зависимости. Обновите composer.json до версий пакетов, поддерживающих целевую версию, и прогоните composer update на ней.
  4. Тесты на целевой версии. Запустите тесты в CI или контейнере с нужным образом, например docker run --rm -v "$PWD":/app -w /app php:8.4-cli vendor/bin/phpunit.
  5. Стенд и выкладка с откатом. Держите старый пул PHP-FPM рядом с новым и переключайте трафик так, чтобы вернуться на прежнюю версию одной правкой конфигурации.

Результат этапа — отчет анализатора без ошибок и зеленые тесты на целевой версии. Срок зависит от объема кода и покрытия тестами, универсальной оценки нет.

Если не получилось

Симптом после переключения Вероятная причина Что делать
Белая страница, в логе Uncaught TypeError встроенной функции пришел не тот тип привести тип на входе, проверить данные из БД и формы
Условие перестало срабатывать сравнение 0 == 'строка' перейти на === и явные приведения
Call to undefined function функция удалена в 8.0 заменить по таблице выше
Ошибка в vendor/ пакет не поддерживает PHP 8 обновить пакет или найти замену
Вызов ломается после обновления библиотеки переименован параметр, а вызов идет по имени зафиксировать версию пакета или обновить вызов

Что появилось после PHP 8.0

Переход с 7.4 в 2026 году ведет не на 8.0, а на актуальную версию, поэтому полезно знать и возможности следующих релизов:

Версия Заметные нововведения
8.1 перечисления enum, readonly-свойства, файберы, тип never
8.2 readonly-классы, типы DNF, типы null/false/true отдельно
8.3 типизированные константы классов, атрибут #[\Override], json_validate()
8.4 property hooks, асимметричная видимость, new без скобок в цепочке вызовов
8.5 оператор конвейера <code>&#124;></code>

Выводы

  • Почти все несовместимости PHP 8 с PHP 7 сосредоточены в релизе 8.0; версии 8.1-8.5 добавляют возможности поверх него.
  • Новый синтаксис 8.0 — именованные аргументы, match, ?->, union types, атрибуты и свойства в конструкторе — делает код короче, но файл с ним не запустится на 7.4.
  • Главные источники поломок: новое сравнение числа с нечисловой строкой, TypeError вместо предупреждений, удаленные функции, фатальная ошибка на вложенном тернарнике без скобок.
  • JIT заметно ускоряет вычислительный код, а на типичном веб-приложении дает небольшой эффект; по умолчанию он выключен.
  • Переходить стоит сразу на поддерживаемую версию 8.x: через чистый 7.4, статический анализ, обновление зависимостей и тесты на целевой версии.

Где применяется / связь с практикой

Освойте тему на практике

Переход на новую версию языка — типовая задача PHP-разработчика в поддержке живого проекта: найти несовместимости, обновить зависимости, не уронить продакшен и объяснить команде, что изменилось. Системно разобрать современный PHP, типизацию, тестирование и работу с фреймворками можно на курсе «PHP-разработчик. Продвинутый уровень». Разборы отдельных тем с преподавателями проходят на открытых уроках Otus.

FAQ

Можно ли перейти с PHP 7.4 сразу на 8.4, минуя 8.0-8.3?
Да, промежуточные версии ставить не нужно. Но изменения всех промежуточных релизов накапливаются, поэтому анализатор и тесты запускайте именно на целевой версии.

Код для PHP 8 можно запустить на PHP 7?
Только если в нем нет нового синтаксиса. Свойства в конструкторе, match, ?-> или union types дают Parse error на 7.4 еще до выполнения первой строки.

Нужно ли включать JIT на обычном сайте?
Не обязательно: выигрыш зависит от доли вычислений в коде. Решение принимайте по замеру на своем приложении с JIT и без него.

OTUS Журнал