HardТеория11 min

PHP 8.5

Pipe operator, clone with, URI extension, #[NoDiscard], array_first/array_last, closures in const expressions

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()

Типичные вопросы на экзамене

  1. Что делает pipe operator |>? Передает результат левого выражения как единственный аргумент функции справа.
  2. Можно ли в pipe использовать функции с несколькими аргументами? Напрямую нет -- нужно обернуть в замыкание.
  3. Как clone() работает с readonly? Указанные в массиве свойства перезаписываются при клонировании, включая readonly.
  4. Чем Uri\Rfc3986\Uri отличается от Uri\WhatWg\Url? RFC 3986 -- строгий стандарт URI, WhatWG -- браузерный стандарт URL с более мягким парсингом.
  5. Что произойдет, если игнорировать результат функции с #[\NoDiscard]? Компилятор выдаст предупреждение (warning).
  6. Как подавить предупреждение #[\NoDiscard]? Использовать приведение (void).
  7. Что вернет array_first([]) для пустого массива? null.
  8. Можно ли использовать не-статические замыкания в константных выражениях? Нет, только static fn().

Проверь себя

5 из 13

Можно ли в PHP 8.5 использовать асимметричную видимость для статических свойств?

Что вернет array_first([]) в PHP 8.5?

Что делает INI-директива max_memory_limit в PHP 8.5?

Что делает curl_share_init_persistent() в PHP 8.5?

Какой будет результат: 5 |> double(...) |> increment(...), если double умножает на 2, а increment прибавляет 1?