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 экономят строки и делают код спокойнее для чтения.