Originally published at hafiz.dev
If you followed the Laravel 13 upgrade path, you're running PHP 8.4 by now (or you should be, since Laravel 13.3+ pulls in Symfony 8 components that require it). The upgrade guide covers the migration steps, but upgrading your runtime and actually using the new language features are two different things.
Most Laravel developers upgrade PHP, confirm their tests pass, and keep writing the same PHP 8.1-style code they've always written. That works, but you're leaving real improvements on the table. PHP 8.4 shipped six features that directly clean up the kind of code you write in a Laravel app every day.
Here's what each one does, with before and after examples.
1. Property Hooks: Replace Your Getters and Setters
Property hooks let you define get and set behavior directly on a class property. No more writing separate getter and setter methods for simple transformations.
Before (PHP 8.3):
class PriceCalculator
{
private float $priceInCents;
public function getPriceInCents(): float
{
return $this->priceInCents;
}
public function setPriceInCents(float $value): void
{
if ($value < 0) {
throw new \InvalidArgumentException('Price cannot be negative.');
}
$this->priceInCents = $value;
}
public function getPriceInDollars(): float
{
return $this->priceInCents / 100;
}
}
After (PHP 8.4):
class PriceCalculator
{
public float $priceInCents {
set {
if ($value < 0) {
throw new \InvalidArgumentException('Price cannot be negative.');
}
$this->priceInCents = $value;
}
}
public float $priceInDollars {
get => $this->priceInCents / 100;
}
}
The $priceInDollars property is virtual. It doesn't store anything. It computes the value on read from $priceInCents. And the set hook on $priceInCents validates the input without a separate method.
Where this shines in Laravel: service classes, value objects, and DTOs where you'd normally write getters with transformation logic. Note that Eloquent models have their own accessor/mutator system via Attribute::make(), so property hooks don't replace those directly. But for any non-Eloquent class in your app (and you should have plenty), property hooks remove a lot of boilerplate.
2. Asymmetric Visibility: Public Read, Private Write
Before PHP 8.4, if you wanted a property that anyone could read but only the class itself could modify, you had two options: make it private and add a getter, or make it readonly. Both had tradeoffs. readonly can only be set once, which doesn't work if the value changes over the object's lifetime.
Asymmetric visibility solves this cleanly:
Before (PHP 8.3):
class OrderStatus
{
private string $status = 'pending';
public function getStatus(): string
{
return $this->status;
}
public function markAsShipped(): void
{
$this->status = 'shipped';
}
}
// Usage
$order->getStatus(); // 'pending'
After (PHP 8.4):
class OrderStatus
{
public private(set) string $status = 'pending';
public function markAsShipped(): void
{
$this->status = 'shipped';
}
}
// Usage
$order->status; // 'pending' - direct access, no getter needed
$order->status = 'cancelled'; // Error: Cannot modify private(set) property
The public private(set) declaration means: anyone can read $status directly, but only the class itself can change it. No getter needed. No readonly restriction. The value can change internally through methods like markAsShipped(), but external code can't tamper with it.
This is ideal for data transfer objects in your Laravel app. API response DTOs (especially if you're following REST API best practices), configuration objects, form data objects. Anywhere you want external code to read properties directly without letting them modify the state.
You can also use public protected(set) to allow child classes to modify the property while keeping external write access restricted.
3. array_find(): Stop Filtering When You Only Need One
PHP has had array_filter() forever, but if you only need the first element that matches a condition, you've been writing this:
Before (PHP 8.3):
$users = [
['name' => 'Alice', 'role' => 'admin'],
['name' => 'Bob', 'role' => 'editor'],
['name' => 'Charlie', 'role' => 'admin'],
];
$firstAdmin = array_values(array_filter(
$users,
fn ($user) => $user['role'] === 'admin'
))[0] ?? null;
That's ugly. array_filter processes the entire array, array_values re-indexes it, and the [0] ?? null handles the empty case. Three operations for something that should be one line.
After (PHP 8.4):
$firstAdmin = array_find($users, fn ($user) => $user['role'] === 'admin');
array_find() returns the first matching element and stops iterating. No re-indexing, no null coalescing. If nothing matches, it returns null.
PHP 8.4 also adds three related functions:
-
array_find_key()returns the key of the first match instead of the value -
array_any()returnstrueif at least one element matches (likeCollection::contains()for arrays) -
array_all()returnstrueif every element matches (likeCollection::every()for arrays)
In Laravel, you'll mostly use these in places where you're working with raw arrays instead of collections: config processing, middleware logic, job payloads, or anywhere performance matters and you don't want to create a Collection instance just to call ->first().
4. The #[\Deprecated] Attribute
PHP has always had a way to deprecate built-in functions, but there was no native mechanism for marking your own functions, methods, or class constants as deprecated. You'd either put a @deprecated docblock comment (which only IDE-level tools read) or throw a manual trigger_error().
Before (PHP 8.3):
/**
* @deprecated Use calculateTotal() instead
*/
function calculateSubtotal(array $items): float
{
trigger_error('calculateSubtotal() is deprecated, use calculateTotal()', E_USER_DEPRECATED);
return calculateTotal($items);
}
After (PHP 8.4):
#[\Deprecated(message: 'Use calculateTotal() instead', since: '2.0')]
function calculateSubtotal(array $items): float
{
return calculateTotal($items);
}
The #[\Deprecated] attribute triggers a real E_USER_DEPRECATED notice when the function is called. IDEs like PhpStorm show it with a strikethrough. Static analysis tools like PHPStan and Larastan flag it automatically. And you get a since parameter to track when the deprecation started.
Where this helps in Laravel: if you maintain internal packages, APIs with versioned endpoints, or shared service classes across teams, this is a cleaner way to signal "stop using this" than a docblock that nobody reads.
5. Method Chaining on new Without Parentheses
A small quality-of-life improvement that removes an annoying syntax limitation:
Before (PHP 8.3):
$date = (new DateTime())->format('Y-m-d');
$response = (new JsonResponse($data))->setStatusCode(201);
Those extra parentheses around new ClassName() were required to chain a method call. They look awkward and trip up developers who forget them.
After (PHP 8.4):
$date = new DateTime()->format('Y-m-d');
$response = new JsonResponse($data)->setStatusCode(201);
No wrapping parentheses needed. You can also access properties directly: new Foo()->bar. This is a small change, but it cleans up code in places where you create and immediately use throwaway objects, which happens often in tests, seeders, and one-off scripts.
6. Multibyte String Functions: trim, ltrim, rtrim
PHP finally has multibyte-aware trim functions. If your app handles content in languages like Japanese, Chinese, Arabic, or Korean, you've probably been using workarounds with preg_replace to trim multibyte whitespace characters that trim() ignores.
Before (PHP 8.3):
// Standard trim doesn't handle multibyte whitespace like \u{3000} (ideographic space)
$cleaned = preg_replace('/^\s+|\s+$/u', '', $input);
After (PHP 8.4):
$cleaned = mb_trim($input);
PHP 8.4 adds mb_trim(), mb_ltrim(), and mb_rtrim(). If your Laravel app processes user input from a multilingual audience, these are a direct improvement. Use them in your form request prepareForValidation() methods or in custom Eloquent casts where you clean input before storage.
When to Start Using These
You don't need to refactor your entire codebase. The practical approach:
Use immediately in new code. When you write a new service class, DTO, or value object, use property hooks and asymmetric visibility instead of getters/setters. When you write a new array operation on raw data, reach for array_find() before array_filter().
Refactor gradually. When you touch an existing class for a feature or bug fix, modernize it if it takes less than five minutes. Don't create refactoring PRs that touch 50 files. That's risk for no product value.
Don't touch Eloquent models. Eloquent has its own accessor/mutator system that doesn't need property hooks. And asymmetric visibility conflicts with how Eloquent hydrates properties. Keep Eloquent models using Laravel's patterns.
FAQ
Do property hooks work with Eloquent models?
Not in the way you might expect. Eloquent uses dynamic property access via __get and __set, which doesn't interact cleanly with PHP property hooks. Stick with Eloquent's Attribute::make() for model accessors and mutators. Use property hooks in your service classes, DTOs, form data objects, and other non-Eloquent classes.
Can I use asymmetric visibility with constructor promotion?
Yes. public private(set) string $name works in constructor parameters:
public function __construct(
public private(set) string $name,
public protected(set) int $age,
) {}
This gives you a publicly readable, internally writable promoted property in one line.
Do I need to upgrade PHPStan or Larastan for PHP 8.4 features?
PHPStan 2.1+ fully supports property hooks, asymmetric visibility, and the #[\Deprecated] attribute. If you're running an older version, upgrade before adopting these features, otherwise your CI pipeline will flag false positives. Larastan follows PHPStan's version, so updating Larastan pulls in the PHP 8.4 support automatically. If you're also upgrading your testing setup to Pest 4, do both at the same time.
What's the minimum PHP 8.4 version I should run?
PHP 8.4.1 or later. The 8.4.0 release had a few edge-case bugs with property hooks in certain inheritance scenarios that were fixed in 8.4.1. If you're deploying to production, start with the latest 8.4.x patch.
What's Next
PHP 8.4 isn't a small release. Property hooks and asymmetric visibility change how you structure classes in a fundamental way. The new array functions remove patterns you've been copy-pasting for years. And the #[\Deprecated] attribute gives you a tool that PHP itself has had forever but never shared with userland code.
If you haven't upgraded yet, the 4 composer conflicts post walks you through the blockers you'll hit on the way to PHP 8.4 and Laravel 13. And if you're building something with Laravel and want help modernizing your codebase for PHP 8.4, get in touch.
Top comments (0)