PHP 8.4: Главные нововведения

PHP 8.4 — релиз «инструментов для взрослых»: он даёт новые способы выражать инварианты на границе свойств (Property Hooks, асимметричная видимость), продвигает внутренности фреймворков вперёд (Lazy Objects, новые Reflection-хелперы) и модернизирует проблемные зоны (WHATWG-совместимые классы Dom\*, корректный multipart-парсинг не только для POST). Параллельно ужесточаются крайние случаи (например, типовое поведение exit(), рекурсия при сравнениях) и намечается отказ от привычек, которые годами удивляли пользователей (implicit nullable params, lcg_value(), старые CSV-дефолты).

Содержание (Оглавление)


Property Hooks (логика get/set у свойства)

Свойства теперь могут определять хуки get и/или set. Это позволяет делать валидацию, нормализацию, вычисляемый доступ и правила хранения на уровне свойства — без россыпи методов и без утечки инвариантов по всему коду.

Типовые сценарии:

  • Нормализация при записи (например, приведение регистра)
  • Валидация при записи (ошибка как можно раньше)
  • Вычисляемые read-only свойства (виртуальные / без backing value)
final class Person
{
    public string $firstName {
        set => ucfirst(strtolower($value));
    }

    public string $lastName {
        set {
            if (strlen($value) < 2) {
                throw new InvalidArgumentException('Too short');
            }
            $this->lastName = $value;
        }
    }

    public string $fullName {
        get => $this->firstName . ' ' . $this->lastName;
    }
}

Если вы поддерживаете фреймворк или слой DTO, hooks часто заменяют горы boilerplate getters/setters и держат инварианты рядом с данными.

Асимметричная видимость (public get, private set)

Теперь можно задавать видимость set отдельно от get. Это практичный компромисс: состояние доступно для чтения, но запись ограничена (например, только внутри класса).

final class User
{
    public private(set) string $email;

    public function __construct(string $email)
    {
        $this->email = $email;
    }
}

Lazy Objects (ghost/proxy через Reflection)

В PHP 8.4 можно создавать объекты с отложенной инициализацией (до первого доступа). Это прежде всего для библиотек/фреймворков: контейнеры, ORM и генераторы прокси могут откладывать дорогую работу (IO, гидратацию, построение графа), но при этом возвращать корректно типизированный объект.

$initializer = static function ($obj) {
    // initialize $obj lazily
};

$r = new ReflectionClass(Example::class);
$lazy = $r->newLazyGhost($initializer);

В Reflection добавлены методы и константы для проверки/управления lazy-состоянием (например, принудительно инициализировать, пропускать инициализацию при сериализации).

#[\\Deprecated] — deprecations в userland с хорошими сообщениями

Теперь можно помечать функции/методы/константы класса как deprecated через #[\Deprecated]. Это похоже на deprecations в ядре, но эмитится E_USER_DEPRECATED и сообщение можно сделать более понятным.

Для авторов библиотек это сильное улучшение: deprecations становятся предсказуемыми и единообразными, без самописных warning-хелперов.

Новый DOM: Dom\\* (WHATWG) + улучшения XPath

PHP 8.4 добавляет новое пространство имён Dom с аналогами классических DOM-классов (например Dom\Node вместо DOMNode). Цель — совместимость с HTML5 и соответствие WHATWG, чтобы закрыть старые проблемы DOM-расширения.

Также есть практичные улучшения:

  • DOMNode::compareDocumentPosition() и связанные константы
  • DOMXPath::registerPhpFunctions() теперь принимает любой callable
  • DOMXPath::registerPhpFunctionNs() поддерживает синтаксис нативного вызова (вместо php:function('...'))

Если у вас есть пользовательские классы-наследники DOM, следите за новыми членами и совместимостью сигнатур (возможны compile errors при конфликте имён).

Multipart не только в POST: request_parse_body()

request_parse_body() позволяет разбирать RFC1867 (multipart) тела в не-POST HTTP-запросах. Это полезно для API, которые корректно используют PUT/PATCH с multipart (файлы + form data).

Практический набросок (обработка запроса на стороне сервера):

if ($_SERVER['REQUEST_METHOD'] === 'PATCH') {
    [$post, $files] = request_parse_body();
    // $post — поля формы, $files — загруженные файлы (RFC1867)
}

Новые функции, которые стоит взять в работу (array_find, fpow, mb_trim, …)

Хайлайты из списка новых функций:

  • Core: request_parse_body()
  • Standard: array_all(), array_any(), array_find(), array_find_key(), fpow(), http_get_last_response_headers(), http_clear_last_response_headers()
  • MBString: mb_trim(), mb_ltrim(), mb_rtrim(), mb_ucfirst(), mb_lcfirst()
  • Intl: grapheme_str_split() и helper’ы вокруг time zones / parsing
  • BCMath: bcround(), bcceil(), bcfloor(), bcdivmod()
  • Opcache: opcache_jit_blacklist()

Если вам нужны IEEE 754 semantics для крайних случаев возведения в степень, fpow() — рекомендуемая замена для deprecated поведения “(0) в отрицательную степень” у ** / pow().

Практические рецепты

Найти элемент без ручных циклов (array_find() / array_find_key())

$user = array_find($users, fn($u) => $u['id'] === $id);
if ($user === null) {
    throw new RuntimeException('User not found');
}

Проверить коллекцию (array_any() / array_all())

if (array_any($orders, fn($o) => $o['status'] === 'failed')) {
    // alert / retry / short-circuit
}

$allPaid = array_all($orders, fn($o) => $o['status'] === 'paid');

IEEE 754 semantics для степеней (fpow())

// Deprecated в 8.4: 0 ** -2  (семантика деления на ноль)
$x = fpow(0.0, -2.0); // IEEE 754 style result

Trim для multibyte строк (mb_trim())

$name = mb_trim($name);
$title = mb_ucfirst(mb_trim($title));

Финансы / явные режимы округления (RoundingMode + round())

В PHP 8.4 у round() параметр режима может быть RoundingMode (в дополнение к старым PHP_ROUND_*), что лучше читается в деньгах и отчётах, чем «магические» константы.

$lineTotal = round($amount * $qty, 2, RoundingMode::HalfAwayFromZero);

Перепроверьте тесты со снапшотами денежных сумм: реализация round() в 8.4 переписана, часть крайних случаев отличается от предыдущих версий.

Deprecations в userland (#[\Deprecated])

Библиотеки могут помечать API так же, как ядро PHP, с подсказкой замены и строкой since (не валидируется PHP — используйте semver или тег релиза).

#[\Deprecated(message: 'Use findById()', since: '2.0')]
function find(): array
{
    // ...
}

Обратно несовместимые изменения (миграционные заметки)

Ядро языка / runtime

  • exit() / die(): ведут себя более «как функция» (их можно передавать как callable, на них влияет strict_types, работает обычная coercion). Неверные типы теперь стабильно дают TypeError вместо приведения к строке.
  • Рекурсия при сравнениях: теперь Error вместо фатального E_ERROR.
  • Readonly + __clone(): косвенная модификация через reference внутри __clone() запрещена (например $ref = &$this->readonly).
  • Типы констант: PHP_DEBUG и PHP_ZTS теперь bool (раньше int).
  • Временные имена файлов: имена upload/tempnam стали длиннее (+13 байт).
  • E_STRICT удалён: уровень ошибок убран; константа E_STRICT теперь deprecated.

Миграция resource → object (высокий риск для legacy)

Ряд расширений перевели свои “handles” с resources на objects. Проверки через is_resource() нужно заменить на проверку false/null согласно документации.

  • DBA: теперь Dba\Connection вместо dba_connection resource.
  • ODBC: теперь Odbc\Connection / Odbc\Result objects.
  • SOAP: SoapClient::$httpurl теперь Soap\Url (null если отсутствует); SoapClient::$sdl теперь Soap\Sdl (null если отсутствует).

Новые warnings и exceptions

Во многих местах неправильные аргументы теперь приводят к ValueError / TypeError вместо предупреждений или “как-нибудь отработало” (например, диапазоны quality/speed в GD, валидация round() mode, ограничения str_getcsv() по 1 байту, null bytes в XMLReader/Writer, проверки параметров XSL и т.д.). Это улучшает корректность, но может ломать код, который продолжал работу после warning.

Избранное по расширениям

  • DOM: DOMImplementation::getFeature() удалён; клонирование DOMXPath теперь кидает Error (раньше получался неработоспособный объект).
  • GMP: класс GMP теперь final (нельзя наследоваться).

Deprecated (исправить до появления ошибок)

Core

  • Implicitly nullable parameters: function f(string $s = null) deprecated, т.к. неявно расширяет тип. Используйте ?string $s = null (или перестройте сигнатуру, если дальше идут обязательные параметры).
  • 0 ** -n / pow(0, -n): deprecated (семантика деления на ноль). Для IEEE 754 используйте fpow().
  • Класс с именем _: deprecated.
  • trigger_error(..., E_USER_ERROR): deprecated — лучше exception или exit().

Стандартная библиотека / расширения

  • DatePeriod ISO-string constructor: deprecated; используйте DatePeriod::createFromISO8601String().
  • DOM: константа DOM_PHP_ERR deprecated; ряд старых DOM-свойств формально deprecated.
  • Random: lcg_value() deprecated; используйте Random\Randomizer::getFloat().
  • Reflection: ReflectionMethod::__construct() с одним аргументом deprecated; используйте ReflectionMethod::createFromMethodName().
  • MySQLi: mysqli_ping(), mysqli_kill(), mysqli_refresh() и связанные refresh-константы deprecated (reconnect убрали; вместо этого SQL-команды).
  • CSV defaults: default escape у fputcsv() / fgetcsv() / str_getcsv() deprecated — передавайте явно.
  • XML: xml_set_object() deprecated; передача non-callable strings в xml_set_* deprecated.

Sessions (ops/security hygiene)

Изменение session.sid_length и session.sid_bits_per_character deprecated; перестаньте переопределять и убедитесь, что storage поддерживает 32-символьные hex session IDs. Несколько session.* INI опций для trans-sid deprecated; константа SID deprecated.

Прочие изменения и эксплуатация (Fibers/GC, builtin server, bcrypt cost)

Что заметно в эксплуатации:

  • Fibers & destructors: переключение fiber во время выполнения destructor теперь разрешено; destructors, вызванные GC, могут исполняться в отдельном fiber (gc_destructor_fiber). Если вы смешиваете Fibers и тяжёлые destructors — обязательно тестируйте планировщик.
  • Builtin server: поиск index-файла теперь поднимается по родительским каталогам даже если путь “выглядит как файл” (точка в последнем компоненте).
  • Apache: поддержка EOL Apache 2.0/2.2 удалена (минимум 2.4).
  • password_hash(): default bcrypt cost поднят с 10 до 12 — больше CPU на хеширование, если вы полагались на дефолты.
  • Rounding: round() принимает RoundingMode|int, добавлены новые режимы и исправлены edge-case баги — перепроверьте финансовую математику снапшотами.

Итог

Если вы пишете фреймворки, главный профит 8.4 — Property Hooks + асимметричная видимость + Lazy Objects: инварианты можно выразить ровно там, где данные меняются, а тяжёлую инициализацию отложить до реального доступа. Для прикладных команд миграция чаще всего упирается в более строгие ошибки (ValueError/TypeError), resource→object переходы и чистку deprecations (implicit nullable params, lcg_value(), CSV defaults) до того, как они станут жёсткими ошибками.