MidТеория2 min

Что нового в PHP 8.5

Обзор ключевых нововведений PHP 8.5: pipe-оператор, clone() с правкой свойств, URI-расширение, #[\NoDiscard], замыкания в const-выражениях

PHP 8.5 вышел 20 ноября 2025 года и стал очередным минорным релизом языка. На момент мая 2026 актуальная стабильная ветка — именно 8.5 (последний патч на эту дату — 8.5.6). Релиз продолжает курс на эргономику кода: меньше вложенных вызовов, меньше ручного копирования объектов, больше встроенных инструментов вместо сторонних библиотек.

Ниже разобраны главные изменения с примерами. Для каждого английского термина в скобках дан перевод.

Pipe-оператор |> (pipe operator — оператор конвейера)

Главная синтаксическая новинка. Оператор |> берёт значение слева и передаёт его как единственный аргумент (single argument — единственный аргумент) функции справа. Цепочка преобразований читается слева направо, а не "изнутри наружу", как при вложенных вызовах.

<?php

declare(strict_types=1);

$input = '  Hello World  ';

// Было (before — раньше): вложенные вызовы, читаются изнутри наружу
$result = strtolower(str_replace(' ', '-', trim($input)));

// Стало (after — теперь): конвейер, читается слева направо
$result = $input
    |> trim(...)                       // обрезать пробелы
    |> fn ($s) => str_replace(' ', '-', $s)
    |> strtolower(...);

// 'hello-world'

Синтаксис trim(...) — это first-class callable (синтаксис вызова как значения), появившийся ещё в 8.1. Если функция принимает несколько аргументов, оборачиваем её в стрелочное замыкание (arrow function — стрелочная функция), чтобы зафиксировать остальные параметры.

Важно: правая сторона должна быть вызываемой (callable — вызываемое), принимающей ровно один аргумент. Это не "магический пайп" из shell — параллелизма (parallelism — параллельное выполнение) здесь нет, всё выполняется последовательно.

clone() с модификацией свойств (clone with — клонирование с изменением)

Раньше для паттерна "wither" (изменяемая копия неизменяемого объекта) приходилось писать отдельный метод и вручную копировать поля, особенно для readonly-свойств (readonly — только для чтения), которые после конструктора менять нельзя. Теперь функция clone() принимает второй аргумент — массив новых значений свойств.

<?php

declare(strict_types=1);

final class Money
{
    public function __construct(
        public readonly int $amount,
        public readonly string $currency,
    ) {}
}

$price = new Money(amount: 100, currency: 'USD');

// Клон с заменой одного свойства, исходный объект не меняется
$discounted = clone($price, ['amount' => 80]);

echo $discounted->amount;   // 80
echo $price->amount;        // 100 — оригинал нетронут

Изменения применяются к копии после клонирования, поэтому даже readonly-поля можно переопределить в этом единственном безопасном месте. Это убирает горы шаблонного кода (boilerplate — шаблонный код) в иммутабельных (immutable — неизменяемых) объектах-значениях.

Расширение URI (URI extension — расширение для работы с URI)

В стандартную поставку добавлено встроенное расширение для разбора и нормализации (normalization — приведение к канонической форме) ссылок. Оно поддерживает два стандарта: RFC 3986 (классические URI) и WHATWG URL (правила браузеров). Раньше для надёжного парсинга URL приходилось тянуть сторонние пакеты, потому что старая функция parse_url() некорректно обрабатывала множество краевых случаев (edge cases — пограничные случаи).

<?php

declare(strict_types=1);

use Uri\Rfc3986\Uri;

$uri = new Uri('https://[email protected]:8080/path?q=1#top');

echo $uri->getScheme();   // 'https'
echo $uri->getHost();     // 'example.com'
echo $uri->getPort();     // 8080
echo $uri->getPath();     // '/path'
echo $uri->getQuery();    // 'q=1'

Доступен также класс по стандарту WHATWG (Uri\WhatWg\Url) — он ведёт себя так же, как разбор адреса в браузере, что важно для веб-приложений, которым нужна совместимость с клиентской логикой.

Атрибут #[\NoDiscard] (no discard — нельзя игнорировать)

Атрибут помечает функцию или метод, чьё возвращаемое значение нельзя молча выбрасывать. Если результат вызова не использован (не присвоен, не передан дальше), PHP выдаёт предупреждение (warning — предупреждение). Это страхует от классических ошибок с иммутабельными API, где метод возвращает новый объект, а не меняет текущий.

<?php

declare(strict_types=1);

final class Builder
{
    #[\NoDiscard('результат withName() надо использовать — объект иммутабельный')]
    public function withName(string $name): static
    {
        return clone($this, ['name' => $name]);
    }

    public readonly string $name;
}

$b = new Builder();

$b->withName('Vitalii');       // Warning: возвращаемое значение проигнорировано
$b2 = $b->withName('Vitalii'); // OK — результат сохранён

Если в каком-то месте игнорировать значение действительно нужно осознанно, его можно явно привести к (void) — тогда предупреждение не сработает.

Замыкания в const-выражениях (closures in constant expressions)

Теперь static-замыкания (static closure — статическое замыкание, без привязки к $this) и first-class callables можно использовать там, где раньше допускались только литералы: в значениях констант, в параметрах атрибутов, в значениях по умолчанию.

<?php

declare(strict_types=1);

#[\Attribute]
final class Validate
{
    public function __construct(
        public readonly \Closure $rule,
    ) {}
}

final class Form
{
    // Замыкание прямо в параметре атрибута — раньше было невозможно
    #[Validate(rule: static fn (string $v): bool => $v !== '')]
    public string $name = '';
}

Ограничение: замыкание должно быть static (без захвата $this) и без use с внешними переменными — ведь значение вычисляется на этапе компиляции константы, а не во время выполнения.

Прочие полезные функции

PHP 8.5 добавил две давно ожидаемые функции для массивов — дополнение к array_key_first()/array_key_last() из 7.3:

<?php

declare(strict_types=1);

$items = ['a' => 10, 'b' => 20, 'c' => 30];

echo array_first($items); // 10 — первое значение
echo array_last($items);  // 30 — последнее значение

Также расширена асимметричная видимость (asymmetric visibility — разная видимость на чтение и запись) на статические свойства, а атрибут #[\Override] (override — переопределение) теперь применим и к свойствам, а не только к методам.

Проверь себя

Сколько аргументов передаёт правой функции pipe-оператор |> в PHP 8.5?

Зачем нужен второй аргумент функции clone() в PHP 8.5?

Что произойдёт, если проигнорировать возвращаемое значение метода с атрибутом #[\NoDiscard]?

Какое ограничение есть у замыканий, используемых в const-выражениях PHP 8.5?