Laravel Magazine

PHP 8.4 Features Every Laravel Developer Should Be Using

Eric Van Johnson · Tips
PHP 8.4 Features Every Laravel Developer Should Be Using

PHP 8.4 landed in November 2024 and, if you have been running Laravel 11 or 12 on it, there are features sitting in the language right now that can meaningfully clean up your code. Some of these took a while to land because of RFC debates that stretched across multiple versions, but the results are worth the wait. Here are the ones that matter most for Laravel developers.

Property Hooks — Computed Properties Without Getters and Setters

Property hooks let you attach get and set logic directly to a class property. No more dedicated getter methods, no more __get() magic, no more docblocks trying to describe what the property actually does.

class User
{
    public string $fullName {
        get => "{$this->firstName} {$this->lastName}";
    }

    public string $email {
        set(string $value) {
            $this->email = strtolower(trim($value));
        }
    }
}

$user = new User();
$user->email = '  Alice@Example.COM  '; // Stored as alice@example.com
echo $user->fullName; // "Alice Smith"

The get hook runs when you read the property. The set hook runs when you write to it. You can define either or both. Computed properties only need a get hook; validation or transformation only needs a set hook.

For Laravel specifically, this is a clean way to handle value object properties and domain model invariants without cluttering your class with accessor methods. Your IDE and static analysis tools understand hooks natively, so they give accurate type information without needing docblocks.

Asymmetric Visibility — Public Read, Protected or Private Write

This one solves a very common Laravel pattern: a property that should be readable from anywhere but only writable from within the class or its children.

Before 8.4, you had two options: a public property (writable by anyone) or a private property with a public getter method (verbose and annoying). Now you can express the intent directly:

class Order
{
    public private(set) OrderStatus $status = OrderStatus::Pending;
    public protected(set) Carbon $createdAt;

    public function approve(): void
    {
        $this->status = OrderStatus::Approved; // Allowed from within
    }
}

$order = new Order();
echo $order->status->value;    // Public read — fine
$order->status = OrderStatus::Cancelled; // Fatal error — private set

The syntax is public private(set) or public protected(set), where the second modifier controls the write visibility. This pairs very naturally with readonly-style domain objects and value objects in Laravel applications where you want to enforce that state transitions go through methods rather than direct assignment.

new Without Parentheses on Chaining

A small quality-of-life improvement that removes a surprisingly common source of boilerplate: you no longer need to wrap new ClassName() in parentheses to chain methods on it.

// Before PHP 8.4
$result = (new QueryBuilder())->select('*')->from('users')->get();

// PHP 8.4
$result = new QueryBuilder()->select('*')->from('users')->get();

This shows up frequently in test factories, builder patterns, and anywhere you chain fluently on a new object. Less noise, same outcome.

array_find() and array_find_key() — Native Array Search

PHP finally has a native equivalent of Laravel's Collection::first() for plain arrays. array_find() returns the first element where a callback returns true:

$users = [
    ['id' => 1, 'role' => 'editor'],
    ['id' => 2, 'role' => 'admin'],
    ['id' => 3, 'role' => 'editor'],
];

$admin = array_find($users, fn ($u) => $u['role'] === 'admin');
// ['id' => 2, 'role' => 'admin']

array_find_key() returns the key of the first matching element rather than the element itself. Both return null if nothing matches.

If you are working with plain arrays (common when dealing with config data or JSON decoded from external APIs), this saves you from converting to a Collection just to call first().

Lazy Objects — Defer Instantiation Until First Access

Lazy objects let you create a proxy that only runs the constructor when you first access a property or method. This is primarily useful for dependency injection containers and proxies, but it has practical use cases in Laravel apps too.

$lazyUser = new ReflectionClass(User::class)
    ->newLazyGhost(function (User $user) {
        // Runs only when $user is first accessed
        $user->__construct(id: 42);
    });

// No constructor has run yet
$lazyUser->name; // Constructor runs here

Laravel's container already handles lazy binding, but lazy objects give you the primitive to build your own deferred initialization patterns in service classes and repositories without relying on nullable properties or factory methods as workarounds.

Upgrading Is Straightforward

Laravel 11 and 12 fully support PHP 8.4. If you are already on a recent Laravel version, you can update your PHP version and start using these features in new code immediately with no framework changes required.

Property hooks and asymmetric visibility are the two to reach for first. They directly address the most common boilerplate in Laravel model and domain object code, and the payoff in readability is immediate.

If you want to go deeper on any of these, stitcher.io's PHP 8.4 overview by Brent Roose is the most thorough reference available.

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.