What's New in PHP 8.4?

February 06, 2025
Written by
Reviewed by

What's New in PHP 8.4?

PHP 8.4 was released a bit over 2 months ago, back on November 21, 2024. So, I thought it well past time to talk about the new release here on the Twilio blog, focusing on some of the key new functionalities. This won't be a deep dive, as there's too much to cover in one post.

Specifically, I'm going to cover the following changes:

  • Property Hooks (the change in 8.4)
  • Virtual Properties
  • Default Property Values
  • New class invocation without parenthesis
  • Asymmetric Visibility
  • Lazy Objects
  • The #[\Deprecated] attribute
  • Parsing Non-POST HTTP requests
  • New array functions

Property Hooks

If you've been writing object-oriented code in PHP for any length of time, the following will be very familiar to you.

<?php

declare(strict_types=1);

final class PhoneNumber
{
    private string $areaCode;
    private string|null $countryCode;
    private string $localPhoneNumber;

    public function __construct(
        string $areaCode, 
        string $localPhoneNumber, 
        string|null $countryCode = null
    ) {
        $this->areaCode = $areaCode;
        $this->countryCode = $countryCode;
        $this->localPhoneNumber = $localPhoneNumber;
    }
}

It's a small class that does an extremely simplistic job of modelling a phone number, storing the country code, area code, and local phone number in three separate class properties ($areaCode, $countryCode, and $localPhoneNumber respectively).

However, unless you're using PHP's Reflection API, it will be of little value to you, as each of the three properties have private visibility. So, while you can set the initial values, you can't change them after the object's been instantiated nor can you retrieve the property values.

Given that, prior to PHP 8.4, you'd have a few choices to make those properties accessible, the top two being getters and setters and the __get() and __set() magic methods.

Before we go any further, however, let's establish some business logic for the class, loosely based around the rules for Australian phone numbers:

  • The area code must be two digits in length. The first number must be zero (0) and the second number can be one of 2, 3, 4, 7, or 8.
  • The country code does not need to be set, but if it is, it can be from one to four digits in length.
  • The local phone number must be eight digits in length with the allowed values being 0 - 9.
  • When printed:
    • If the country code is provided, it must have a preceding plus symbol and the area code must not have a preceding zero.
    • If the country code is not provided, the area code must have a preceding zero.
    • The area code and local phone number must be printed together, in groups of three digits, separated by a single space.

Here's an example of a phone number with and without the country code:

+61 498 867 191
0498 867 191

Using getters and setters

Here's an example of how you could refactor the class using getter and setter functions:

<?php

declare(strict_types=1);

namespace Settermjd\Scratch;

use function chunk_split;
use function preg_match;
use function sprintf;
use function substr;
use function trim;

final class PhoneNumber
{
    public const int MATCHES_REGEX = 1;
    public const int NUMBER_SPACING = 3;
    public const string COUNTRY_CODE_PREFIX = "+";
    public const string REGEX_AREA_CODE = "/0[23478]/";
    public const string REGEX_COUNTRY_CODE = "/[1-9][0-9]{0,3}/";
    public const string REGEX_LOCAL_NUMBER = "/[0-9]{8}/";
   
    private string $areaCode;
    private string|null $countryCode;
    private string $localPhoneNumber;

    public function __construct(string $areaCode, string $localPhoneNumber, string|null $countryCode)
    {
        $this->setAreaCode($areaCode);
        $this->setCountryCode($countryCode);
        $this->setLocalPhoneNumber($localPhoneNumber);
    }

    public function getAreaCode(): string
    {
        return $this->countryCode === null
            ? $this->areaCode
            : substr($this->areaCode, 1);
    }

    public function setAreaCode(string $areaCode): void
    {
        $this->areaCode = $this->matchesRegex(self::REGEX_AREA_CODE, $areaCode)
            ? $areaCode
            : null;
    }

    public function getCountryCode(): string|null
    {
        return $this->countryCode !== null
            ? self::COUNTRY_CODE_PREFIX . $this->countryCode
            : null;
    }

    public function setCountryCode(string|null $countryCode): void
    {
        $this->countryCode = $this->matchesRegex(self::REGEX_COUNTRY_CODE, $countryCode)
            ? $countryCode
            : null;
    }

    public function getLocalPhoneNumber(): string
    {
        return $this->localPhoneNumber ?? "";
    }

    public function setLocalPhoneNumber(string $localPhoneNumber): void
    {
        $this->localPhoneNumber = $this->matchesRegex(self::REGEX_LOCAL_NUMBER, $localPhoneNumber)
            ? $localPhoneNumber
            : null;
    }

    public function getPhoneNumber(): string
    {
        return $this->countryCode === null
            ? sprintf("%s %s",
                substr($this->getAreaCode() . $this->getLocalPhoneNumber(), 0, 4),
                trim(chunk_split(substr($this->getAreaCode() . $this->getLocalPhoneNumber(), 4), self::NUMBER_SPACING, " "))
            )
            : sprintf("%s %s",
                $this->getCountryCode(),
                trim(chunk_split($this->getAreaCode() . $this->getLocalPhoneNumber(), self::NUMBER_SPACING, " "))
            );
    }

    private function matchesRegex(string $regex, string|null $value): bool
    {
        return $value !== null && preg_match($regex, $value) === self::MATCHES_REGEX;
    }
}

The class defines a series of constants to make the class' functionality more readable by avoiding using magic variables. The last three (REGEX_AREA_CODE, REGEX_COUNTRY_CODE, and REGEX_LOCAL_NUMBER) are important, as they simplify the code required to ensure that the area code, country code, and local phone number are valid.

If you're not familiar with regular expressions (regexes), check out this excellent guide at Regular-Expressions.info.

Following that, the class properties are defined, as before, followed by the class' constructor, which calls the setter functions, which we'll define shortly, to initialise the respective properties

After that, a getter and setter are defined for each of the three member variables. Using the regular expression constants, these functions implement the first three points of business logic that we established. The final two functions, getPhoneNumber() and matchesRegex(), implement the final four points of business logic regarding displaying the phone number.

Using magic methods

And here's an example refactor of the class using __get() and __set() magic methods.

<?php

declare(strict_types=1);

namespace Settermjd\Scratch;

use function chunk_split;
use function preg_match;
use function sprintf;
use function substr;
use function trim;

final class PhoneNumber
{
    public const int MATCHES_REGEX = 1;
    public const int NUMBER_SPACING = 3;
    public const string COUNTRY_CODE_PREFIX = "+";
    public const string REGEX_AREA_CODE = "/0[23478]/";
    public const string REGEX_COUNTRY_CODE = "/[1-9][0-9]{0,3}/";
    public const string REGEX_LOCAL_NUMBER = "/[0-9]{8}/";

    private string $areaCode;
    private string|null $countryCode;
    private string $localPhoneNumber;

    public function __construct(
        string $areaCode, 
        string $localPhoneNumber, 
        string|null $countryCode
    ) {
        $this->__set("areaCode", $areaCode);
        $this->__set("countryCode", $countryCode);
        $this->__set("localPhoneNumber", $localPhoneNumber);
    }

    public function __get(string $name): string|null
    {
        $value = "";
        switch ($name) {
            case "areaCode":
                $value = $this->countryCode === null
                    ? $this->areaCode
                    : substr($this->areaCode, 1);
                break;
            case "countryCode":
                $value = $this->countryCode !== null
                    ? self::COUNTRY_CODE_PREFIX . $this->countryCode
                    : null;
                break;
            case "localPhoneNumber":
                $value = $this->localPhoneNumber ?? "";
                break;
            case "phoneNumber":
                $value = empty($this->countryCode)
                    ? sprintf("%s %s",
                        substr($this->__get("areaCode") . $this->__get("localPhoneNumber"), 0, 4),
                        trim(
                            chunk_split(
                                substr(
                                    $this->__get("areaCode") . $this->__get("localPhoneNumber"),
                                    4
                                ),
                                self::NUMBER_SPACING,
                                " "
                            )
                        )
                    )
                    : sprintf("%s %s",
                        $this->__get("countryCode"),
                        trim(
                            chunk_split(
                                $this->__get("areaCode") . $this->__get("localPhoneNumber"),
                                self::NUMBER_SPACING,
                                " "
                            )
                        )
                    );
        }
        return $value;
    }

    public function __set(string $name, string|null $value): void
    {
        if (property_exists($this, $name)) {
            switch ($name) {
                case "areaCode":
                    $this->areaCode = $this->matchesRegex(
                        self::REGEX_AREA_CODE, 
                        $value
                    )
                        ? $value
                        : null;
                    break;
                case "countryCode":
                    $this->countryCode = $this->matchesRegex(
                        self::REGEX_COUNTRY_CODE, 
                        $value
                    )
                        ? $value
                        : null;
                    break;
                case "localPhoneNumber":
                    $this->localPhoneNumber = $this->matchesRegex(
                        self::REGEX_LOCAL_NUMBER, 
                        $value
                    )
                        ? $value
                        : null;
                    break;
            }
            $this->{$name} = $value;
        }
    }

    private function matchesRegex(string $regex, string|null $value): bool
    {
        return $value !== null 
                && preg_match($regex, $value) === self::MATCHES_REGEX;
    }
}

This example is shorter, which might make it preferable. But, it's a blunter approach, because the magic methods will be called for requests to read or write class properties if it is not defined or not visible from the calling scope.

What's more, this applies to the initial three properties which we've defined to any that may be added in the future that fit the criteria just outlined. So, boilerplate code needs to be added to avoid this, as well as to determine how to address the referenced property. Otherwise, the class is, essentially, the same.

What are Property Hooks?

Enter Property Hooks! If you've written code in Kotlin, C#, Swift, JavaScript, Python, and Ruby, Property Hooks will be (more or less) familiar to you. In these languages, they're generally referred to as Property Accessors.

In PHP, paraphrasing the RFC, they allow us to add additional logic to the get and set operations of object properties. They also introduce additional features, which we'll come to shortly.

There are two hooks for each property, get which defines the behaviour for reading an object property, and set which defines the behaviour for setting it. You can define the hooks on an as-needed basis, otherwise falling back on the default functionality.

They can be defined in a number of ways, which you can see examples of below, which I borrowed from Matthew Weier O'Phinney's Property Hooks deep dive.

// Full block form
set ($value) { $this->propertyName = $value }

// Block form with implicit $value
set { $this->propertyName = $value }

// Expression form with explicit $value
set ($value) => { expression }

// Expression form with implicit $value
set => { expression }

This variety of forms allows for a lot of flexibility given what you need or want to achieve. So far, my preference is for a combination of the full block form at the top of the list, and the expression form with an explicit $value in the set hook. I find that they allow for the greatest amount of control. However, the others, like PHP's Arrow Functions, are great when you only have small expressions.

One of their best effects is that they avoid the need (or the feeling that you need) to define getters and setters to control access to one or more object properties, just because you might need to do so now or in the future. Feeling an obligation to do this might be justified, but may also result in unnecessary changes to the class' API.

How do you use Property Hooks?

Let's step through a refactored example, below, of the PhoneNumber class to see them in action.

<?php

declare(strict_types=1);

namespace Settermjd\Scratch;

final class PhoneNumber
{
    public const int MATCHES_REGEX = 1;
    public const int NUMBER_SPACING = 3;
    public const string COUNTRY_CODE_PREFIX = "+";
    public const string REGEX_AREA_CODE = "/0[23478]/";
    public const string REGEX_COUNTRY_CODE = "/[1-9][0-9]{0,3}/";
    public const string REGEX_LOCAL_NUMBER = "/[0-9]{8}/";

    private string|null $countryCode {
        set (string|null $countryCode) {
            $this->countryCode = $this->matchesRegex(
                self::REGEX_COUNTRY_CODE, 
                $countryCode
            )
                ? $countryCode
                : null;
        }
        get {
            return $this->countryCode !== null
                ? self::COUNTRY_CODE_PREFIX . $this->countryCode
                : null;
        }
    }

    private string $areaCode {
        set (string $areaCode) {
            $this->areaCode = $this->matchesRegex(
                self::REGEX_AREA_CODE, 
                $areaCode
            )
                ? $areaCode
                : null;
        }
        get {
            return $this->countryCode === null
                ? $this->areaCode
                : substr($this->areaCode, 1);
        }
    }

    private string $localPhoneNumber {
        set (string $localPhoneNumber) {
            $this->localPhoneNumber = $this->matchesRegex(
                self::REGEX_LOCAL_NUMBER, 
                $localPhoneNumber
            )
                ? $localPhoneNumber
                : null;
        }
        get => $this->localPhoneNumber ?? "";
    }

    public string $phoneNumber {
        get {
            return $this->countryCode === null
                ? sprintf("%s %s",
                    substr($this->areaCode . $this->localPhoneNumber, 0, 4),
                    trim(
                        chunk_split(
                            substr(
                                $this->areaCode . $this->localPhoneNumber, 
                                4
                            ), 
                            self::NUMBER_SPACING, 
                            " "
                        ))
                )
                : sprintf("%s %s",
                    $this->countryCode,
                    trim(
                        chunk_split(
                            $this->areaCode . $this->localPhoneNumber, 
                            self::NUMBER_SPACING, 
                            " "
                        ))
                );
        }
    }

    private function matchesRegex(string $regex, string|null $value): bool
    {
        return $value !== null && preg_match($regex, $value) === self::MATCHES_REGEX;
    }

    public function __construct(
        string $areaCode, 
        string $localPhoneNumber, 
        string|null $countryCode = null
    ) {
        $this->areaCode = $areaCode;
        $this->countryCode = $countryCode;
        $this->localPhoneNumber = $localPhoneNumber;
    }
}

All of the logic that was in the getters and setters or __get and __set has been refactored into the get and set hooks of the respective object properties. I won't go through all of them, as that would be more than a bit redundant. Rather, I'll just step through one, so that it's clear what's going on.

Take a look at the $countryCode property below.

private string|null $countryCode {
    set (string|null $countryCode) {
        $this->countryCode = $this->matchesRegex(
            self::REGEX_COUNTRY_CODE, 
            $countryCode
        )
            ? $countryCode
            : null;
    }
    get {
        return $this->countryCode !== null
            ? self::COUNTRY_CODE_PREFIX . $this->countryCode
            : null;
    }
}

The set hook names the parameter it is passed as $countryCode. I've done this to make using the variable as intuitive as possible. You don't need to do this, though. PHP being PHP, it offers significant flexibility in almost all that it does. Property Hooks are no different.

If we'd not set a name, as in the example below, the hook would have been passed the value implicitly with the generic name of $value. However, a lot of clarity (in my opinion) would be lost.

private string|null $countryCode {
    set {
        $this->countryCode = $this->matchesRegex(
            self::REGEX_COUNTRY_CODE, 
            $value
        )
            ? $value
            : null;
    }
    get {
        return $this->countryCode !== null
            ? self::COUNTRY_CODE_PREFIX . $this->countryCode
            : null;
    }
}
Note that, when explicitly setting the parameter's name, you must set the parameter's type to match the property's type.

The logic in the hooks are as before. The other point worth drawing attention to is the constructor, specifically, how the properties are set. Without Property Hooks, when using getters we had to call the setter methods to set the properties. When using magic methods, the constructor had to call the applicable __set() method.

This is because all reads and writes to the properties will go through the hooks, whether within the class itself, or by calling classes. For getters and setters and magic methods, this only applies to calling classes.

Regardless of whether you're using Property Hooks or not, you'd initialise the class the same way:

$number = new PhoneNumber('61', '04', '98867191');

However, consider the way you'd access the respective properties. When using Property Hooks and magic methods, manipulating the properties would have the same API:

print $number->phoneNumber;
$number->areaCode = '02';
$number->countryCode = '61';
$number->localPhoneNumber = '98867191';
print $number->phoneNumber;

But getters and setters would be a little longer:

print $number->getPhoneNumber();
$number->setAreaCode('02');
$number->setCountryCode('61');
$number->setLocalPhoneNumber('98867191');
print $number->getPhoneNumber();

Sure, these differences are, effectively, trivial. But, it's something to consider.

Virtual properties

There are a few more fun additions that you may be interested in. One, that I implicitly covered, is virtual and backed properties. Virtual properties are properties that are not backed by an actual value.

Said another way, it's a property where a set hook has not been defined that sets a value for the property. The value that is returned through the get hook is backed by other properties or is the result of an expression using one or more other properties. Backed properties, however, do have a set hook defined.

I won't copy the definition of $phoneNumber here, as it's a bit lengthy. Rather, take a look at this shorter example:

public bool $isMobileNumber {
    get => $this->areaCode === "04" || $this->areaCode === "4";
}

Here, I've defined a boolean property $isMobileNumber, which will be set to true if $areaCode is set to either "04" or "4". This is because mobile (cell) numbers in Australia start with "04". As no set hook is defined, it's virtual.

Default property values

The next point that I want to cover is default values. They're defined exactly the same as in earlier versions of PHP.

private string $areaCode = "07" {
    set …
    get …
}

Two important points are worth noting with them. These are:

  • They are only supported on backed properties. It would not make sense to allow them for virtual properties, as virtual properties have no underlying value store.
  • They are assigned directly, not through the set hook (if no value is provided through the constructor). However, after initialisation, all writes to the property will go through the set hook.

Property Hook implications

Serialization

There are several implications for serialization, but the one that I want to point out is when serializing an object, only the raw, underlying value of backed properties will be stored. The get hook will not be called. During unserialization, backed properties will be written directly without going through the set hook. __serialize() and __unserialize() will, however, go through the get hook.

Magic methods

Quoting the RFC:

…if the property is not visible in the calling scope then the appropriate magic method will be called just as if there were no hooks. Within the magic methods, the property will be visible and therefore accessible. Reads or writes to a hooked property will behave the same as from any other method, and thus hooks will still be invoked as normal.

Constructor property promotion

Property hooks are allowed with Constructor Promotion, as in the following, refactored example of PhoneNumber's constructor:

public function __construct(
    string $areaCode,
    string $localPhoneNumber,
    string|null $countryCode = null,
    public private(set) string|null $extension = null {
        set {
            $this->extension = $this->matchesRegex("/[0-9]{1,4}/", $value) 
                ? $value 
                : null;
        }
    }
) {
    $this->areaCode = $areaCode;
    $this->countryCode = $countryCode;
    $this->localPhoneNumber = $localPhoneNumber;
    $this->extension = $extension;
}
If the property's visibility definition seems strange (or just stands out) jump down to the next section on Asymmetric Visibility, which will explain what's going on.

It's worth noting that there is the possibility that, if you're not careful, constructors could become quite complicated. So, if you choose to use property hooks with constructor promotion, try to keep them simple, or make use of utility functions if they are becoming too complex.

Asymmetric Visibility

PHP has three visibility levels for class properties, functions, and constants: public, protected, and private. Up until PHP 8.4 only one could be set. However, 8.4 introduces Asymmetric Visibility, which allows for properties to have different visibilities for read and write operations.

Let's say that in a future release of the PhoneNumber class, the ability to set an extension will be available. The business logic for it says that it must be a string, between one and four digits long, with valid values being digits between zero and nine. Whether it's set or not will not affect printing of the phone number, as it won't be included in the printed value.

Here's how we could define it, making use of Asymmetric Visibility:

public private(set) string|null $extension = null {
    set {
        $this->extension = $this->matchesRegex("/[0-9]{1,4}/", $value)
            ? $value
            : null;
    }
}

The part to focus on is public private(set). This means that $extension has public visibility for read operations, but is private for writes. Given that, writes to it will go through the set hook and reads will return its backed value directly. Because of that, we've implicitly created a getter, without the effort of actually having to do so. For the sake of brevity, I've used the implicit block form in the above example.

There is so much more to Property Hooks such as abstract properties, property type variance, inheritance and how they interact with traits, as well as some potentially confusing implications.

So, I strongly recommend you take the time to read through the Property Hooks RFC, which does an excellent job of covering everything that you need to know and Matthew Weier O'Phinney's excellent deep dive.

New class invocation without parentheses

Now, for a much smaller change, one that I'm excited to see, but which might also be a little confusing to read at first. Way back in PHP 5.4.0, class member access on instantiation was added. This meant if you wrapped object instantiation in parenthesis, you could call a method or access a property on the new object straight away, without needing to use an intermediate variable.

Take the following example where we instantiate a new PhoneNumber object, and check if the phone number is a mobile (cell) number by referencing the isMobileNumber property:

print (new PhoneNumber($areaCode, $localNumber, $countryCode))->isMobileNumber;

The parenthesis make it clear what's going on. However, it's a little verbose. With the new invocation without parenthesis form, it can be simplified to the following:

echo new PhoneNumber($areaCode, $localNumber, $countryCode)->isMobileNumber;

What do you think? Is it easier to read, or not? I know that, for me, it's taken a little getting used to.

The #[\Deprecated] attribute

For some time, we've been able to use the @deprecated docblock comment to mark a function as deprecated. However, while helpful, docblock comments don't directly interact with the PHP engine.

Attributes, however, can. Enter the #[\Deprecated] attribute, as you can see in the example below.

#[\Deprecated]
private function matchesRegex(string $regex, string|null $value): bool
{
    return $value !== null 
            && preg_match($regex, $value) === self::MATCHES_REGEX;
}

By using it, you can make use of PHP's deprecation mechanism to throw an E_USER_DEPRECATED error to be emitted on calls to functions and class constants marked with the attribute. Here's an example of what you can expect to see:

1) /Users/msetter/Workspace/PHP/scratch/src/PhoneNumber.php:31
Method Settermjd\Scratch\PhoneNumber::matchesRegex() is deprecated

It's worth knowing that the attribute supports two parameters: $message and $since. Both will be included with the error message. $message is meant to provide more meaningful context to the error message and $since is meant to provide the version, date, or whatever else would be helpful to show when the function of constant was deprecated.

Here's an example of using both:

#[\Deprecated(
    "matchesRegex has been deprecated in favour of doesMatchRegex", 
    "version 2.1.3"
)]
private function matchesRegex(string $regex, string|null $value): bool
{
    return $value !== null 
            && preg_match($regex, $value) === self::MATCHES_REGEX;
}

Here's an example of what the error message might look like:

4) /Users/msetter/Workspace/PHP/scratch/src/PhoneNumber.php:44
Method Settermjd\Scratch\PhoneNumber::matchesRegex() is deprecated since version 2.1.3, matchesRegex has been deprecated in favour of doesMatchRegex

This will be a solid addition with static analysis tools, text editors, and IDEs able to help you write better code.

Parsing Non-POST HTTP requests

You'll be familiar with PHP parsing both GET and POST requests, storing query string parameters in the $_GET superglobal, POST request data in the $_POST superglobal, and uploaded files in the $_FILES superglobal.

But, there are other HTTP request methods, including PUT, DELETE, and PATCH. PHP's various frameworks have functionality for handling requests that use these other methods, such as Slim's MethodOverrideMiddleware, Method Spoofing in Laravel, or BodyParamsMiddleware in Mezzio.

However, what about a native PHP function? PHP 8.4 introduced the request_parse_body function which:

…reads the request body and parses it according to the Content-Type header.

If the HTTP request method is not POST, the function will return an array with the $_POST superglobal as the first element and $_FILES superglobal as the second element, merged with the variables from the request, where applicable. But, the request's Content-Type header must be set to one of application/x-www-form-urlencoded or multipart/form-data.

The function also allows you to override several ini settings, including max_file_uploads and upload_max_filesize, by passing an associative array to the function.

Have a look at the following example, which is an extremely minimal Slim framework-powered application.

<?php

declare(strict_types=1);

use App\Handler\DefaultHandler;
use Slim\Factory\AppFactory;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;

require __DIR__ . '/../vendor/autoload.php';

$app = AppFactory::create();

$app->map(['DELETE', 'PATCH'], '/', function (
    Request $request, Response $response, array $args
    ) {
        [$_POST, $_FILES] = request_parse_body([
            'post_max_size' => '25M',
            'upload_max_filesize' => '10M',
        ]);
        print_r($_POST);
        return $response;
    });

$app->run();

The code sets up a web application with one route / that can be requested with either the DELETE or PATCH HTTP methods. Any non-file request variables will be stored in the $_POST superglobals, and any uploaded files will be stored in the $_FILES superglobal.

Let's assume that you made the following PATCH request with curl:

curl \
    -X PATCH \
    -F "name=matthew" \
    http://localhost:8080/

The form variable "name", which is set to "matthew", would now be available in the $_POST superglobal; so you'd see the following printed to the terminal:

Array
(
    [name] => matthew
)

Four new array functions

PHP has four new array functions to add to its extensive list:

  • array_find: This returns the first array element that satisfies the provided callback.
  • array_all: This checks if all array elements satisfy the provided callback.
  • array_any: This checks if at least one array element satisfies the provided callback.
  • array_find_key: This returns the key of the first array element that satisfies the provided callback.

Here's some example code that uses all four functions.

<?php

declare(strict_types=1);

$data = [
    1 => "David",
    2 => "Matthew",
    3 => "Mike",
    4 => "Peter",
    5 => "Sebastian",
];

print array_find($data, fn ($value, $key) => $value === "Sebastian") . "\n";
print array_all($data, fn ($value, $key) => is_string($value)) ? "All elements match\n" : "At least one element did not match\n";
print array_any($data, fn ($value, $key) => strlen($value) >= 5) ? "At least one element matches\n" : "No elements match\n";
print array_find_key($data, fn ($value, $key) => strlen($value) >= 7);

Running the above code will result in the following output:

Sebastian
All elements match
At least one element matches
2

And that's an overview of what's changed in PHP 8.4

It's a release filled with new functionality that will simplify the code that you write, allowing you to do more with less, all the while writing more maintainable code.

What's more, like most releases, it contains performance improvements and bug fixes that continue making the language more enjoyable to use, more capable, and more professional in nature.

If you haven't already used it, I hope that this post convinces you to try it out. Check out the official 8.4 release information, install and play with it, then start making plans to migrate your applications as soon as practically possible.

Matthew Setter is a PHP and Go editor in the Twilio Voices team and a PHP and Go developer. He’s also the author of Mezzio Essentials and Deploy With Docker Compose. You can find him at msetter[at]twilio.com and on LinkedIn and GitHub.