PHP 8.5 на практике: pipe-оператор, URI extension и clone with — PROG-TIME

PHP 8.5 на практике: pipe-оператор, URI extension и clone with

01.06.2026

PHP 8.5 вышел 20 ноября 2025 года и стал заметным обновлением языка: новый pipe-оператор, встроенный URI-парсер, синтаксис clone($obj, [...]) для readonly-классов, атрибут #[\NoDiscard] и десяток мелких удобств. Активная поддержка версии заявлена до конца 2027 года, поэтому есть смысл присмотреться сейчас и не тащить за собой костыли из 8.4. В этой статье — практический разбор того, что действительно пригодится в реальном коде, без перечисления каждой строчки changelog.

Pipe-оператор: пайплайны без матрёшки вызовов

Самая обсуждаемая фича релиза. Оператор |> позволяет передавать значение слева направо через цепочку callable. Раньше типичный код для генерации слага выглядел как вложенные вызовы, читаемые «изнутри наружу»:

$title = ' PHP 8.5 Released ';

$slug = strtolower(
    str_replace('.', '',
        str_replace(' ', '-',
            trim($title)
        )
    )
);
// "php-85-released"

В 8.5 это же преобразование пишется линейно — сверху вниз, шаг за шагом:

$slug = $title
    |> trim(...)
    |> (fn($s) => str_replace(' ', '-', $s))
    |> (fn($s) => str_replace('.', '', $s))
    |> strtolower(...);

Слева от |> — значение, справа — любой callable: имя функции через first-class callable синтаксис strtolower(...), замыкание, статический метод или объект с __invoke. Оператор пропускает ровно один аргумент, поэтому функции с дополнительными параметрами оборачиваем в стрелочное замыкание.

На что хорошо ложится pipe в реальных проектах: нормализация входных данных (трим, lowercasing, очистка от лишних символов), цепочки трансформаций над DTO, обработка результата запроса перед сериализацией. На что плохо ложится — там, где у вас несколько «веток», нужны промежуточные имена для логирования или условная остановка. В таких местах оставляйте обычные переменные, чтобы не превращать pipe в новую матрёшку.

URI extension: пора прощаться с parse_url

Функция parse_url жила в PHP с незапамятных времён, и её главная проблема — она не парсит URL по стандартам, а лишь приблизительно разбирает строку. Это давно приводило к расхождениям с тем, что видит браузер, и к уязвимостям типа host-injection.

В 8.5 появилось встроенное расширение Uri с двумя классами: Uri\Rfc3986\Uri (строгий RFC 3986) и Uri\WhatWg\Url (стандарт, который реализуют браузеры). Базовый разбор:

use Uri\Rfc3986\Uri;

$uri = new Uri('https://user:pass@prog-time.ru:8080/path?x=1#frag');

$uri->getScheme();   // "https"
$uri->getHost();     // "prog-time.ru"
$uri->getPort();     // 8080
$uri->getPath();     // "/path"
$uri->getQuery();    // "x=1"
$uri->getFragment(); // "frag"

Объект иммутабельный: чтобы получить изменённый URI, используется withX-методы.

$normalized = (new Uri\WhatWg\Url('HTTPS://Prog-Time.RU/Path/../A'))
    ->withPath('/articles');

// нормализация хоста, схемы и path выполняется по стандарту

На практике это закрывает сразу несколько типовых задач: безопасный редирект (можно проверить, что хост из whitelist), построение canonical-URL для SEO, корректная склейка относительных ссылок при парсинге. Если в коде встречается parse_url + ручная конкатенация — это первый кандидат на замену.

Clone with: with-er pattern без боли

Readonly-классы и DTO стали стандартом для слоёв домена, но обновление одного поля выглядело уродливо: либо вручную перечислять все аргументы конструктора, либо использовать get_object_vars и спред:

readonly class Color
{
    public function __construct(
        public int $red,
        public int $green,
        public int $blue,
        public int $alpha = 255,
    ) {}

    public function withAlpha(int $alpha): self
    {
        $values = get_object_vars($this);
        $values['alpha'] = $alpha;
        return new self(...$values);
    }
}

В 8.5 функция clone() принимает второй аргумент — ассоциативный массив свойств, которые нужно переопределить:

readonly class Color
{
    public function __construct(
        public int $red,
        public int $green,
        public int $blue,
        public int $alpha = 255,
    ) {}

    public function withAlpha(int $alpha): self
    {
        return clone($this, ['alpha' => $alpha]);
    }
}

$blue = new Color(79, 91, 147);
$transparent = $blue->withAlpha(128);

Переопределять можно несколько свойств за раз. Под капотом PHP создаёт копию объекта и применяет указанные значения — для readonly-полей это единственный момент, когда их можно изменить. Особенно приятно в Value Object-ах из Laravel-домена, Symfony Messenger payload-ах и event-ах в EventSourcing.

#[\NoDiscard]: «не игнорируй мой результат»

Новый атрибут предупреждает, если возвращаемое значение функции не используется. Это полезно в API, где результат — это статус операции или новый объект (а не побочный эффект).

#[\NoDiscard]
function validate(Request $request): Result
{
    return Result::ok();
}

validate($request);
// Warning: The return value of function validate() should
// either be used or intentionally ignored by casting it as (void)

(void) validate($request); // намеренно проигнорировали

Хорошие кандидаты на пометку: фабрики (UserBuilder::build()), with-er методы, валидаторы возвращающие Result, всё что возвращает immutable-копию. Это статически ловит классическую ошибку «забыл присвоить результат» в IDE и в линтере.

Мелочи, которые экономят строки

Две новые функции массивов снимают вечный вопрос «как взять первый/последний элемент, не зная ключа»:

$first = array_first($events); // null если массив пуст
$last  = array_last($events);

// Старый способ
$last = $events === [] ? null : $events[array_key_last($events)];

Дополнительно появились: get_error_handler() и get_exception_handler() — теперь можно посмотреть текущий обработчик без хака через установку нового и возврат старого; стектрейс в фатальных ошибках (включая превышение max_execution_time) — невероятно полезно для разбора инцидентов; constexpr-замыкания в параметрах атрибутов; персистентные cURL share handles между запросами FPM (актуально для интеграций с одним внешним API).

Депрекации, на которые стоит обратить внимание

Перед миграцией прогоните проект статанализатором — несколько мелочей помечены deprecated и в 9.0 станут ошибкой:

  • `команда` (backtick) как алиас для shell_exec — депрекейтнут. Используйте shell_exec() явно или Symfony\Process.
  • Касты (boolean), (integer), (double), (binary) — теперь только (bool), (int), (float), (string).
  • null в качестве ключа массива и в array_key_exists() — пишите пустую строку явно.
  • mysqli_execute — заменить на mysqli_stmt_execute.
  • Завершение case точкой с запятой вместо двоеточия — да, такое тоже было.

Итоги: что брать сразу, что подождёт

Сразу в работу: clone($obj, [...]) для readonly-классов и DTO — выигрыш в читаемости огромный; array_first() / array_last() вместо array_key_* костылей; URI extension вместо parse_url в местах, где парсится пользовательский ввод. Pipe-оператор хорош, но требует привычки команды — внедряйте через ревью, не везде подряд.

Подождать стоит с тотальным переводом на #[\NoDiscard]: атрибут проявит себя только когда им покрыт каркас API, иначе будет шуметь предупреждениями в чужом коде. Сначала пометьте билдеры и валидаторы, посмотрите, как отзовётся CI.

Если приложение крутится на PHP 8.4 — обновление обычно проходит без правок кода: совместимость хорошая, ломающих изменений минимум. Главный фронт работ — пройтись по депрекациям и заменить точечно. А все новые модули писать уже на новом синтаксисе: pipe и clone with экономят строки и делают код спокойнее для чтения.