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.0throw— выражение.?->(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: порядок работ
- Сначала 7.4 без предупреждений. Включите
error_reporting=E_ALLна тестовом стенде и исправьте всеDeprecated: 7.4 заранее сообщает о вложенных тернарниках и приоритете конкатенации. - Статический анализ. PHP_CodeSniffer с набором PHPCompatibility находит несовместимый синтаксис, Rector автоматически переписывает часть кода, PHPStan или Psalm ловят ошибки типов.
- Зависимости. Обновите
composer.jsonдо версий пакетов, поддерживающих целевую версию, и прогонитеcomposer updateна ней. - Тесты на целевой версии. Запустите тесты в CI или контейнере с нужным образом, например
docker run --rm -v "$PWD":/app -w /app php:8.4-cli vendor/bin/phpunit. - Стенд и выкладка с откатом. Держите старый пул 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>|></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 и без него.



