Введение
После 8.5 в PHP выходит очередная минорная версия — 8.6, и эта статья продолжает обзор PHP 8.5. Финальный релиз (GA) назначен на 19 ноября 2026 года. На момент написания доступен RC2 от 24 сентября, а по графику впереди ещё три кандидата: RC3 (8 октября), RC4 (22 октября) и RC5 (5 ноября).
Новых возможностей больше не добавят: RFC нужно было влить до 11 августа, а 22 сентября наступил hard feature freeze. Поэтому уже сейчас можно разобрать версию целиком — новые возможности с примерами, изменения поведения, которые затронут код без единой правки, устаревания и то, что в 8.6 так и не попало. Примеры проверены на php:8.6-rc-cli (8.6.0RC2).
Частичное применение функций
Самое заметное нововведение — partial function application, PFA (частичное применение функций). Функцию вызывают с частью аргументов, а на месте недостающих оставляют плейсхолдер:
?— один аргумент, который подставят позже;...— все оставшиеся аргументы разом.
На выходе получается Closure, которому остаётся передать только пропущенное. Знакомый по PHP 8.1 синтаксис first-class callable strlen(...) теперь выглядит как частный случай PFA.
$titles = ['hello world', 'hello php'];
// до 8.6: fn(string $s) => str_replace('hello', 'hi', $s)
$result = array_map(str_replace('hello', 'hi', ?), $titles);
// ['hi world', 'hi php']
PFA легко спутать со стрелочной функцией, но разница между ними существенная. Во-первых, заранее переданные аргументы PFA вычисляет один раз — когда создаётся замыкание, а стрелочная функция пересчитывает их на каждом вызове. Во-вторых, у замыкания остаются имена и типы параметров исходной функции: это видно через Reflection и позволяет вызывать его с именованными аргументами.
function discount(int $price, int $percent): int
{
return $price - intdiv($price * $percent, 100);
}
$pfa = discount(?, currentPercent()); // currentPercent() выполнится один раз
$arrow = fn(int $price) => discount($price, currentPercent()); // на каждом вызове
Если функция вызывается в цикле, а аргумент дорогой (запрос к БД, чтение конфига), это разница между одним вычислением и тысячей. Обратная сторона та же: если значение должно быть свежим на каждом вызове, нужна стрелочная функция.
С необязательными параметрами действует простое правило: там, где стоит ?, параметр становится обязательным, даже если у него было значение по умолчанию. Именованный плейсхолдер вроде separator: ? ведёт себя так же. Сохранить необязательность позволяет только ....
function makeSlug(string $title, string $separator = '-', int $maxLength = 60): string
{
return substr(strtolower(str_replace(' ', $separator, $title)), 0, $maxLength);
}
$slug = makeSlug(?, ?);
$slug('New Order'); // ArgumentCountError: ожидается 2 аргумента
$slug('New Order', '_'); // 'new_order'
$slug = makeSlug(?, ...);
$slug('Big Sale'); // 'big-sale', $separator по-прежнему необязателен
PFA как колбэк в коллекциях Laravel
Здесь PFA помогает обойти давнюю ловушку first-class callable. Метод map() у коллекций Laravel передаёт в колбэк два аргумента — значение и ключ. Функция, переданная как round(...), принимает оба, и ключ молча оказывается во втором параметре. Замыкание PFA без ... лишние аргументы просто отбрасывает:
$prices = collect([1.2345, 2.3456, 3.4567]);
$prices->map(round(...))->all(); // [1.0, 2.3, 3.46] — ключ стал $precision
$prices->map(round(?, 2))->all(); // [1.23, 2.35, 3.46] — ключ отброшен
Ошибка коварна тем, что исключения нет: на первом элементе ключ равен 0, и результат выглядит правдоподобно. Та же история с intval(...) (ключ превращается в основание системы счисления), trim(...) (ключ становится списком удаляемых символов) и любыми функциями с необязательными параметрами. Для колбэков надёжнее ?, чем ....
Связка с оператором |>
Полностью PFA раскрывается вместе с оператором |> из PHP 8.5. Раньше функцию с несколькими аргументами в пайпе приходилось оборачивать в стрелочную функцию, да ещё и брать её в скобки. В 8.6 хватает плейсхолдера:
$slug = ' New Order '
|> trim(...)
|> strtolower(...)
|> str_replace(' ', '-', ?); // 'new-order'
Накладных расходов при этом почти нет: если в вызове ровно один ?, оптимизатор OPcache (при включённом OPcache) заменяет такой шаг пайпа прямым вызовом функции, без создания замыкания. Кроме того, PFA разрешено в константных выражениях — это продолжение того, что в PHP 8.5 появилось для замыканий и first-class callables в константных выражениях. Например, значением свойства по умолчанию может быть private Closure $toInt = array_map(intval(?), ?);.
Функция clamp()
Задача «зажать значение в диапазон» встречается постоянно, и теперь для неё есть встроенная функция:
clamp(mixed $value, mixed $min, mixed $max): mixed
Значение внутри диапазона возвращается без изменений, вышедшее за него заменяется ближайшей границей. Сравнение подчиняется обычным правилам PHP, так что кроме чисел подходят строки и любые другие сравнимые значения. При $min > $max или границе, равной NAN, функция бросает ValueError.
$percent = clamp($request->integer('discount'), 0, 100); // скидка от 0 до 100 %
$page = clamp($request->integer('page'), 1, $lastPage); // номер страницы не выходит за пагинацию
$perPage = clamp($request->integer('per_page', 15), 10, 100);
clamp('m', 'a', 'f'); // 'f' — строки сравниваются лексикографически
clamp(5, 10, 1); // ValueError: $min больше $max
Привычная запись max($min, min($max, $value)) работает, но читается с усилием, и min с max в ней легко поменять местами — такая ошибка незаметна до первого граничного значения. У clamp() порядок аргументов говорит сам за себя.
Одна тонкость: типы функция не приводит. Если значение вышло за диапазон, вернётся граница — вместе со своим типом. В итоге clamp(1.5, 0, 1) даёт int(1), а clamp('150', 0, 100) — int(100), тогда как строка '50' внутри диапазона останется строкой. Когда результат записывается в типизированное свойство или сериализуется в JSON, границы лучше передавать того же типа, что и само значение.
Класс Time\Duration
Расширение date получило класс Time\Duration — длительность с наносекундной точностью, объявленную как final readonly, то есть неизменяемую. Конструктор закрыт, объекты создаются фабричными методами: fromSeconds() (наносекунды можно передать вторым аргументом), fromMilliseconds(), fromMicroseconds(), fromNanoseconds(), fromMinutes(), fromHours() и fromIso8601DurationString().
use Time\Duration;
$timeout = Duration::fromSeconds(30);
$sla = Duration::fromIso8601DurationString('PT1H30M'); // полтора часа
$spent = Duration::fromMilliseconds(1500);
$total = $sla->add(Duration::fromMinutes(15))->multiplyBy(2); // возвращается новый объект
$half = $sla->divideBy(2); // 45 минут
var_dump($sla > Duration::fromHours(1)); // true — операторы сравнения работают
var_dump(Duration::compare($timeout, $spent)); // 1
$diff = $spent->sub($timeout); // длительность со знаком «минус»
$diff->seconds; // 28 — без знака
$diff->nanoseconds; // 500000000
$diff->negative; // true
$diff->absolute(); // те же 28,5 секунды, но положительные
Помимо арифметики доступны negate() и absolute(). Состояние объекта описывают три свойства — $seconds, $nanoseconds и $negative; знак вынесен в отдельный флаг, поэтому $seconds никогда не бывает отрицательным. Ошибка разбора строки приводит к Time\TimeException, деление на ноль — к DivisionByZeroError.
Путать Duration с DateInterval не стоит: это разные сущности. DateInterval — календарный интервал вроде «1 месяц», чья реальная длина зависит от того, от какой даты его отсчитывать; к тому же он изменяем и не умеет складываться и делиться. Duration — фиксированное количество времени. Отсюда и ограничение fromIso8601DurationString(): принимается только временная часть (PT…), а на P1D или P1M метод ответит TimeException. Форматирования в классе пока нет, так что вывод в человекочитаемом виде придётся реализовать самому.
Readonly-свойства со значениями по умолчанию
До 8.6 у readonly-свойства не могло быть значения по умолчанию, и даже неизменную по смыслу константу приходилось присваивать в конструкторе. Теперь это разрешено. Больше всего выигрывают классы, реализующие свойства интерфейса из PHP 8.4:
interface HasCurrency
{
public string $currency { get; }
}
final class Price implements HasCurrency
{
public readonly string $currency = 'RUB';
public function __construct(public readonly int $amount) {}
}
echo new Price(1500)->currency; // RUB
Здесь легко ошибиться. Свойство со значением по умолчанию считается уже инициализированным, поэтому записать в него что-то другое не получится нигде, в том числе в конструкторе, — PHP выбросит Error: Cannot modify readonly property. Если значение по умолчанию нужно иногда переопределять при создании объекта, подходит прежний вариант: параметр конструктора (constructor property promotion) со значением по умолчанию.
Enum SortDirection и Laravel
В глобальном пространстве имён появился enum SortDirection с двумя кейсами — Ascending и Descending. Скалярных значений у него нет (unbacked enum): его задача — стать общим типом для направления сортировки вместо строк 'asc'/'desc' и параметров bool $descending. Функции ядра вроде sort() enum пока не принимают, это оставлено на будущее.
А вот Laravel его уже понимает. Начиная с 13.8.0 orderBy() и reorder() принимают SortDirection наравне со строками 'asc'/'desc', а orderedChunkById() и orderedLazyById() — вместо булева флага $descending. Методы orderByDesc(), latest() и oldest() направление не принимают, но внутри тоже перешли на enum. На PHP ниже 8.6 enum берётся из пакета symfony/polyfill-php86, который фреймворк подтягивает как зависимость.
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class OrderController
{
public function index(Request $request): View
{
$direction = $request->boolean('oldest_first')
? SortDirection::Ascending
: SortDirection::Descending;
$orders = Order::query()
->where('status', 'paid')
->orderBy('created_at', $direction) // раньше: 'asc' / 'desc'
->paginate(20);
return view('orders.index', compact('orders'));
}
}
Выигрыш — в проверке типов. Опечатка 'dsc' в строке всплывёт только при выполнении запроса, а несуществующий SortDirection::Dsc подсветят IDE и статический анализатор ещё до запуска.
Мелкие, но полезные изменения
- Атрибут
#[\Override]можно ставить на константы классов и кейсы enum. Если у родителя или интерфейса такой константы нет, код не скомпилируется. - В enum разрешён метод
__debugInfo()— он управляет тем, что покажетvar_dump(). - Объект, сохранённый в константе, теперь изменяется напрямую:
CONFIG->env = 'dev'. - Новая INI-настройка
error_include_argsдописывает аргументы функции в сообщения об ошибках. По умолчанию она выключена, а параметры с#[\SensitiveParameter]в вывод не попадают. json_last_error_msg()и текстJsonExceptionпоказывают, где именно ошибка:Syntax error near location 1:10.- В Reflection добавлены
ReflectionProperty::isReadable()/isWritable()иReflectionParameter::getDocComment()— докблоки параметров больше не теряются. - Производительность: вызов
array_map()с first-class callable или PFA-колбэком компилируется в обычныйforeach, промежуточное замыкание не создаётся. Статические замыкания, не захватывающие переменных, теперь кэшируются — явныйstatic fnначинает окупаться. - Прочее одной строкой:
grapheme_strrev(),mysqli_quote_string(), модификаторы порядка байтов</>вpack()/unpack(), Poll API (Io\Poll\*), обработка ошибок потоков (StreamException,stream_last_errors()), билдерыUri\Rfc3986\UriBuilderиUri\WhatWg\UrlBuilderдля URI-расширения из 8.5.
Что изменится без правок кода
| Где | Было | Стало в 8.6 |
|---|---|---|
session.use_strict_mode |
0 |
1 — неизвестные ID сессий отклоняются |
session.cookie_httponly |
0 |
1 — cookie недоступна из JavaScript |
session.cookie_samesite |
не задано | Lax |
trim(), ltrim(), rtrim() |
\f не удалялся |
form feed \f удаляется по умолчанию |
array_filter() с неверным $mode |
молча фильтровал по значению | ValueError |
preg_grep() при ошибке PCRE |
частичный массив | false, как у других preg_* |
К этому стоит добавить редкий случай: foreach по ссылке после уплотнения массива может пройти по элементам, которые раньше пропускал.
Laravel-приложений новые значения session.* практически не касаются. Фреймворк не вызывает session_start(): данные сессии хранит Illuminate\Session\Store, а cookie выставляет middleware StartSession, опираясь на config/session.php (http_only, same_site). Изменения почувствует только код на нативных сессиях — старые скрипты рядом с приложением или сторонние библиотеки.
Устаревания
Устаревшие конструкции продолжают работать, но выдают E_DEPRECATED. Главное из списка:
| Что устарело | Чем заменить |
|---|---|
return в блоке finally |
вернуть значение после try/finally |
возврат значения из __construct()/__destruct() |
убрать return $value |
is_double(), is_long(), is_integer() |
is_float(), is_int() |
doubleval() |
floatval() |
strcoll(), флаг SORT_LOCALE_STRING |
Collator::compare(), Collator::sort()/asort() |
metaphone() |
библиотека фонетического сравнения |
spl_object_hash() |
spl_object_id() |
ArrayIterator::getFlags(), setFlags(), asort(), ksort(), uasort(), uksort(), natsort(), natcasesort(), serialize(), unserialize() |
сортировать массив до создания итератора или использовать ArrayObject, у которого методы остаются; для сериализации — функции serialize()/unserialize() |
SplFileObject::fgetcsv(), fputcsv(), setCsvControl(), getCsvControl() |
функции fgetcsv()/fputcsv() с дескриптором из fopen() и явным аргументом $escape |
объект в http_build_query() |
http_build_query(get_object_vars($obj)) |
третий аргумент define() |
убрать аргумент |
mbregex (mb_ereg*()), Oniguruma больше не поддерживается |
preg_*() с модификатором u |
Из более редкого: функция с именем readonly, namespace в роли имени константы класса, is_a()/is_subclass_of() со строкой при $allow_string = false, ReflectionMethod::invoke() с объектом для статического метода, spl_classes(), mysqli_get_charset()/mysqli_stmt_init(), объекты в array_walk().
Собственные обработчики сессий в Laravel
Если класс реализует SessionHandlerInterface, но не содержит методов create_sid() и validateId() (и не реализует SessionIdInterface и SessionUpdateTimestampHandlerInterface), PHP теперь сообщает об этом уже при объявлении класса: в PHP 9.0 оба метода станут обязательными. Встроенные обработчики Laravel получили их в 13.31.0 и 13.32.0 (PR #61469 и #61517). Собственные — например, для хранения сессий в отдельной базе — придётся дополнить вручную:
use Illuminate\Support\Str;
final class TenantSessionHandler implements SessionHandlerInterface
{
// open(), close(), read(), write(), destroy(), gc() — без изменений
public function create_sid(): string
{
return Str::random(40); // как в Illuminate\Session\Store
}
public function validateId(string $id): bool
{
$data = $this->read($id);
return is_string($data) && $data !== ''; // read() может вернуть false
}
}
Откладывать это не стоит. UPGRADING относит изменение к устареваниям, но в 8.6.0RC2 уведомление приходит с уровнем E_WARNING, а не E_DEPRECATED. Laravel превращает предупреждения в ErrorException, так что без этих методов запрос упадёт при первой же загрузке класса обработчика, а не оставит тихую запись в логе deprecations. До финального релиза уровень могут понизить, но рассчитывать на это не стоит: методы всё равно понадобятся для PHP 9.0.
А вот list() остаётся в языке: RFC о его устаревании не прошёл — 23 голоса «за» и 23 «против» при необходимых двух третях.
Что не вошло в PHP 8.6
В ранних обзорах встречаются возможности, которых в 8.6 не будет:
| RFC | Статус |
|---|---|
Pipe Assignment Operator (|>=) |
отклонён |
| Bound-Erased Generic Types (дженерики) | отклонён |
array_only_keys()/array_except_keys() |
отклонён |
array_path_get() |
отклонён |
магический метод __exists() |
отклонён |
Time\Instant и Time\Clock |
на обсуждении, не успели |
флаг PREG_THROW_ON_ERROR |
на обсуждении, не успели |
| автозагрузка функций | на обсуждении, не успели |
устаревание «нечётких» приведений ((int) "123abc") |
на обсуждении, не успели |
| асинхронность в ядре | на обсуждении, не успели |
RFC, которые не успели довести до голосования, могут попасть в следующую версию. Time\Instant и Time\Clock стали бы естественным дополнением к Duration, но пока это только предложения.
Как попробовать и что проверить перед обновлением
Удобнее всего запускать релиз-кандидат в Docker — локальная установка PHP при этом не затрагивается:
docker run --rm -i php:8.6-rc-cli php -v
docker run --rm -v "$PWD":/app -w /app php:8.6-rc-cli php -l app/Models/Order.php
# ищем устаревшие функции в своём коде и зависимостях
grep -rnE "spl_object_hash|is_double|is_long|is_integer|doubleval|strcoll|SORT_LOCALE_STRING" app/ vendor/
grep -rln "implements.*SessionHandlerInterface" app/ vendor/
План проверки:
- Прогнать тесты на RC с
error_reporting=E_ALL— так видны всеE_DEPRECATED. - Поискать устаревшие функции в
app/иvendor/; особенно часто в пакетах встречаетсяspl_object_hash(). - Проверить собственные обработчики сессий и код, работающий с нативными сессиями.
- Обновить Laravel до 13.32 или новее — в этих версиях есть поддержка
SortDirectionи недостающие методы встроенных обработчиков сессий.
Итог
- PHP 8.6 выходит 19 ноября 2026 года, набор возможностей окончательный.
- Можно использовать сразу:
clamp()вместоmax(min()), плейсхолдеры?вarray_map()и пайпах,SortDirectionвorderBy()на Laravel 13.8+,Time\Durationдля таймаутов и SLA. - Проверить до обновления: устаревшие функции в коде и
vendor/, собственные обработчики сессий,returnвнутриfinally. - Дженериков,
|>=иTime\Clockв 8.6 не будет, аlist()устаревшим не объявлен.