Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

PHP 8.4 Features You'll Actually Use (and One Deprecation That Will Find You)

About Post

Every PHP release comes with a changelog long enough to scroll for a while. Most of it is important to someone. Very little of it changes the code you write on a normal Tuesday.

PHP 8.4, released in November 2024, is different. A few of its features genuinely change how you design everyday classes, and they've had a few months to settle in now. Here are the ones I think you'll actually use, plus one deprecation that's going to find you whether you go looking for it or not.

1. Property hooks: goodbye, boilerplate getters and setters

For years, PHP gave you two choices for a class field. Make it public and accept that anyone can put anything in it, or make it private and write a getEmail() and setEmail() pair just to add one line of logic.

Property hooks let you attach that logic directly to the field:

class Tenant
{
    public string $email {
        set => strtolower(trim($value));
    }

    public int $rentCents {
        set(int $value) {
            if ($value < 0) {
                throw new InvalidArgumentException('Rent cannot be negative.');
            }
            $this->rentCents = $value;
        }
    }
}

$tenant = new Tenant();
$tenant->email = '  [email protected] ';
echo $tenant->email; // [email protected]

The short form set => expression; stores whatever the expression returns. The long form gives you a full block. Callers just read and write $tenant->email, with no method calls, and you can add a hook later without changing a single caller. That last point is the real win: you can start with a plain public field and add rules only when you need them.

You can also create virtual fields that are computed on read and have no stored value at all:

class Person
{
    public function __construct(
        public string $first,
        public string $last,
    ) {}

    public string $fullName {
        get => "{$this->first} {$this->last}";
    }
}

And interfaces can now declare that a field must exist and be readable (public string $fullName { get; }), which was impossible before.

A few rules to know: hooks can't be combined with readonly, a set hook's parameter type must be compatible with the field's type, and a virtual field with only a get hook can't be written to.

2. Asymmetric visibility: public to read, private to write

This is the one I expect to use most. How often have you written a private field plus a public getter, purely so outsiders can read a value but not change it?

class Contract
{
    public private(set) string $status = 'draft';

    public function sign(): void
    {
        // business rules here...
        $this->status = 'signed';
    }
}

$contract = new Contract();
echo $contract->status;       // fine: 'draft'
$contract->status = 'signed'; // Error: cannot modify private(set) field from outside

Anyone can read $contract->status, but only the class itself can change it, so the only way to sign a contract is through sign(), where your rules live. There's also protected(set) if child classes need to write it, and it works with constructor promotion too: public function __construct(public private(set) string $id) {}.

How is that different from readonly? A readonly field can be set exactly once and never again, even by the class itself. Asymmetric visibility allows the class to change it as often as it needs, which is what you want for state like a status.

3. array_find, array_any, array_all

Four small functions that remove a surprising amount of loop code:

$firstOverdue = array_find($invoices, fn (Invoice $i) => $i->isOverdue());
$overdueKey   = array_find_key($invoices, fn (Invoice $i) => $i->isOverdue());

$hasDraft = array_any($contracts, fn (Contract $c) => $c->status === 'draft');
$allPaid  = array_all($invoices, fn (Invoice $i) => $i->isPaid());

Before 8.4 you'd write a foreach with an early return, or abuse array_filter() and take the first element (which processes the whole array). These stop at the first match. The callback receives the value and the key, in that order.

The gotcha: array_all() on an empty array returns true, and array_any() returns false. That's mathematically correct, and it will still surprise you the day "all invoices are paid" is true for a tenant who has no invoices at all. If an empty list should mean something else, check for it first.

If you're in Laravel, Collections have had first(), contains() and every() for years. These are the plain-array equivalents, useful in packages, small scripts and anywhere you don't want to wrap an array in a collection.

4. new without the extra brackets

A small one, but you'll feel it every day:

// Before 8.4
$nextMonth = (new DateTimeImmutable('2025-03-30'))->modify('+1 month');

// PHP 8.4
$nextMonth = new DateTimeImmutable('2025-03-30')->modify('+1 month');

You can now call a method, read a field or access a constant directly on a new instance. The brackets after the class name are still required: new Foo()->bar() works, new Foo->bar() doesn't.

5. #[\Deprecated] for your own code

PHP's own functions could always emit deprecation notices. Now yours can too:

class RentCalculator
{
    #[\Deprecated(message: 'use LateFee::forRent() instead', since: '2.3')]
    public function lateFee(int $rentCents, int $daysLate): int
    {
        return (new LateFee())->forRent($rentCents, $daysLate);
    }
}

Calling it triggers a real E_USER_DEPRECATED notice with your message, so it shows up in logs, test output and static analysis. It works on functions, methods and class constants. It's a much clearer migration path than a @deprecated docblock that only IDEs notice.

Where to start: pick one class with a getter-and-setter pair that only exists to protect a single field. Replace it with public private(set) or a set hook. It's the quickest way to feel why this release matters.

The deprecation that will find you

PHP 8.4 deprecates implicitly nullable parameter types. This very common old pattern now triggers a deprecation notice:

function notify(User $user = null) { /* ... */ }   // deprecated in 8.4

function notify(?User $user = null) { /* ... */ }  // explicit, correct

It still works for now, but your logs and test output will fill up with notices, especially from older packages. Static analysis tools and automated refactoring tools like Rector can fix these across a codebase quickly. Do it before you upgrade production, not after.

Can you use all this today?

In your own application, yes, as long as every environment runs PHP 8.4: local, CI and production. Laravel 12 supports it. Check your Docker images, your server packages and your CI config before using the new syntax, because property hooks on an 8.3 server are a parse error, not a warning.

If you maintain a package, be more careful. Laravel 12 itself still supports PHP 8.2 and up, so a package that uses 8.4 syntax forces every user onto 8.4.

The official PHP 8.4 release page has the full list, including lazy objects, the new HTML5-capable DOM API and multibyte mb_trim().

Which PHP 8.4 feature has made it into your real code so far? I'm betting on asymmetric visibility, but property hooks might surprise me.

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close