Введение
PHP 8.2 вышел 8 декабря 2022 года. Из крупного в нём появились readonly-классы, DNF-типы, отдельные типы null, false и true, расширение Random и константы в трейтах. Но при обновлении с 8.1 больше хлопот доставляет другое: версия пометила устаревшими динамические свойства и интерполяцию ${}, и лог быстро заполняется предупреждениями.
Для проектов, которые до сих пор работают на 8.2, важнее всего сроки. Активная поддержка версии закончилась 31 декабря 2024 года, а исправления безопасности выходят до 31 декабря 2026 года. Дальше разобраны нововведения, изменения, которые ломают код при переходе с 8.1, и выбор целевой версии PHP с учётом требований Laravel 12 и 13.
Readonly-классы
В PHP 8.1 readonly ставился только на отдельное свойство. В DTO (объекте передачи данных), где неизменяемы все поля, модификатор приходилось писать в каждой строке конструктора.
PHP < 8.2
enum OrderStatus: string
{
case New = 'new';
case Paid = 'paid';
}
final class OrderData
{
public function __construct(
public readonly int $id,
public readonly OrderStatus $status,
public readonly float $total,
) {}
}
PHP 8.2
final readonly class OrderData
{
public function __construct(
public int $id,
public OrderStatus $status,
public float $total,
) {}
}
$order = new OrderData(1, OrderStatus::New, 1500.0);
$order->total = 0; // Error: Cannot modify readonly property OrderData::$total
В 8.2 достаточно одного readonly перед class — он распространяется на все свойства. Взамен класс получает несколько ограничений:
- у каждого свойства должен быть тип —
public $amount;вызовет ошибку компиляции; - статические свойства запрещены;
- динамические свойства создать нельзя, а атрибут
#[\AllowDynamicProperties]к такому классу не применяется; - readonly-класс может наследовать только readonly-класс, и его наследник тоже обязан быть readonly.
readonly class Price
{
public function __construct(public int $amount) {}
}
// class DiscountPrice extends Price {} // Fatal error: Non-readonly class DiscountPrice cannot extend readonly class Price
$price = new Price(100);
$price->currency = 'RUB'; // Error: Cannot create dynamic property Price::$currency
DNF-типы
В PHP 8.1 уже были объединения типов (A|B) и пересечения (A&B), но в одной декларации их нельзя было смешивать. Параметр «объект с двумя интерфейсами или null» приходилось проверять вручную.
DNF (Disjunctive Normal Form, дизъюнктивная нормальная форма) снимает это ограничение: пересечения можно перечислять через |. Правило одно — каждое пересечение внутри объединения заключается в скобки.
PHP < 8.2
final class OrderItemsPrinter
{
public function print(mixed $items): void
{
// тип приходится проверять вручную
if ($items !== null && !($items instanceof Countable && $items instanceof Traversable)) {
throw new InvalidArgumentException('Ожидается коллекция позиций или null');
}
echo $items === null ? 0 : count($items), PHP_EOL;
}
}
PHP 8.2
final class OrderItemsPrinter
{
// объект, который одновременно Countable и Traversable, либо null
public function print((Countable&Traversable)|null $items): void
{
echo $items === null ? 0 : count($items), PHP_EOL;
}
}
(new OrderItemsPrinter())->print(new ArrayIterator(['iPhone', 'Чехол'])); // 2
(new OrderItemsPrinter())->print(null); // 0
(new OrderItemsPrinter())->print([1, 2]); // TypeError
Скобки обязательны: если написать Countable&Traversable|null, файл не пройдёт разбор.
null, false и true как самостоятельные типы
Раньше null и false можно было указать лишь внутри объединения (?array, string|false), а типа true не было совсем. В 8.2 все три пишутся как самостоятельные типы. Пригождается это в двух случаях: когда реализация интерфейса или наследник сужает тип возврата и когда функция всегда возвращает одно и то же значение.
PHP < 8.2
interface ProductRepository
{
public function isArchived(): bool;
public function findDeleted(): ?array;
}
// function isArchived(): false {} // Fatal error: False can not be used as a standalone type
PHP 8.2
final class InMemoryProductRepository implements ProductRepository
{
// сужение типа возврата в реализации — допустимо
public function isArchived(): false
{
return false;
}
public function findDeleted(): null
{
return null;
}
}
function warmUpCache(): true
{
// ...
return true;
}
Объединить их в false|true нельзя — для этого есть bool.
Расширение Random
У rand(), mt_rand() и shuffle() один общий генератор Mersenne Twister на весь процесс. Если какая-нибудь библиотека вызовет mt_srand(), «случайные» значения во всём остальном коде станут предсказуемыми, и найти причину будет непросто.
Расширение random решает это объектным API. Состояние генератора хранится в объекте-движке (engine), а готовые операции — число из диапазона, байты, перемешивание — предоставляет класс Random\Randomizer.
use Random\Engine\Mt19937;
use Random\Randomizer;
// по умолчанию используется криптостойкий движок Random\Engine\Secure
$randomizer = new Randomizer();
$pinCode = $randomizer->getInt(1000, 9999); // код подтверждения
$promoCode = strtoupper(bin2hex($randomizer->getBytes(4))); // например, 9F04C1A2
// с зерном последовательность повторяется — удобно в тестах и сидерах
$seeded = new Randomizer(new Mt19937(42));
$winners = $seeded->pickArrayKeys(['anna' => 1, 'boris' => 2, 'vera' => 3], 2);
$queue = $seeded->shuffleArray([101, 102, 103, 104]); // id заказов
Кроме Secure (CSPRNG, криптостойкий генератор) доступны Mt19937, PcgOneseq128XslRr64 и Xoshiro256StarStar. Для токенов, паролей и кодов подтверждения подходит только Secure, остальные нужны там, где важна повторяемость. Рандомизаторы с одинаковым движком и зерном дают одну и ту же последовательность, так что упавший тест с тестовыми данными легко воспроизвести. У двух последних движков есть ещё jump(): он переносит состояние далеко вперёд, и из одного зерна получается несколько независимых потоков — например, по одному на Fiber или воркер.
Константы в трейтах
В 8.1 константа в трейте вызывала ошибку компиляции, поэтому общие для трейта значения выносили в интерфейс или прописывали прямо в методах.
PHP < 8.2
trait HasOrderLimits
{
public const MAX_ITEMS = 50; // Fatal error: Traits cannot have constants
}
PHP 8.2
trait HasOrderLimits
{
public const MAX_ITEMS = 50;
public function canAddItem(int $count): bool
{
return $count < self::MAX_ITEMS;
}
}
final class Cart
{
use HasOrderLimits;
}
echo Cart::MAX_ITEMS; // 50
echo HasOrderLimits::MAX_ITEMS; // Error: Cannot access trait constant HasOrderLimits::MAX_ITEMS directly
Константа принадлежит классу, в который подключён трейт. Через имя самого трейта к ней не обратиться — только через класс или через self:: в методах.
Устаревшие динамические свойства
PHP создаёт свойство на лету, если присвоить объекту поле, которого нет в объявлении класса. Чаще всего за этим стоит опечатка или забытое поле, поэтому с 8.2 такое присваивание выдаёт Deprecated.
class Customer
{
public string $name = '';
}
$customer = new Customer();
$customer->phone = '+7 900 000-00-00';
// PHP 8.1: работает молча
// PHP 8.2: Deprecated: Creation of dynamic property Customer::$phone is deprecated
$row = new stdClass();
$row->phone = '+7 900 000-00-00'; // stdClass — без предупреждений
На stdClass и классы с магическим методом __set() предупреждение не распространяется. В остальных случаях есть три выхода. Лучший — объявить свойство. Для старого кода, который пока нельзя переделать, подойдёт атрибут #[\AllowDynamicProperties] (его наследуют и дочерние классы). А если нужно прикрепить данные к объекту чужого класса, их можно хранить в WeakMap.
class Customer
{
public string $name = '';
public ?string $phone = null; // свойство объявлено
}
#[\AllowDynamicProperties]
class LegacyCustomer
{
public string $name = '';
}
$legacy = new LegacyCustomer();
$legacy->phone = '+7 900 000-00-00'; // без предупреждения
Предупреждение — только первый шаг: согласно RFC, в PHP 9.0 такая запись будет выбрасывать Error. Когда выйдет PHP 9, пока неизвестно. Найти проблемные места проще всего по логу deprecation-предупреждений на тестах или стейджинге, а недостающие свойства массово объявляет Rector.
Атрибут #[\SensitiveParameter]
В трассировку стека попадают аргументы вызовов, а вместе с ней — в логи и системы мониторинга. Параметр с атрибутом #[\SensitiveParameter] в трассировке показывается как объект SensitiveParameterValue, а не как значение.
function login(string $email, #[\SensitiveParameter] string $password): void
{
throw new RuntimeException('Сервис авторизации недоступен');
}
try {
login('admin@example.com', 'secret-123');
} catch (RuntimeException $e) {
echo $e->getTraceAsString();
// #0 ...: login('admin@example.c...', Object(SensitiveParameterValue))
}
Новые функции и методы
mysqli_execute_query()иmysqli::execute_query()готовят запрос, привязывают параметры и выполняют его за один вызов.memory_reset_peak_usage()обнуляет пиковое потребление памяти — пригодится для замеров в долгоживущих воркерах.ini_parse_quantity()превращает значения вида128M, полученные изini_get(), в число байт.curl_upkeep()поддерживает открытое соединение cURL и проверяет его.openssl_cipher_key_length()сообщает длину ключа для заданного шифра.sodium_crypto_stream_xchacha20_xor_ic()иlibxml_get_external_entity_loader().ReflectionFunction::isAnonymous()иReflectionMethod::hasPrototype()в Reflection.ZipArchive::getStreamIndex(),ZipArchive::getStreamName()иZipArchive::clearError().- Атрибуты
#[\AllowDynamicProperties]и#[\SensitiveParameter].
Устаревания и несовместимые изменения
Устаревший синтаксис пока работает и лишь пишет Deprecated в лог, так что обновление он не блокирует. Исправить его всё равно стоит заранее: в PHP 9 эти конструкции планируют убрать.
$name = 'Анна';
echo "Здравствуйте, ${name}"; // Deprecated: Using ${var} in strings is deprecated
echo "Здравствуйте, {$name}"; // рекомендуемая запись
$latin1 = utf8_decode('Café'); // Deprecated
$latin1 = mb_convert_encoding('Café', 'ISO-8859-1', 'UTF-8'); // замена
echo strtolower('ЗАКАЗ'); // 'ЗАКАЗ' — кириллица не меняется
echo mb_strtolower('ЗАКАЗ'); // 'заказ'
Устарело:
- интерполяция
"${var}"и"${expr}"— удаление запланировано в PHP 9; замена —"$var","{$var}"и"{${expr}}"; utf8_encode()иutf8_decode()— заменаmb_convert_encoding();- callable, которые понимает
call_user_func(), но не синтаксис$callable():"self::method","parent::method","static::method",["self", "method"],["parent", "method"],["static", "method"],["Foo", "Bar::method"],[new Foo, "Bar::method"]. Обычные"Order::method"и[Order::class, "method"]не затронуты; - внутренний метод
SplFileInfo::_bad_state_ex(); - кодировки QPrint, Base64, Uuencode и HTML-ENTITIES в функциях mbstring.
class OrderExporter
{
public function exportAll(array $orders): array
{
$old = array_map('self::formatRow', $orders); // Deprecated: Use of "self" in callables is deprecated
$new = array_map(self::formatRow(...), $orders); // работает, PHP 8.1+
return $new;
}
private static function formatRow(array $order): string
{
return $order['id'] . ';' . $order['total'];
}
}
Опаснее изменения поведения — они не выдают предупреждений, код просто начинает работать иначе:
strtolower(),strtoupper()и родственные функции (stripos(),ucfirst(),str_ireplace()и др.) перестали учитывать локаль и меняют регистр только у ASCII-символов. Если код рассчитывал наsetlocale()с однобайтовой кодировкой вроде CP1251, теперь нужныmb_*-функции;str_split('')возвращает пустой массив, а не[''];ksort()иkrsort()сSORT_REGULARсравнивают числовые строки по правилам PHP 8;DateTime::createFromImmutable()иDateTimeImmutable::createFromMutable()получили предварительный (tentative) тип возвратаstatic;SplFileObject::hasChildren()иSplFileObject::getChildren()получили предварительные типы возвратаfalseиnull; ряд методовSplFileObject(fgetc(),ftell(),fflush()и др.) теперь строго проверяет сигнатуру;- расширения ODBC и PDO_ODBC экранируют имя пользователя и пароль в строке подключения.
Сроки поддержки: что делать проектам на PHP 8.2
С 2024 года у каждой версии PHP одинаковый цикл: два года активной поддержки (исправления ошибок и безопасности), затем два года только исправлений безопасности; сроки заканчиваются 31 декабря. У Laravel для каждого мажорного релиза своя минимальная версия PHP, поэтому обновлять язык и фреймворк приходится согласованно.
| Версия | Релиз | Исправления ошибок до | Исправления безопасности до | Версии PHP |
|---|---|---|---|---|
| PHP 8.2 | 08.12.2022 | 31.12.2024 | 31.12.2026 | — |
| PHP 8.3 | 23.11.2023 | 31.12.2025 | 31.12.2027 | — |
| PHP 8.4 | 21.11.2024 | 31.12.2026 | 31.12.2028 | — |
| PHP 8.5 | 20.11.2025 | 31.12.2027 | 31.12.2029 | — |
| Laravel 12 | 24.02.2025 | 13.08.2026 | 24.02.2027 | 8.2–8.5 |
| Laravel 13 | 17.03.2026 | III квартал 2027 | 17.03.2028 | 8.3–8.5 |
Выход PHP 8.6 запланирован на 19 ноября 2026 года; сроки его поддержки появятся на php.net после релиза. Что войдёт в версию — в обзоре PHP 8.6.
Выводы из таблицы:
- с 1 января 2027 года найденные в PHP 8.2 уязвимости закрывать не будут, поэтому переезд нужно закончить до 31.12.2026;
- Laravel 12 уже получает только исправления безопасности (до 24.02.2027), а Laravel 13 на PHP 8.2 не встанет — ему нужен 8.3 или новее. Значит, на связке «PHP 8.2 + Laravel 12» сначала обновляется PHP и лишь потом фреймворк;
- PHP 8.3 — самый короткий шаг, но активная поддержка у него уже закончилась, а у PHP 8.4 закончится 31.12.2026. Для новых установок и больших переездов разумнее сразу выбрать PHP 8.5.
Прежде чем трогать сервер, полезно выяснить, какие зависимости не пускают на новую версию, и проверить код в контейнере с нужным PHP:
# какие пакеты из composer.lock не совместимы с PHP 8.5
composer why-not php 8.5
# синтаксическая проверка кода на PHP 8.5
docker run --rm -v "$PWD":/app -w /app php:8.5-cli \
sh -c "find app config routes -name '*.php' -print0 | xargs -0 -n1 php -l"
Затем тесты прогоняются на новой версии с включёнными E_DEPRECATED — так видно и ошибки, и то, что сломается в PHP 9. После этого в composer.json обновляются require.php и config.platform.php. Свежие версии PHP удобно запускать в контейнере — например, на FrankenPHP.
Что появилось после 8.2
- PHP 8.3 — типизированные константы классов, функция
json_validate(), атрибут#[\Override]. - PHP 8.4 — property hooks (хуки свойств) и асимметричная видимость свойств (
public private(set)). - PHP 8.5 — pipe-оператор
|>и замыкания в константных выражениях; подробнее — что изменилось в PHP 8.5. - PHP 8.6 — частичное применение функций и
clamp(); подробнее — обзор PHP 8.6.
Итог
- Нововведения PHP 8.2: readonly-классы, DNF-типы, типы
null/false/true, расширение Random, константы в трейтах и#[\SensitiveParameter]. - Большинство предупреждений после перехода с 8.1 дают динамические свойства, интерполяция
${},utf8_encode()/utf8_decode()и callable вида"self::method"; без предупреждений меняется поведениеstrtolower()и родственных функций. - Исправления безопасности для 8.2 выходят до 31.12.2026. Обновляться лучше сразу на PHP 8.5, минимально допустимый шаг — 8.3.
- Laravel 13 на PHP 8.2 не устанавливается, поэтому порядок такой: сначала PHP, затем фреймворк.