PHP 8.4 in Practice: Property Hooks and Asymmetric Visibility โ
PHP 8.4 came out on 21 November 2024, and it's had plenty of time to bed in. The hype was mostly about property hooks, and they've earned it. The feature I actually reach for more often is asymmetric visibility. And a couple of the small additions have quietly removed a lot of boilerplate.
This isn't a full changelog (the official release page does that well). It's what has actually changed the way I write backend code.
๐ช Property Hooks: Getters and Setters, Built In โ
Property hooks let you attach get and set logic straight to a property. Callers still write $customer->email. They don't need getEmail() or setEmail(), and you get no docblock that drifts away from the code.
final class Customer
{
public function __construct(
public string $email {
set => strtolower(trim($value));
},
) {}
public string $domain {
get => substr($this->email, strpos($this->email, '@') + 1);
}
}
$customer = new Customer(' Jimmy@Example.COM ');
echo $customer->email; // jimmy@example.com
echo $customer->domain; // example.comThe short set => expr; form assigns the result of the expression to the property. $domain is a virtual property: none of its hooks touch its own value, so PHP never stores one, and writing to it is an error.
Interfaces can also declare properties now. That is a bigger deal than it sounds:
interface HasSlug
{
public string $slug { get; }
}A word of caution, though. Hooks can't be combined with readonly. And it's easy to hide expensive work behind what looks like a plain property read. My rule is that a hook normalises or derives a value. If it touches the database, it should be a method.
๐ Asymmetric Visibility: The Feature I Use Most โ
For years, the answer to "public to read, private to write" has been a private property plus a getter. Now it's one line:
final class Order
{
public function __construct(
public private(set) string $status = 'pending',
) {}
public function markPaid(): void
{
$this->status = 'paid';
}
}
$order = new Order();
echo $order->status; // pending
$order->status = 'paid'; // Error: cannot modify private(set) propertySome details are worth knowing:
- It only works on typed properties.
private(set)makes the property implicitlyfinal. Useprotected(set)if subclasses need to write to it.- You can drop the
public:private(set) string $statusmeans the same thing. readonlynow impliesprotected(set)rather thanprivate(set).
I've found this a better fit than readonly for entities and aggregates. Their state really does change over time, but only through methods that enforce the rules.
โ๏ธ new Without Parentheses โ
A small one, but I hit it every day. You no longer need to wrap a new expression in brackets to call something on it:
// Before 8.4
$date = (new DateTimeImmutable())->format('Y-m-d');
// PHP 8.4
$date = new DateTimeImmutable()->format('Y-m-d');Builders, fluent query objects and test fixtures all read better for it.
๐ array_find, array_find_key, array_any, array_all โ
These four functions cover things every codebase has written badly at least once. Each takes the array and then a callback, and the callback receives both the value and the key:
$users = [
['email' => 'a@example.com', 'active' => false],
['email' => 'b@example.com', 'active' => true],
];
$firstActive = array_find($users, fn (array $u): bool => $u['active']);
$index = array_find_key($users, fn (array $u): bool => $u['active']); // 1
$anyActive = array_any($users, fn (array $u): bool => $u['active']); // true
$allActive = array_all($users, fn (array $u): bool => $u['active']); // falseThey stop at the first decisive match, which array_filter() followed by reset() never did. array_find() returns null when nothing matches, so it pairs nicely with ??.
โ ๏ธ Deprecations to Fix First โ
The deprecation most codebases hit first is implicitly nullable parameter types:
// Deprecated in 8.4
function send(Message $message, Options $options = null) {}
// Make it explicit
function send(Message $message, ?Options $options = null) {}It's tedious rather than hard, and Rector's PHP 8.4 rules handle most of it. Vendor code is where it gets noisy, so update your dependencies before you start chasing warnings.
Other changes worth checking:
E_STRICTis deprecated, and so is passingE_USER_ERRORtotrigger_error(). Throw an exception instead._as a class name is deprecated, as is raising zero to a negative power.lcg_value()andmysqli_ping()/mysqli_kill()/mysqli_refresh()are deprecated.- Changing
session.sid_lengthandsession.sid_bits_per_characteris deprecated. - The IMAP, OCI8, PDO_OCI and pspell extensions have moved out of core to PECL. If you still use
imap_*, check this before you bump your base image.
The 8.4 deprecations page has the full list.
๐งฐ Honourable Mentions โ
#[\Deprecated]lets your own functions, methods and class constants raise real deprecation notices.- Lazy objects are mostly aimed at framework and ORM authors, but you benefit indirectly.
Dom\HTMLDocumentfinally gives PHP a spec-compliant HTML5 parser withquerySelector().BcMath\Numberlets you use normal operators with arbitrary-precision maths, which is handy for anything involving money.
๐ฏ Conclusion โ
PHP 8.4 isn't a revolutionary release, but it removes a lot of the ceremony that made PHP classes verbose. Asymmetric visibility has replaced most of my getters. Property hooks handle the rest when used with restraint. And the new array functions mean less hand-rolled looping.
If you're still on 8.2 or 8.3, the upgrade is mostly about clearing deprecations, and it's worth doing now. PHP 8.5 is due in November, and you'll want a clean deprecation log before then.