Веб-разработка 194 просмотров

PHP 8.6 beta: что нового для разработчиков

Разбор PHP 8.6.0beta3: частичное применение функций, clamp(), Time\Duration, Io\Poll, новые дефолты сессий и депрекейты — с примерами и оговоркой, что это ещё не GA.

PHP 8.6 beta: что нового для разработчиков

10 сентября 2026 вышел PHP 8.6.0beta3. Это уже не «набросок RFC», а конкретный снимок ветки: язык получил частичное применение функций, clamp(), класс Time\Duration, отдельный Poll API и пачку практичных правок в стримах, сессиях и криптографии.

Это всё ещё beta. На прод ставить рано: баги ещё правят, а сигнатуры отдельных API могут чуть сдвинуться до GA (ориентир — 19 ноября 2026). Зато смотреть уже стоит — особенно если вы пишете Laravel-сервисы, очереди, интеграции и CLI-утилиты.


1. Частичное применение функций

Главное языковое изменение 8.6. Вы вызываете функцию, но вместо части аргументов ставите ? или ... — и получаете Closure, а не результат. Это логичное продолжение first-class callables из PHP 8.1 (strtoupper(...)) и хорошо стыкуется с pipe-оператором из 8.5.

<?php

$greet = str_replace('hello', 'hi', ?);

echo $greet('hello world'); // hi world

$titles = ['hello php', 'hello laravel'];
$mapped = array_map(str_replace('hello', 'hi', ?), $titles);
// ['hi php', 'hi laravel']

Плейсхолдеры:

  • ? — ровно один аргумент, который заполнят позже;
  • ... — «остальные аргументы как есть».
<?php

function discount(int $percent, float $price, bool $taxInclusive = true): float
{
    $net = $price * (1 - $percent / 100);
    return $taxInclusive ? $net : $net * 1.2;
}

$minus20 = discount(20, ...);
$minus20(1000);          // 800
$minus20(1000, false);   // 960

$onlyPrice = discount(15, ?, false);
$onlyPrice(200);         // 204

Вместе с |> это убирает лестницу одноразовых fn():

<?php

$names = ['  alice ', 'BOB', 'carol'];

$normalized = $names
    |> array_map(trim(...), ?)
    |> array_map(strtolower(...), ?);

// ['alice', 'bob', 'carol']

Практика: PFA копирует типы, значения по умолчанию и атрибуты вроде #[SensitiveParameter]. Для колбэков array_map/array_filter это чище, чем ручные стрелочные функции. Не превращайте в головоломку каждую строку: если без ? читается хуже — оставьте обычный вызов.


2. clamp()

Функция, которую в проектах копировали десятки раз: «если меньше минимума — минимум, если больше максимума — максимум».

<?php

clamp(1, 0, 3);   // 1
clamp(1, 2, 5);   // 2
clamp(4, 1, 3);   // 3

$opacity = clamp($request->integer('opacity'), 0, 100);
$retry   = clamp($attempt * 250, 250, 5_000);

Если $min > $max или в границы передали NAN, будет ValueError. Для пагинации, процентов, рейт-лимитов и UI-слайдеров это ровно то, что нужно.


3. Time\Duration

DateInterval умеет «2 месяца» — а месяцы разной длины. Для таймаутов, backoff и stopwatch это плохая модель. Time\Duration — readonly-класс «яичного таймера»: фиксированные секунды + наносекунды, без таймзон и календарных чудес.

<?php

use Time\Duration;

$timeout = Duration::fromMilliseconds(500);
$backoff = Duration::fromSeconds(1)->multiplyBy(2); // 2 секунды
$limit   = Duration::fromIso8601DurationString('PT1H30M');

if (Duration::compare($timeout, $backoff) < 0) {
    $wait = $backoff;
}

$poll->wait($timeout);

Фабрики: fromSeconds(), fromMilliseconds(), fromMicroseconds(), fromNanoseconds(), fromMinutes(), fromHours(), fromIso8601DurationString(). Операции иммутабельны: add(), sub(), multiplyBy(), divideBy(), negate(), absolute(). Сравнения <, === работают; арифметика через + — нет, только методы. Переполнение бросает Time\TimeException.

Важно: ISO-строка только с time-компонентой (PT...). P1D и «месяц» сюда не входят — это сознательно, чтобы таймаут всегда означал одно и то же число наносекунд.


4. Io\Poll: современный polling

stream_select() жив, но это старый select(): потолок дескрипторов, линейный рост стоимости, нет epoll/kqueue. В 8.6 появился Io\Poll — обёртка над epoll (Linux), kqueue (BSD/macOS), WSAPoll (Windows) и запасным poll().

Это не полноценный event loop вроде Revolt/AMPHP. Это низкоуровневый wait по нескольким стримам. Фреймворки смогут опереться на один бэкенд, а вам — написать простой TCP/WebSocket-сервер без ext-uv.

<?php

use Io\Poll\Context;
use Io\Poll\Event;
use Io\Poll\StreamPollHandle;
use Time\Duration;

$poll = new Context();
$server = stream_socket_server('tcp://127.0.0.1:8080');
stream_set_blocking($server, false);

$poll->add(new StreamPollHandle($server), [Event::Read], ['role' => 'accept']);

while (true) {
    foreach ($poll->wait(Duration::fromSeconds(1)) as $watcher) {
        if ($watcher->hasTriggered(Event::Read)) {
            // accept / fread / fwrite
        }
    }
}

В beta3 Context::wait() принимает Time\Duration (не пару «секунды + микросекунды» из раннего RFC) и отклоняет слишком большой $maxEvents.


5. Readonly-свойства с default

Раньше readonly без конструктора нельзя было инициализировать значением по умолчанию. В 8.6 можно:

<?php

final class FeatureFlags
{
    public readonly bool $darkMode = false;
    public readonly int $rolloutPercent = 0;
}

final readonly class ListQuery
{
    public function __construct(
        public string $q = '',
        public int $page = 1,
        public int $perPage = 20,
    ) {}
}

Для DTO и конфигов меньше пустого конструктора. После clone-with нетронутые readonly-поля остаются заблокированными — это правка поверх PHP 8.5, а не новая семантика clone.


6. Стримы, TLS и ошибки I/O

Здесь 8.6 выглядит как «наконец навели порядок», а не как игрушка.

  • Stream errors API. Классы StreamException, StreamError, enum’ы режима/кода, функции stream_last_errors() / stream_clear_errors(), опции контекста error_mode, error_store, error_handler. Вместо «warning где-то в логе» можно ловить типизированную ошибку.
  • Быстрое копирование. php_stream_copy_to_stream_ex() использует sendfile, splice, copy_file_range, TransmitFile.
  • Сокетные опции контекста: keepalive (so_keepalive, tcp_keepidle, …), so_reuseaddr, so_linger, а в beta3 — ещё so_rcvbuf и so_sndbuf.
  • TLS. Session resumption, внешний PSK, TLS 1.3 0-RTT / early data, статус WANT_READ/WANT_WRITE, AES-SIV, $salt_length для RSA-PSS в openssl_sign()/openssl_verify().
<?php

$ctx = stream_context_create([
    'socket' => [
        'so_keepalive' => true,
        'tcp_keepidle' => 60,
        'so_rcvbuf'    => 262144,
        'so_sndbuf'    => 262144,
    ],
    'ssl' => [
        'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT,
    ],
]);

7. Сессии: безопаснее из коробки

Дефолты, которые давно стоило включить:

  • session.use_strict_mode = 1
  • session.cookie_httponly = 1
  • session.cookie_samesite = Lax

Плюс: SessionHandler::validateId() реально работает, поэтому strict mode применяется к встроенному хендлеру. Объект сессии без create_sid()/validateId() — deprecated. Невалидные SameSite, lifetime и NUL в path/domain теперь получают warning.

Laravel и так выставляет HttpOnly/SameSite в большинстве приложений. Проверьте кастомные SessionHandler и легаси-проекты на чистом PHP: смена дефолтов может «внезапно» починить cookie — или сломать странный кросс-доменный логин, который жил на пустом SameSite.


8. Мелочи, которые реально пригодятся

ИзменениеЗачем
json_decode пишет место ошибкиКолонка/позиция в сообщении, а не абстрактный syntax error
SortDirection enumЯвное направление сортировки вместо магических int
pack()/unpack(): < и >Endianness в формате, без платформенных сюрпризов
Uri\Rfc3986\UriBuilderСборка URI поверх API 8.5, плюс host/type helpers
grapheme_strrev()Реверс строки по графемам, не по байтам
IntlNumberRangeFormatter«1–3 кг», «10–20 €» с учётом локали
mysqli_quote_string()Экранирование без устаревшего real_escape-ритуала
ZipArchive::openString()Открыть ZIP из строки в памяти
EXIF из WebPexif_read_data() больше не только JPEG/TIFF
gmp_prevprime(), gmp_powm_sec()Крипто-арифметика без самодельных циклов
PDO_PGSQL ATTR_CHUNK_SIZEВыборка большого result set кусками
libsodium KEM (X-Wing, ML-KEM768)Post-quantum обмен ключами, если собрали с libsodium 1.0.22
trim() режет и \fForm-feed в дефолтном наборе
array_intersect() быстрееБесплатный выигрыш на больших массивах
<?php

try {
    json_decode('{ "ok": true, }', flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    // В 8.6 в тексте есть позиция ошибки — проще найти лишнюю запятую
    logger()->warning($e->getMessage());
}

$binary = pack('n<', 0x1234); // little-endian 16-bit
[$value] = unpack('n<', $binary);

9. Что уже deprecated

8.6 продолжает зачистку алиасов 90-х и сомнительных перегрузок. На beta это warning, на 9.0 — кандидат на удаление.

  • is_long(), is_integer() → is_int(); is_double() → is_float(); doubleval() → floatval()
  • Третий аргумент define() (case-insensitive константы)
  • Имя функции readonly
  • return внутри finally
  • Объект в http_build_query() и в array_walk() / array_walk_recursive() — сначала get_object_vars()
  • Объект в фильтрах zlib/bzip2
  • spl_object_hash() → spl_object_id(); spl_classes() → Reflection
  • metaphone(), strcoll(), SORT_LOCALE_STRING
  • CSV-методы SplFileObject и почти все «удобные» методы ArrayIterator
  • mysqli_get_charset(), mysqli_stmt_init(), создание mysqli_stmt без SQL
  • mbregex; объекты в mb_convert_variables()
  • Nullable return type у __debugInfo(); возврат значения из __construct()/__destruct()
  • is_a()/is_subclass_of() со строкой, если $allow_string = false

Отдельно: больше 16 фильтров в php://filter без filter.max_filter_count — тоже deprecated. Если у вас самодельный pipeline из десяти convert.base64-*, проверьте.


10. Что это значит для Laravel

  1. Пока ничего не ставьте на прод. Laravel официально поедет на 8.6 после GA и зелёного CI.
  2. PFA упростит колбэки в коллекциях, jobs и pipeline. Pipe |> уже есть в 8.5 — в 8.6 он становится полезнее.
  3. clamp() заменяет самописные хелперы для page size, retry delay, процентов.
  4. Duration — хороший кандидат на таймауты HTTP-клиента, очередей и health-check, когда экосистема подтянется.
  5. Сессии и cookie. Если приложение не на Laravel Session, прогоните сценарии логина: SameSite=Lax и HttpOnly теперь дефолт движка.
  6. Депрекейты. Пройдитесь по is_integer, http_build_query($dto), кастомным SessionHandler и Spl-итераторам в легаси-пакетах.
<?php

$page = clamp((int) $request->query('per_page', 20), 1, 100);

$clean = $request->collect('tags')
    ->map(trim(...))
    ->filter(filled(...))
    ->values();

11. Как попробовать beta

# Официальные QA-сборки
https://www.php.net/pre-release-builds.php
https://downloads.php.net/~svpernova09/php-8.6.0beta3.tar.xz

git clone https://github.com/php/php-src.git --depth 1 --branch php-8.6.0beta3

# Docker (тег проверяйте)
docker run --rm php:8.6-rc php -v

Минимальный чеклист перед экспериментами:

  1. Поднять 8.6 рядом с 8.4/8.5, не вместо.
  2. Прогнать тесты с error_reporting=E_ALL — депрекейты всплывут сразу.
  3. Не включать JIT/Opcache «на удачу» в первом прогоне: сначала корректность, потом скорость.
  4. Смотреть NEWS между beta2 и beta3: там много security-фиксов (Phar UAF, SOAP WSDL, стримы, SimpleXML).

Вывод

PHP 8.6 beta3 — не косметический минор. Частичное применение делает колбэки человеческими, clamp() и Time\Duration закрывают дыры, которые все латали хелперами, Poll API и stream errors двигают I/O ближе к тому, что давно есть в Go/Node, а новые дефолты сессий чуть меньше оставляют дыр «из коробки».

До GA используйте как песочницу. Если нужен аудит готовности проекта к 8.6 или план миграции Laravel-стека — напишите мне в Telegram @rootseo или через страницу контактов.

Комментарии

Комментарии появляются после модерации.

Опубликованных комментариев пока нет. Будьте первым.

Написать в Telegram
Cookies Этот сайт использует файлы cookie для улучшения работы сервиса и аналитики. Продолжая использование сайта, вы соглашаетесь с обработкой данных.