PHP 8.5 -- Pipe Operator, Clone With и URI Extension
PHP 8.5 (ноябрь 2025) -- мажорный релиз, добавляющий pipe operator для функциональных цепочек, функцию clone() с модификацией readonly-свойств, полноценное расширение для работы с URI, атрибут #[\NoDiscard] и множество других улучшений.
Pipe Operator (|>)
Pipe operator передает результат левого выражения как единственный аргумент функции справа. Это позволяет писать цепочки преобразований слева направо вместо вложенных вызовов.
Базовый синтаксис
<?php
declare(strict_types=1);
// Before (PHP 8.4) — nested calls, read inside-out
$result = strtolower(trim(str_replace(' ', '-', $input)));
// After (PHP 8.5) — pipe operator, read left to right
$result = $input
|> trim(...)
|> str_replace(' ', '-', ...)
|> strtolower(...);
Использование с замыканиями
Когда функция принимает больше одного аргумента, pipe operator передает значение как единственный аргумент. Для многоаргументных функций используйте замыкания:
<?php
declare(strict_types=1);
$title = ' Hello World. PHP 8.5 is Here! ';
// Pipe with closures for multi-argument functions
$slug = $title
|> trim(...)
|> (fn(string $s): string => str_replace(' ', '-', $s))
|> (fn(string $s): string => str_replace('.', '', $s))
|> strtolower(...);
echo $slug; // "hello-world-php-85-is-here!"
First-class callables в pipe
<?php
declare(strict_types=1);
// Using first-class callables
$words = " hello world "
|> trim(...)
|> strtoupper(...)
|> str_split(...);
// $words = ['H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D']
// Pipe with named functions
function double(int $n): int { return $n * 2; }
function increment(int $n): int { return $n + 1; }
$result = 5 |> double(...) |> increment(...); // 11
Практический пример: обработка данных
<?php
declare(strict_types=1);
// Data transformation pipeline
final readonly class UserTransformer
{
/**
* @param array<string, mixed> $raw
* @return array<string, mixed>
*/
public static function transform(array $raw): array
{
return $raw
|> self::normalizeEmail(...)
|> self::sanitizeName(...)
|> self::addTimestamps(...);
}
private static function normalizeEmail(array $data): array
{
$data['email'] = strtolower(trim($data['email'] ?? ''));
return $data;
}
private static function sanitizeName(array $data): array
{
$data['name'] = htmlspecialchars(trim($data['name'] ?? ''), ENT_QUOTES);
return $data;
}
private static function addTimestamps(array $data): array
{
$data['created_at'] = new \DateTimeImmutable();
return $data;
}
}
Сравнение: до и после
<?php
declare(strict_types=1);
// PHP 8.4 — вложенные вызовы (читаем изнутри наружу)
$encoded = base64_encode(json_encode(array_filter(
array_map(fn($x) => $x * 2, $numbers),
fn($x) => $x > 10
)));
// PHP 8.5 — pipe operator (читаем слева направо)
$encoded = $numbers
|> (fn(array $arr): array => array_map(fn($x) => $x * 2, $arr))
|> (fn(array $arr): array => array_filter($arr, fn($x) => $x > 10))
|> json_encode(...)
|> base64_encode(...);
Clone With -- модификация readonly при клонировании
PHP 8.5 добавляет функцию clone(), которая принимает массив свойств для изменения при клонировании. Это решает главную проблему readonly-классов -- невозможность создать with-er методы без рефлексии.
Базовый синтаксис
<?php
declare(strict_types=1);
readonly class Money
{
public function __construct(
public int $amount,
public string $currency,
) {}
public function withAmount(int $amount): self
{
// clone() modifies readonly properties during cloning
return clone($this, ['amount' => $amount]);
}
public function withCurrency(string $currency): self
{
return clone($this, ['currency' => $currency]);
}
}
$price = new Money(1000, 'USD');
$discounted = $price->withAmount(800); // Money(800, 'USD')
$converted = $price->withCurrency('EUR'); // Money(1000, 'EUR')
Сравнение с PHP 8.4
<?php
declare(strict_types=1);
readonly class Color
{
public function __construct(
public int $red,
public int $green,
public int $blue,
public int $alpha = 255,
) {}
// PHP 8.4 — приходилось пересоздавать объект целиком
public function withAlphaOld(int $alpha): self
{
return new self($this->red, $this->green, $this->blue, $alpha);
}
// PHP 8.5 — clone() с указанием изменяемых свойств
public function withAlpha(int $alpha): self
{
return clone($this, ['alpha' => $alpha]);
}
}
$red = new Color(255, 0, 0);
$semiTransparent = $red->withAlpha(128); // Color(255, 0, 0, 128)
Практический пример: immutable DTO
<?php
declare(strict_types=1);
readonly class OrderDto
{
public function __construct(
public string $id,
public string $status,
public float $total,
public \DateTimeImmutable $createdAt,
public ?\DateTimeImmutable $paidAt = null,
) {}
public function markAsPaid(): self
{
return clone($this, [
'status' => 'paid',
'paidAt' => new \DateTimeImmutable(),
]);
}
public function cancel(): self
{
return clone($this, ['status' => 'cancelled']);
}
}
$order = new OrderDto('ord-1', 'pending', 99.99, new \DateTimeImmutable());
$paid = $order->markAsPaid(); // New object with status=paid, paidAt=now
echo $order->status; // "pending" — original unchanged
echo $paid->status; // "paid"
URI Extension
PHP 8.5 включает новое расширение ext-uri с RFC 3986 и WhatWG URL парсерами. Это полноценная замена parse_url() с валидацией, нормализацией и типобезопасностью.
Uri\Rfc3986\Uri -- строгий RFC-парсер
<?php
declare(strict_types=1);
use Uri\Rfc3986\Uri;
// Create from string
$uri = Uri::parse('https://user:[email protected]:8080/path?query=1#frag');
echo $uri->getScheme(); // "https"
echo $uri->getHost(); // "example.com"
echo $uri->getPort(); // 8080
echo $uri->getPath(); // "/path"
echo $uri->getQuery(); // "query=1"
echo $uri->getFragment(); // "frag"
echo $uri->getUserInfo(); // "user:pass"
echo $uri->getAuthority(); // "user:[email protected]:8080"
// Normalization is automatic
$uri = Uri::parse('HTTP://EXAMPLE.COM/./foo/../bar');
echo $uri->toString(); // "http://example.com/bar"
Uri\WhatWg\Url -- браузерный парсер
<?php
declare(strict_types=1);
use Uri\WhatWg\Url;
// WhatWG URL Standard — как в браузерах
$url = Url::parse('https://example.com/search?q=php 8.5');
echo $url->getHost(); // "example.com"
echo $url->getPathname(); // "/search"
echo $url->getSearch(); // "?q=php%208.5"
// With base URL resolution
$resolved = Url::parse('/api/users', 'https://example.com');
echo $resolved->toString(); // "https://example.com/api/users"
Сравнение с parse_url()
<?php
declare(strict_types=1);
use Uri\Rfc3986\Uri;
$url = 'https://example.com:443/path?key=value#section';
// parse_url() — returns associative array, no validation
$parts = parse_url($url);
// ['scheme' => 'https', 'host' => 'example.com', 'port' => 443, ...]
// URI extension — type-safe object with validation and normalization
$uri = Uri::parse($url);
$uri->getScheme(); // string "https"
$uri->getPort(); // int 443
$uri->getHost(); // string "example.com"
// parse_url() fails silently on malformed URLs
parse_url('http:///invalid'); // Returns partial data
// URI extension throws or returns null for invalid URIs
$invalid = Uri::parse('http:///invalid'); // null or throws InvalidArgumentException
Атрибут #[\NoDiscard]
Атрибут #[\NoDiscard] генерирует предупреждение, если возвращаемое значение функции или метода игнорируется. Полезно для иммутабельных объектов и валидаторов.
Базовый синтаксис
<?php
declare(strict_types=1);
#[\NoDiscard]
function validateEmail(string $email): bool
{
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
// Warning: the return value of validateEmail() is ignored
validateEmail('[email protected]');
// Correct usage
$isValid = validateEmail('[email protected]');
if ($isValid) {
// ...
}
С пользовательским сообщением
<?php
declare(strict_types=1);
readonly class Collection
{
/** @param list<mixed> $items */
public function __construct(
private array $items = [],
) {}
#[\NoDiscard('Collection is immutable; calling filter() without using the result has no effect')]
public function filter(callable $predicate): self
{
return new self(array_filter($this->items, $predicate));
}
#[\NoDiscard('Collection is immutable; calling map() without using the result has no effect')]
public function map(callable $transform): self
{
return new self(array_map($transform, $this->items));
}
}
$col = new Collection([1, 2, 3, 4, 5]);
// Warning: Collection is immutable; calling filter() without using the result has no effect
$col->filter(fn(int $x): bool => $x > 3);
// Correct
$filtered = $col->filter(fn(int $x): bool => $x > 3);
Подавление с помощью (void)
<?php
declare(strict_types=1);
#[\NoDiscard]
function importantResult(): int
{
return 42;
}
// Suppress the warning intentionally with (void) cast
(void) importantResult();
Closures в константных выражениях
PHP 8.5 разрешает статические замыкания и first-class callables в контекстах, которые раньше допускали только константные выражения: атрибуты, значения параметров по умолчанию, константы.
Static closures в атрибутах
<?php
declare(strict_types=1);
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final readonly class Validate
{
public function __construct(
public \Closure $validator,
public string $message = 'Validation failed',
) {}
}
final class UserDto
{
#[Validate(
validator: static fn(string $v): bool => strlen($v) >= 3,
message: 'Name must be at least 3 characters',
)]
public string $name;
#[Validate(
validator: static fn(string $v): bool => filter_var($v, FILTER_VALIDATE_EMAIL) !== false,
message: 'Invalid email address',
)]
public string $email;
}
First-class callables в значениях по умолчанию
<?php
declare(strict_types=1);
// Callable as default parameter value
function process(
string $data,
\Closure $transform = strtolower(...),
): string {
return $transform($data);
}
echo process('HELLO'); // "hello" — uses default
echo process('hello', strtoupper(...)); // "HELLO" — overridden
Ограничения
<?php
declare(strict_types=1);
// Only STATIC closures are allowed (no $this binding)
// static fn() => ... ✅
// fn() => ... ❌ (non-static closure not allowed)
// No use() capturing
// static fn() => $x ❌ (cannot capture variables)
// No Closure::bind / Closure::bindTo on const-expr closures
array_first() и array_last()
Новые функции для получения первого и последнего элемента массива без изменения внутреннего указателя и без создания копий.
<?php
declare(strict_types=1);
$fruits = ['apple', 'banana', 'cherry'];
// New in PHP 8.5
$first = array_first($fruits); // "apple"
$last = array_last($fruits); // "cherry"
// Works with associative arrays
$config = ['host' => 'localhost', 'port' => 5432, 'db' => 'myapp'];
$first = array_first($config); // "localhost"
$last = array_last($config); // "myapp"
// Empty array returns null
$empty = [];
$first = array_first($empty); // null
$last = array_last($empty); // null
// Comparison with PHP 8.4 approaches
$arr = [10, 20, 30, 40];
// PHP 8.4 — awkward alternatives
$first = reset($arr); // Moves internal pointer
$first = $arr[array_key_first($arr)]; // Verbose
$last = end($arr); // Moves internal pointer
$last = $arr[array_key_last($arr)]; // Verbose
// PHP 8.5 — clean and simple
$first = array_first($arr); // 10
$last = array_last($arr); // 40
Persistent cURL Share Handles
PHP 8.5 добавляет curl_share_init_persistent() для переиспользования DNS-кеша и SSL-сессий между запросами.
<?php
declare(strict_types=1);
// Persistent share handle survives across requests (FPM/CLI long-running)
$share = curl_share_init_persistent('my-connection-pool');
// Share DNS cache and SSL sessions
curl_share_setopt($share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
curl_share_setopt($share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_SHARE, $share);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
// Share handle persists — next request reuses DNS/SSL cache
// No need to call curl_share_close() for persistent handles
Backtraces при Fatal Error
PHP 8.5 автоматически генерирует backtrace при фатальных ошибках, что значительно упрощает отладку.
<?php
declare(strict_types=1);
// PHP 8.4 — fatal error shows only the error line
// Fatal error: Allowed memory size exhausted in /app/process.php on line 42
// PHP 8.5 — fatal error includes a full backtrace
// Fatal error: Allowed memory size exhausted in /app/process.php on line 42
// Stack trace:
// #0 /app/process.php(42): processLargeFile()
// #1 /app/handler.php(15): DataProcessor->run()
// #2 /app/index.php(8): Handler->handle()
get_error_handler() и get_exception_handler()
Новые функции для получения текущих обработчиков ошибок и исключений.
<?php
declare(strict_types=1);
// Set custom handlers
set_error_handler(function (int $errno, string $errstr): bool {
// Handle error
return true;
});
set_exception_handler(function (\Throwable $e): void {
// Handle exception
});
// PHP 8.5: inspect current handlers
$errorHandler = get_error_handler(); // Returns the callable or null
$exceptionHandler = get_exception_handler(); // Returns the callable or null
// Useful for middleware, testing, and temporary handler replacement
$previous = get_error_handler();
set_error_handler(function () use ($previous) {
// Custom logic
if ($previous !== null) {
return $previous(...func_get_args());
}
return false;
});
Асимметричная видимость для статических свойств
PHP 8.4 ввёл асимметричную видимость только для свойств экземпляра. PHP 8.5 расширяет это на статические свойства.
<?php
declare(strict_types=1);
final class Registry
{
// Readable everywhere, writable only inside this class
public private(set) static array $services = [];
public static function register(string $name, object $service): void
{
self::$services[$name] = $service; // OK — within the class
}
}
Registry::register('logger', new Logger());
$services = Registry::$services; // OK — public read
// Registry::$services = []; // Error: Cannot modify private(set) static property
Final свойства через constructor promotion
<?php
declare(strict_types=1);
class BaseEntity
{
public function __construct(
// Property cannot be overridden in child classes
public final string $id,
public string $name,
) {}
}
class User extends BaseEntity
{
public function __construct(
string $id,
public string $name, // OK — not final
public string $email,
// Cannot redeclare $id — it is final in parent
) {
parent::__construct($id, $name);
}
}
#[\Override] на свойствах
<?php
declare(strict_types=1);
abstract class BaseConfig
{
public string $dsn = 'sqlite::memory:';
}
class AppConfig extends BaseConfig
{
#[\Override]
public string $dsn = 'pgsql:host=localhost;dbname=app';
}
// If parent property is renamed or removed — compile error
#[\Deprecated] на трейтах и константах
<?php
declare(strict_types=1);
#[\Deprecated('Use LoggerTrait instead')]
trait OldLogTrait
{
public function log(string $message): void
{
echo $message;
}
}
class Service
{
use OldLogTrait; // Deprecation warning
}
class Config
{
#[\Deprecated('Use MAX_RETRIES instead')]
public const int MAX_RETRY = 3;
public const int MAX_RETRIES = 3;
}
echo Config::MAX_RETRY; // Deprecation warning
Closure::getCurrent()
<?php
declare(strict_types=1);
// Get reference to the current closure (for recursion without variable binding)
$factorial = function (int $n): int {
if ($n <= 1) return 1;
$self = Closure::getCurrent();
return $n * $self($n - 1);
};
echo $factorial(5); // 120
Другие новые функции
grapheme_levenshtein()
<?php
declare(strict_types=1);
// Unicode-aware Levenshtein distance (works with grapheme clusters)
$distance = grapheme_levenshtein('cafe', 'cafe'); // 0
$distance = grapheme_levenshtein('resume', 'resume'); // Correct for accented chars
IntlListFormatter
<?php
declare(strict_types=1);
// Locale-aware list formatting
$formatter = new \IntlListFormatter('ru', \IntlListFormatter::TYPE_AND);
echo $formatter->format(['PHP', 'Go', 'Python']);
// "PHP, Go и Python"
$formatter = new \IntlListFormatter('en', \IntlListFormatter::TYPE_OR);
echo $formatter->format(['red', 'green', 'blue']);
// "red, green, or blue"
max_memory_limit (INI)
<?php
declare(strict_types=1);
// New INI directive: absolute ceiling that memory_limit cannot exceed
// Set in php.ini: max_memory_limit = 256M
// ini_set('memory_limit', '512M'); // Warning: exceeds max_memory_limit
// Useful for shared hosting and multi-tenant environments
FILTER_THROW_ON_FAILURE
<?php
declare(strict_types=1);
// Throw exception instead of returning false on filter failure
$email = filter_var(
'invalid-email',
FILTER_VALIDATE_EMAIL,
FILTER_THROW_ON_FAILURE,
);
// Throws ValueError: filter_var(): failed to validate value
Dom\Element улучшения
<?php
declare(strict_types=1);
use Dom\HTMLDocument;
$doc = HTMLDocument::createFromString('<div class="a b c"><p class="x">Text</p></div>');
// getElementsByClassName() — like in browsers
$elements = $doc->getElementsByClassName('a');
// insertAdjacentHTML() — insert HTML relative to element
$div = $doc->querySelector('div');
$div->insertAdjacentHTML('beforeend', '<span>New content</span>');
Устаревшее (Deprecated) в PHP 8.5
Неканонические приведения типов
<?php
declare(strict_types=1);
// Deprecated — non-canonical cast names
$x = (boolean) $value; // Use (bool) instead
$x = (integer) $value; // Use (int) instead
$x = (double) $value; // Use (float) instead
// Still valid
$x = (bool) $value;
$x = (int) $value;
$x = (float) $value;
$x = (string) $value;
$x = (array) $value;
$x = (object) $value;
Backticks как shell_exec()
<?php
// Deprecated — backtick operator
$output = `ls -la`; // Deprecated alias for shell_exec()
// Use explicitly
$output = shell_exec('ls -la');
Переобъявление констант
<?php
// Deprecated — redefining constants
define('MY_CONST', 'value1');
define('MY_CONST', 'value2'); // Deprecated in PHP 8.5
Функции-заглушки (no-ops)
<?php
// These functions are now no-ops and deprecated:
curl_close($ch); // No-op since PHP 8.0, deprecated in 8.5
curl_share_close($share); // No-op, deprecated
xml_parser_free($parser); // No-op, deprecated
// socket_set_timeout() — use stream_set_timeout()
// All MHASH_* constants — deprecated, use HASH_* instead
Удалено в PHP 8.5
<?php
// CLI option -z / --zend-extension removed
// Was used to load Zend extensions from CLI:
// php -z extension.so script.php ❌ Removed
// Use php.ini or -d zend_extension=... instead
Сводная таблица: PHP 8.5
| Возможность | Синтаксис / Описание |
|---|---|
| Pipe operator | $value |> trim(...) |> strtolower(...) |
| Clone with | clone($this, ['prop' => $newValue]) |
| URI extension | Uri\Rfc3986\Uri::parse($url) |
#[\NoDiscard] |
Предупреждение при игнорировании возвращаемого значения |
| Closures in const expr | static fn() => ... в атрибутах и значениях по умолчанию |
array_first() |
array_first([1, 2, 3]) => 1 |
array_last() |
array_last([1, 2, 3]) => 3 |
| Persistent cURL share | curl_share_init_persistent('pool') |
| Fatal error backtrace | Автоматический stack trace при fatal |
get_error_handler() |
Получить текущий обработчик ошибок |
get_exception_handler() |
Получить текущий обработчик исключений |
| Asymmetric visibility (static) | public private(set) static string $x |
| Final properties | public final string $id в constructor promotion |
#[\Override] on properties |
Проверка переопределения свойств |
#[\Deprecated] on traits |
Пометить трейт как устаревший |
Closure::getCurrent() |
Ссылка на текущее замыкание (рекурсия) |
grapheme_levenshtein() |
Unicode-aware расстояние Левенштейна |
IntlListFormatter |
Локализованное форматирование списков |
max_memory_limit INI |
Верхний предел для memory_limit |
FILTER_THROW_ON_FAILURE |
Исключение вместо false при фильтрации |
Dom\Element additions |
getElementsByClassName(), insertAdjacentHTML() |
Типичные вопросы на экзамене
- Что делает pipe operator
|>? Передает результат левого выражения как единственный аргумент функции справа. - Можно ли в pipe использовать функции с несколькими аргументами? Напрямую нет -- нужно обернуть в замыкание.
- Как
clone()работает с readonly? Указанные в массиве свойства перезаписываются при клонировании, включая readonly. - Чем
Uri\Rfc3986\Uriотличается отUri\WhatWg\Url? RFC 3986 -- строгий стандарт URI, WhatWG -- браузерный стандарт URL с более мягким парсингом. - Что произойдет, если игнорировать результат функции с
#[\NoDiscard]? Компилятор выдаст предупреждение (warning). - Как подавить предупреждение
#[\NoDiscard]? Использовать приведение(void). - Что вернет
array_first([])для пустого массива?null. - Можно ли использовать не-статические замыкания в константных выражениях? Нет, только
static fn().