What's New in PHP 8.4?
Time to read: 11 minutes
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.
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:
Using getters and setters
Here's an example of how you could refactor the class using getter and setter functions:
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.
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.
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.
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.
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.
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.
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:
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:
But getters and setters would be a little longer:
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:
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.
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:
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:
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:
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:
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.
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:
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:
Here's an example of what the error message might look like:
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.
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:
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:
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.
Running the above code will result in the following output:
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.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.