PBfLsEU9nydV95LZfrOiV98qdM91zVQFY7AxnwJa.webpPHP 8.6 Features & Updates

Согласно графику релизов PHP 8.6, версия PHP 8.6.0 RC3 была зафиксирована тегом 8 октября 2026 года, а общий доступ (GA) запланирован на 19 ноября 2026 года. Всё еще планируются RC4 и RC5, поэтому детали могут измениться. Всё написанное ниже сверялось с файлами UPGRADING и NEWS для тега RC3, а не с целевыми версиями RFC. RFC со статусом «PHP 8.6» ничего не гарантирует до тех пор, пока код не будет объединен (merged).

Мое краткое мнение: PHP 8.6 — это релиз с одной языковой возможностью, которая меняет восприятие повседневного кода — частичным применением функций (partial function application), а также горсткой небольших, хорошо продуманных дополнений и длинным списком отшлифованных шероховатостей. Именно эти шероховатости отнимут у вас время, поскольку они кроются там, где никто не смотрит, пока что-нибудь не сломается: конфигурация сессий, валидация аргументов и старые SPL API.

Частичное применение функций — это то, что нужно изучить в первую очередь. Дефолтные настройки сессий и более строгая валидация аргументов — то, что нужно протестировать. Большинство устареваний носят механический характер и могут подождать до спокойного спринта, за исключением mbregex.

Дата актуализации информации: 11 октября 2026 года. Эта статья основана на PHP 8.6.0 RC3 и не охватывает ничего из опубликованного после этой даты.

В статье я разделяю два вопроса: насколько полезно нововведение (ценность для разработчика) и как может сломаться существующий код (серьезность миграции). Изменение дефолтного значения в одну строку может быть вам совершенно бесполезно и при этом сломать авторизацию.

Частичное применение функций (PFA) было принято единогласно и объединено в ветке 8.6 alpha3. Оно позволяет вызывать функцию с частью аргументов, а для остальных оставлять плейсхолдеры. Вместо выполнения вызов возвращает замыкание (Closure), которое ожидает недостающие аргументы. Предусмотрено два плейсхолдера:

  •  ? обозначает ровно один аргумент на этой позиции.
  •  ... обозначает ноль или более оставшихся аргументов. Вы уже знаете strlen(...) из синтаксиса callable первого класса — концептуально это частичное применение без привязанных аргументов.

Классический пример «до/после» выглядит следующим образом:

// PHP 8.5
$slugs = array_map(
    fn (string $title): string => str_replace(' ', '-', $title),
    $titles,
);

// PHP 8.6
$slugs = array_map(str_replace(' ', '-', ?), $titles);

Реальная ценность заключается в том, что намерение становится очевидным: «это str_replace() с первыми двумя фиксированными аргументами», без создания бросовых имен параметров. Движок тоже получает выгоду: согласно UPGRADING, array_map() с callable первого класса или частичным применением теперь компилируется в цикл без создания промежуточных замыканий.

PFA и оператор конвейера (pipe operator)

В PHP 8.5 был представлен оператор конвейера, однако его правая сторона должна быть callable, принимающим ровно один аргумент. Для всего остального требовались стрелочные функции в скобках. PFA заполняет этот пробел, поэтому The PHP Foundation назвала его идеальным дополнением для оператора pipe:

// PHP 8.5
$slug = $title
    |> trim(...)
    |> (fn (string $s): string => str_replace(' ', '-', $s))
    |> strtolower(...);

// PHP 8.6
$slug = $title
    |> trim(...)
    |> str_replace(' ', '-', ?)
    |> strtolower(...);

Здесь есть важная деталь для критически важных участков кода (hot paths): когда частичное применение с одним плейсхолдером располагается прямо справа от |>, компилятор превращает его в прямой вызов функции. Никакого замыкания не создается вовсе. Формы с двумя и более плейсхолдерами, такие как foo(?, ?), всё равно создают замыкание.

Аргументы связываются в момент создания

В этом заключается семантическое отличие от стрелочных функций, на котором многие погорят. Связанные аргументы вычисляются  один раз, при создании частичного применения, а их значения сохраняются в замыкании. Стрелочная функция вычисляет свое тело при каждом вызове.

function logLine(string $time, string $message): string
{
    return "[$time] $message";
}

$partial = logLine(date('H:i:s'), ?);                          // date() runs now, once
$arrow   = fn (string $m): string => logLine(date('H:i:s'), $m); // date() runs on every call

// Ten seconds later:
echo $partial('cache warmed'); // timestamp from creation time
echo $arrow('cache warmed');   // current timestamp

Обычно это именно то, что вам нужно, но при конвертации стрелочных функций в частичные проверяйте каждый связанный аргумент, который не является константой или обычной переменной.

Каждый ? создает обязательный параметр

Второе правило проистекает из дополнительного RFC — опциональные плейсхолдеры PFA, который ужесточил исходную семантику перед релизом. Параметры, созданные с помощью ?, всегда обязательны, даже если у базового параметра есть значение по умолчанию. Параметры, захваченные через ..., сохраняют свою опциональность. Непокрытые плейсхолдерами необязательные параметры просто используют свои дефолтные значения.

function connect(string $host, int $port = 5432, int $timeout = 5): Connection
{
    // ...
}

$a = connect(?, ?);   // closure(string $host, int $port) - $port is now required
$a('db');             // ArgumentCountError: too few arguments

$b = connect(?, ...); // closure(string $host, int $port = 5432, int $timeout = 5)
$b('db');             // fine, defaults apply

Будьте осторожны со старыми блогами и даже с оригинальной страницей RFC: в некоторых их примерах ? наследует значения по умолчанию. В RC3 поведение уже не такое; в него включен специальный тест для этого правила. Я считаю это решение правильным: знак ? означает «здесь должно быть значение».

 Ценность для разработчика: высокая. Это избавляет от множества мелких замыканий, отлично работает с оператором pipe и имеет понятную модель вычислений.  Серьезность миграции: нулевая — это новый синтаксис.

Другие полезные небольшие нововведения

Readonly-свойства со значениями по умолчанию

Readonly-свойства теперь могут объявлять значение по умолчанию, в том числе и в readonly-классах. Никакого нового синтаксиса нет. Важное правило: значение по умолчанию считается инициализирующим присваиванием. Вы не можете «переопределить» его в конструкторе.

final class RetryPolicy
{
    public readonly int $maxAttempts = 3;

    public function __construct(int $maxAttempts)
    {
        $this->maxAttempts = $maxAttempts; // Error: Cannot modify readonly property
    }
}

Если значение должно быть настраиваемым, используйте вместо этого продвинутый параметр конструктора (promoted parameter) со значением по умолчанию. __clone() и clone-with могут повторно инициализировать свойство один раз. Полезно, но нишево. Чтобы увидеть общую картину, почитайте, заменяют ли readonly-свойства геттеры.

clamp()

Функция clamp(mixed $value, mixed $min, mixed $max): mixed возвращает значение, если оно находится в пределах границ, или ближайшую границу в противном случае (RFC). Она использует стандартное сравнение PHP, поэтому работает как со строками и объектами DateTimeInterface, так и с числами.

// Before
$page = max(1, min($lastPage, $requestedPage));

// PHP 8.6
$page = clamp($requestedPage, 1, $lastPage);

Главный плюс в том, что clamp() выбрасывает ValueError, если $min превышает $max или одна из границ равна NAN. Вложенная конструкция с max/min молча возвращает 1, когда $lastPage равно 0 для пустого набора результатов; а clamp() выбросит исключение. Проверьте этот краевой случай, прежде чем массово заменять max(min()).

Time\Duration

Time\Duration — это финализированный readonly-класс для точных интервалов времени с наносекундной точностью. Экземпляры создаются через фабричные методы с именами вместо конструктора:

use Time\Duration;

$timeout = Duration::fromSeconds(2, 500_000_000);       // 2.5 seconds
$budget  = Duration::fromMinutes(1)->sub($timeout);     // 57.5 seconds
$sla     = Duration::fromIso8601DurationString('PT1H30M');

if ($elapsed > $budget) {
    // comparison operators work; + and - do not, use add() and sub()
}

Следует знать о двух ограничениях: нет интеграции с DateTime или DateInterval, а существующие функции, такие как sleep(), не принимают Duration. На сегодняшний день главная польза — это типизированная конфигурация (никаких больше int $timeout с неизвестными единицами измерения) и API пуллинга, который использует этот класс для таймаутов.

Изменения поведения по степени серьезности миграции

Безопасные настройки сессий по умолчанию

Я бы проверил это в первую очередь: изменение может сломать аутентификацию без какого-либо изменения кода. Меняются три дефолтных параметра INI (RFC):

  • session.use_strict_mode: с 0 на 1. PHP отклоняет идентификаторы сессий, которые он не создавал. Если вы разделяете сессии между поддоменами или сервисами, передавая принудительно заданный извне ID, это перестанет работать.
  • session.cookie_httponly: с 0 на 1. JavaScript больше не может читать куку сессии через document.cookie.
  • session.cookie_samesite: с unset на "Lax". Кука больше не отправляется при кросс-доменных POST-запросах. Это затрагивает некоторые сценарии SSO (например, SAML POST bindings) и обратные вызовы (callbacks) от платежных систем, которые отправляют POST-запросы обратно в сессию.

Это те настройки, которые по-хорошему и так должны были стоять в большинстве приложений, и фреймворки обычно конфигурируют параметры кук явно. Риск кроется в кастомной обработке сессий и устаревшем коде. Если вы зависите от старого значения, установите его явно перед обновлением.

Более строгая валидация аргументов

Многие функции теперь выбрасывают ValueError там, где раньше они игнорировали некорректный ввод, усекали его или выдавали предупреждение. Примеры из UPGRADING:

  • Некорректные значения режимов или флагов: array_filter() с недопустимым $mode, array_change_key_case(), флаги pathinfo(), порядок сортировки в scandir().
  • Числа вне допустимого диапазона: количество знаков после запятой в number_format(), sleep() и usleep() за пределами системных лимитов.
  • Нулевые байты (NUL bytes): getenv(), putenv(), parse_str(), setlocale() (ранее усекались), а также все семейство функций для работы с файлами, такие как file_exists(), is_file(), filesize() и stat().

Правило в отношении нулевых байтов заслуживает особого внимания. Код, передающий пользовательский ввод в file_exists(), раньше получал false для путей, содержащих \0. Теперь он получает неперехваченное исключение, что превращает тихий ответ «не найдено» в ошибку 500. Это лучше, но меняет поведение в рамках жизненного цикла запроса.

Мелкие шероховатости в той же категории: trim(), ltrim() и rtrim() теперь по умолчанию удаляют и символ подачи страницы (\f). base_convert(), bindec(), hexdec() и octdec() вызывают уведомление (notice), когда не могут выполнить точное преобразование. preg_grep() возвращает false при ошибке выполнения, а расширения GMP, Intl, GD, Sodium и другие аналогичным образом ужесточают валидацию.

Отчеты об ошибках

Новая опция INI error_include_args добавляет аргументы функций в сообщения об ошибках, используя то же форматирование, что и трассировка стека (RFC). По умолчанию она отключена, так что ничего не изменится, пока вы ее не включите. Если вы ее включите, проверьте, что именно попадает в ваши логи: параметры, помеченные как #[\SensitiveParameter], маскируются, а всё остальное — нет.

При критической ошибке нехватки памяти (hard out-of-memory error) PHP теперь вызывает abort() вместо exit(1). Код выхода меняется на 134, и может быть записан дамп ядра (core dump). Скорректируйте системы супервизоров и алертинга, завязанные на коды возврата.

Краевые случаи движка

Цикл foreach по ссылке (by-reference foreach) может повторно обойти элементы после компактизации массива, а ?? и empty() для магических свойств больше не вызывают __get(), если __isset() уже записал свойство. Это редкость, но важно для ORM и библиотек-прокси.

Устаревшая функциональность (Deprecations), сгруппированная по трудоемкости

Устаревание — это не ломающее изменение (breaking change). Ваш код продолжает работать и генерирует E_DEPRECATED. Рассматривайте это как задел на PHP 9, а не как блокираторы обновления. В RFC об устареваниях в PHP 8.6 каждое голосование описано отдельно.

 Механидированные исправления — поиск с заменой или правило Rector:

  • Неканонические псевдонимы скалярных функций is_integer(), is_long(), is_double() и doubleval(). Используйте is_int(), is_float() и floatval(). Приведения типов (integer) и (boolean) под это устаревание не подпадают.
  • spl_object_hash() — используйте spl_object_id(). Если вы сохраняли хеш в качестве строкового ключа, заменой будет целое число, поэтому проверьте сериализованные данные и ключи кэша.
  • Третий аргумент define(), spl_classes() и mysqli_get_charset().

 Требующие рассмотрения — решение зависит от намерений:

  • return внутри блока finally. Он молча отбрасывает исключения, поэтому для каждого такого случая нужно решить, что код должен был делать на самом деле.
  • SPL API: методы сортировки, флагов и сериализации в ArrayIterator (asort(), ksort(), uasort(), setFlags(), serialize() и др.), а также методы CSV в SplFileObject (fgetcsv(), fputcsv(), setCsvControl(), getCsvControl()).
  • Возврат значений из __construct() или __destruct(), вызов is_a() или is_subclass_of() со строкой, когда $allow_string равен false, а также передача объектов в array_walk() или http_build_query().
  • Кастомные обработчики сессий без create_sid() и validateId().

 Стратегические — спланируйте прямо сейчас: mbregex (mb_ereg(), mb_eregi(), mb_ereg_replace(), mb_split(), mb_regex_encoding() и сопутствующие) объявлен устаревшим, поскольку базовая библиотека Oniguruma прекратила поддержку (RFC). Удаление запланировано в PHP 9.0. Переход на PCRE с модификатором u обычно прост, но диалекты отличаются, поэтому сначала напишите тесты.

Стоит отметить то, что не прошло: кандидаты list() и псевдоинтернационализации _() были отвергнуты и остаются на месте.

Рантайм и инфраструктура

 Io\Poll. Новый API пуллинга предоставляет объектно-ориентированный примитив цикла событий: Context с автоматическим выбором бэкенда (epoll, kqueue, poll, event ports, WSAPoll), наблюдатели за дескрипторами потоков (stream handles), типизированные события и метод wait(), принимающий таймаут Time\Duration. Обратите внимание, что страница RFC до сих пор показывает старую сигнатуру wait(), основанную на целых числах. Это инструмент для авторов библиотек — асинхронных рантаймов, сокет-серверов, менеджеров процессов. Разработчики приложений выиграют от этого косвенно, как только библиотеки избавятся от костылей со stream_select().

 Ошибки потоков (Stream errors). Обработку ошибок потоков можно переключить с предупреждений (warnings) на исключения для каждого контекста потока по отдельности. По умолчанию остаются предупреждения, так что ничего не изменится, пока вы не включите это явно:

$context = stream_context_create([
    'stream' => ['error_mode' => StreamErrorMode::Exception],
]);

try {
    $body = file_get_contents($url, false, $context);
} catch (StreamException $e) {
    foreach ($e->getErrors() as $error) {
        // StreamError with code, message, wrapperName, ...
    }
}

Больше никаких оберток set_error_handler() только ради того, чтобы поймать ошибку. Больше всего от этого выиграют HTTP-клиенты, адаптеры хранилищ и задачи импорта.

 Расширение URI. URI-расширение из PHP 8.5 получает дальнейшие улучшения: текучие построители (fluent builders: Uri\Rfc3986\UriBuilder, Uri\WhatWg\UrlBuilder), инспекцию типов хостов и URI, а также функцию процентного кодирования WHATWG. Это полезно для редиректов, подписанных URL и проверок на SSRF, где конкатенация строк исторически приводила к багам.

 Производительность. UPGRADING перечисляет кэш статических замыканий, скомпилированные вызовы printf() и array_map(), более быстрый TAILCALL VM (теперь и на Windows с Clang), ускоренное кодирование JSON, а также более быстрые функции для работы с массивами и строками. Ожидайте прироста производительности «по чуть-чуть», а не скачкообразного изменения, и обязательно замеряйте показатели на своей рабочей нагрузке.

Чего нет в PHP 8.6

  •  Настоящая асинхронность (True Async) не вошла в релиз. RFC был отменен.
  •  Автоматический вывод статических замыканий не был принят. Из RFC по оптимизации замыканий в релиз попал только кэш безсостоятельных (stateless) замыканий. Замыкания, которые вы явно пометили как static, получают полную выгоду.
  •  SortDirection — это новое перечисление (enum) с элементами Ascending и Descending, но ни одна существующая функция сортировки его не принимает. SORT_ASC, SORT_DESC и константы scandir() остаются прежними.
  •  Написание в Reflection: новый метод называется ReflectionProperty::isWritable() рядом с isReadable(), несмотря на то, что в URL RFC написано «iswriteable».

Вердикт

PHP 8.6 — это обновление, к которому я бы запланировал переход в первый квартал после релиза; я бы не стал с ним спешить, но и откладывать в долгий ящик тоже. Частичное применение функций — самое полезное языковое нововведение для повседневного кода со времен хуков свойств, и в сочетании с оператором pipe оно наконец-то делает функциональный стиль практичным без лишнего шума от замыканий. Мелкие дополнения весьма разумны, и ничто из этого не заставляет вас в обязательном порядке менять код.

Риски миграции локализованы и легко проверяемы. Настройки сессий по умолчанию могут сломать продакшен без падения модульных тестов, а более строгая валидация в основном вытягивает наружу уже существующие баги. Устаревания — это технический долг на пути к PHP 9, причем mbregex — это пункт, который стоит запланировать к исправлению как можно раньше.

Порядок тестирования, которого я бы придерживался:

  1. Запустить весь набор тестов для PHP 8.6 RC в CI, сделав E_DEPRECATED видимым в логах, но не приводящим к падению сборки.
  2. Запустить статический анализ и проверку зависимостей: убедитесь, что ваш фреймворк и библиотеки заявляют о поддержке PHP 8.6.
  3. Сопоставить диффы эффективной конфигурации сессий и протестировать логин, SSO и платежные вебхуки (callbacks) от начала и до конца.
  4. Выполнить поиск пользовательского ввода, который попадает в функции файловой системы, окружения и локали, и протестировать пути обработки ошибок.
  5. Провести инвентаризацию mbregex, return в finally и устаревших методов SPL, а затем запланировать работы по их устранению.
  6. Развернуть приложение на стейджинге с трафиком, похожим на продакшен, и внимательно изучить логи ошибок и коды возврата перед официальным релизом.

Если ваша кодовая база все еще отстает на несколько версий, чек-лист миграции со старых версий PHP послужит лучшей отправной точкой, чем эта статья.

Источники

Планируете переход на PHP 8.6?

Я помогаю командам обновлять PHP-приложения без неприятных сюрпризов на продакшене — от миграции между версиями PHP 8 до работы со старыми кодовыми базами, отстающими на несколько мажорных версий (на чистом PHP, Symfony и Laravel).