PHP 8.4: Major Features

PHP 8.4 is a “power tools” release: it gives you new ways to express invariants at the property boundary (Property Hooks, asymmetric visibility), pushes framework internals forward (Lazy Objects, new Reflection helpers), and modernizes long-problematic areas (WHATWG-aligned Dom\* classes, better multipart parsing beyond POST). At the same time, it tightens edge cases (e.g. exit() type behavior, recursion during comparisons) and starts deprecating patterns that kept surprising users (implicitly-nullable params, lcg_value(), old CSV defaults).

Table of Contents


Property Hooks (get/set logic on properties)

Properties can now define get and/or set hooks. This lets you put validation, normalization, computed accessors, and storage rules at the property without creating separate methods or leaking invariants across the codebase.

Common patterns:

  • Normalize on write (e.g. title-casing names)
  • Validate on write (throw early, at the boundary)
  • Computed read-only properties (virtual / no 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;
    }
}

If you maintain a framework or DTO layer, hooks can replace a lot of boilerplate getters/setters while still keeping invariants close to the data.

Asymmetric property visibility (public get, private set)

Properties may now control set visibility separately from get. This is a pragmatic middle ground between fully-public state and strict encapsulation: you can make state observable but only writable from inside the class (or a narrower scope).

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

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

Lazy Objects (ghosts/proxies via Reflection)

PHP 8.4 adds support for creating objects whose initialization is deferred until first access. This is aimed at libraries/frameworks: containers, ORMs, and proxy generators can delay expensive work (IO, hydration, graph building) while still returning a correctly-typed object.

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

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

Reflection also gained methods and constants to inspect and control lazy state (e.g. initialize explicitly, skip initialization on serialize).

#[\\Deprecated] — userland deprecations with good messages

You can now mark userland functions/methods/class constants as deprecated via #[\Deprecated]. It behaves like core deprecations, but emits E_USER_DEPRECATED and can provide clearer messaging.

This is a big win for library authors: you can ship deprecations with predictable semantics and readable output, without inventing your own warning infrastructure.

New DOM: Dom\\* (WHATWG-compliant) + XPath improvements

PHP 8.4 introduces a new Dom namespace with counterparts to classic DOM classes (e.g. Dom\Node vs DOMNode). The goal is better HTML5 compatibility and WHATWG spec alignment, solving long-standing DOM extension issues.

There are also practical DOM improvements:

  • DOMNode::compareDocumentPosition() plus its constants
  • DOMXPath::registerPhpFunctions() accepts any callable
  • DOMXPath::registerPhpFunctionNs() supports native-call syntax (instead of php:function('...'))

If your project subclasses DOM classes, pay attention to new members and signature constraints (these can turn into compile errors when names collide).

HTTP multipart beyond POST: request_parse_body()

request_parse_body() allows parsing RFC1867 (multipart) bodies in non-POST HTTP requests. That matters for APIs that legitimately use PUT/PATCH with multipart payloads (file uploads, mixed form data).

Practical sketch (server-side request handling):

if ($_SERVER['REQUEST_METHOD'] === 'PATCH') {
    [$post, $files] = request_parse_body();
    // $post is form fields, $files is uploaded files (RFC1867)
}

New functions worth adopting (array_find, fpow, mb_trim, …)

Highlights from the new function list:

  • 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() and helpers around time zones / parsing
  • BCMath: bcround(), bcceil(), bcfloor(), bcdivmod()
  • Opcache: opcache_jit_blacklist()

If you want IEEE 754 semantics for exponentiation edge cases, fpow() is the recommended replacement for the deprecated “(0) to a negative power” behavior in ** / pow().

Practical recipes

Find an item without manual loops (array_find() / array_find_key())

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

Validate collections (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 power semantics (fpow())

// Deprecated in 8.4: 0 ** -2  (division-by-zero semantics)
$x = fpow(0.0, -2.0); // IEEE 754 style result

Multibyte-safe trimming (mb_trim())

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

Financial / explicit rounding modes (RoundingMode + round())

PHP 8.4 lets round() accept a RoundingMode enum (in addition to legacy PHP_ROUND_* integers), which reads better in money and reporting code than magic constants.

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

Re-test suites that snapshot monetary output: the round() implementation was reworked in 8.4 and some edge cases differ from earlier PHP versions.

Userland deprecations (#[\Deprecated])

Library code can emit the same style of deprecation as core PHP, with an optional replacement hint and “since” string (not validated—use your semver or release tag).

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

Backward incompatible changes (migration notes)

Core language/runtime

  • exit() / die() behavior: now more function-like (callable passing, affected by strict_types, standard coercions). Invalid types consistently throw TypeError instead of being string-cast.
  • Recursion during comparisons: now throws Error instead of fatal E_ERROR.
  • Readonly + __clone(): indirect modification via references inside __clone() is no longer allowed (e.g. $ref = &$this->readonly).
  • Constant types: PHP_DEBUG and PHP_ZTS are now bool (previously int).
  • Temporary filenames: upload/tempnam filenames are longer (+13 bytes).
  • E_STRICT removed: the error level is gone; E_STRICT constant is deprecated.

Resource → object migrations (high impact on legacy checks)

Several extensions moved from returning resources to returning objects. Replace is_resource() checks with documented false/null checks.

  • DBA: now uses Dba\Connection instead of dba_connection resource.
  • ODBC: now uses Odbc\Connection / Odbc\Result objects.
  • SOAP: SoapClient::$httpurl is now Soap\Url (null when absent); SoapClient::$sdl is now Soap\Sdl (null when absent).

New warnings and exceptions

Many functions now throw ValueError / TypeError for invalid arguments instead of warnings or inconsistent behavior (examples include GD quality/speed ranges, round() mode validation, str_getcsv() one-byte constraints, XMLReader/Writer null-byte handling, XSL parameter validation, etc.). This mostly helps correctness—but it can break code that relied on warnings and “continue anyway”.

Extension-specific highlights

  • DOM: DOMImplementation::getFeature() removed; cloning DOMXPath now throws Error (previously created an unusable clone).
  • GMP: GMP class is now final (cannot be extended).

Deprecations (fix before they become errors)

Core

  • Implicitly nullable parameters: function f(string $s = null) is deprecated because it widens the type implicitly. Use ?string $s = null (or restructure if followed by required params).
  • 0 ** -n / pow(0, -n): deprecated (division-by-zero semantics). Use fpow() for IEEE 754 behavior.
  • Class named _: deprecated.
  • trigger_error(..., E_USER_ERROR): deprecated—throw an exception or exit() instead.

Standard library / extensions

  • DatePeriod ISO-string constructor: deprecated; use DatePeriod::createFromISO8601String().
  • DOM: DOM_PHP_ERR constant deprecated; several old DOM properties formally deprecated.
  • Random: lcg_value() deprecated; use Random\Randomizer::getFloat().
  • Reflection: ReflectionMethod::__construct() with one argument deprecated; use ReflectionMethod::createFromMethodName().
  • MySQLi: mysqli_ping(), mysqli_kill(), mysqli_refresh() and related refresh constants deprecated (reconnect removed; use SQL commands instead).
  • CSV defaults: default escape parameter for fputcsv() / fgetcsv() / str_getcsv() deprecated—pass it explicitly.
  • XML: xml_set_object() deprecated; passing non-callable strings to xml_set_* is deprecated.

Sessions (ops/security hygiene)

Changing session.sid_length and session.sid_bits_per_character is deprecated; stop overriding and ensure your storage backend supports 32-char hex IDs. Several session.* INI toggles related to trans-sid are deprecated; SID constant is deprecated.

Other changes & ops notes (Fibers/GC, builtin server, bcrypt cost)

Operationally relevant items:

  • Fibers & destructors: fiber switching during destructor execution is allowed; GC-triggered destructors may run in a separate fiber (gc_destructor_fiber). If you mix Fibers with resource-heavy destructors, test for surprising scheduling.
  • Builtin server: index-file lookup now traverses parent directories even when the path “looks like a file” (dot in last component).
  • Apache: support for EOL Apache 2.0/2.2 removed (minimum 2.4).
  • password_hash(): default bcrypt cost increased from 10 to 12—expect more CPU per hash if you rely on defaults.
  • Rounding: round() accepts RoundingMode|int, adds new modes, and fixes rounding edge cases—re-test financial math with snapshots.

Closing thoughts

If you build frameworks, PHP 8.4’s biggest payoff is Property Hooks + asymmetric visibility + Lazy Objects: you can express invariants at the exact boundary where data changes, while deferring heavy initialization until it’s truly needed. For application teams, the migration work is mostly about stricter error behavior (more ValueError/TypeError), resource→object transitions, and cleaning up deprecations (implicitly-nullable params, lcg_value(), CSV defaults) before they become hard errors in future versions.