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)
- Asymmetric property visibility (
public get,private set) - Lazy Objects (ghosts/proxies via Reflection)
#[\\Deprecated]â userland deprecations with good messages- New DOM:
Dom\\*(WHATWG-compliant) + XPath improvements - HTTP multipart beyond POST:
request_parse_body() - New functions worth adopting (array_find, fpow, mb_trim, âŚ)
- Backward incompatible changes (migration notes)
- Deprecations (fix before they become errors)
- Other changes & ops notes (Fibers/GC, builtin server, bcrypt cost)
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 constantsDOMXPath::registerPhpFunctions()accepts any callableDOMXPath::registerPhpFunctionNs()supports native-call syntax (instead ofphp: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 bystrict_types, standard coercions). Invalid types consistently throwTypeErrorinstead of being string-cast.- Recursion during comparisons: now throws
Errorinstead of fatalE_ERROR. - Readonly +
__clone(): indirect modification via references inside__clone()is no longer allowed (e.g.$ref = &$this->readonly). - Constant types:
PHP_DEBUGandPHP_ZTSare nowbool(previouslyint). - Temporary filenames: upload/tempnam filenames are longer (+13 bytes).
E_STRICTremoved: the error level is gone;E_STRICTconstant 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\Connectioninstead ofdba_connectionresource. - ODBC: now uses
Odbc\Connection/Odbc\Resultobjects. - SOAP:
SoapClient::$httpurlis nowSoap\Url(null when absent);SoapClient::$sdlis nowSoap\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; cloningDOMXPathnow throwsError(previously created an unusable clone). - GMP:
GMPclass is nowfinal(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). Usefpow()for IEEE 754 behavior.- Class named
_: deprecated. trigger_error(..., E_USER_ERROR): deprecatedâthrow an exception orexit()instead.
Standard library / extensions
- DatePeriod ISO-string constructor: deprecated; use
DatePeriod::createFromISO8601String(). - DOM:
DOM_PHP_ERRconstant deprecated; several old DOM properties formally deprecated. - Random:
lcg_value()deprecated; useRandom\Randomizer::getFloat(). - Reflection:
ReflectionMethod::__construct()with one argument deprecated; useReflectionMethod::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 toxml_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()acceptsRoundingMode|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.