Great new PHP 8.1 features: enums, readonly & more (2021)

Ralph J. Smit Laravel Software Engineer

The latest big version of PHP was released less than a month ago. It brought some great features that will make your life easier and your code more readable. I’m already using it on production and many others are too, so now is the perfect time to take a look at all the new features go PHP 8.1.

This article will show you the latest and greatest features of PHP 8.1, accompanied with easy code blocks.

Native support for enums (enumerations)

This is no doubt the biggest feature of PHP 8.1: enums. I think that many people will love them, though if you’ve never used some enum-like class, you might need to see the value of them first before you’re going to love it.

In short, what are enums? 

Have you ever had the situation where something (like a database record) could have several states? And did you ever want to ‘programmatically enforce’ that state? (Programmatically enforcing is a nice way of saying that you want to allow fellow developers to 'do whatever they like' with your code.)

Imagine an invoice: it could have states like draft, pending, paid and expired and we’re storing those states in the database as strings. But how do we actually enforce this state, so that it can only have a value from a certain set of values?

Everybody would agree that a state like exploded doesn’t make sense for an invoice, so the question is: can we make sure that we only have valid states?

Of course you could litter your code with if-else statements and other such conditions, but that is far from ideal. In particular when a project becomes bigger and multiple people work on it, this isn’t great.

Well, enums are the solution to the above problem of managing states. In fact, an enum itself is a certain state. It works like this:

  1. An enum is a sort of PHP-class, except that they are defined with the enum keyword instead of the class keyword.

  2. In the enum, you can define all the allowed values (like draft, pending, paid, etc.).

  3. (Optional) For each of those values, you define a string, which can be used to store the state in a database.

  4. Whenever you retrieve a database record, you cast the string value to the correct enum object

  5. Now you are completely sure that no weird values can be used a state (it’ll throw an exception). And you can do things like: if ($state === InvoiceStatus::paid). (See how nice!)

How to use enums

So, now that we know the theory about enums in PHP, how do we use enums? Defining an enum is very easy:

namespace App\Enums;
enum InvoiceStatus: string {
case draft = ‘draft’;
case pending = ‘pending’;
case paid = ‘paid’;
case expired = ‘expired’;

You can use them like this:

use App\Enums\InvoiceStatus;
$stateAsString = 'pending';
$state = InvoiceStatus::from($stateAsString); // Returns exception if fails
// Or: $state = InvoiceStatus::tryFrom($stateAsString); // Returns null if fails

Simple, right? You can also add methods, implement interfaces, use traits and many other things. Also, you could have an enum that doesn’t use strings to back values. (Could be useful for DTOs.)

Checkout my full article about enums for all the examples. (Coming soon.)

Readonly properties

Another major (but slightly less major than enums) is the introduction of readonly properties. As the name suggests, you can now set properties on a class to be readable only. That means that they cannot be changed after they were set for the first time.

If you wanted to achieve that readonly effect in the past, you had to choose one of the following options:

  1. Set the property to public and agree that no-one should ever write to it.

  2. Make the property protected or private and add a special getter method to the class (like getName(), getTitle(), etc.)

The first method is not 100% secure and the second method is very boring. Now checkout the following PHP8.1 example:

namespace App\DataTransferObjects;
use App\Enums\PaymentStatus;
class PaymentData {
public function __construct(
public readonly string $uuid,
public readonly int $amount,
public readonly PaymentStatus $status, /* See this little enum? :-) */
) {}

Now you are 100% sure that the properties can be set once, and only once, and never changed afterwards.

Array unpacking with string keys

Another nice quality-of-life improvement is that array unpacking is now also possible with string keys. Array unpacking is a very clean and nice syntax (available since PHP 7.4), but unfortunately you still needed the old array_merge() for string keys. Not anymore!

$author = [
'name' => 'John',
'age' => '25',
$job = [
'title' => 'Developer',
$authorInfo = [
// => [
// 'name' => 'John',
// 'age' => '25',
// 'title' => 'Developer',

Use `new Class()` in constructors/initializers

You must already know that we can give default values to parameters in functions and constructors. Now, we can also automatically create a new instance of a class, without having to pass it into the function anymore!

Remember our PaymentData example? Here's the updated version for PHP 8.1:

use App\Enums\PaymentStatus;
use App\DataTransferObjects\Subscription;
class PaymentData {
public function __construct(
public readonly string $uuid,
public readonly int $amount,
public readonly PaymentStatus $status,
public readonly Subscription = new Subscription(),
) {}
public function newSubscription(
$subscription = new Subscription(),
) {}

How to check whether a PHP array is a list

Have you ever had the need to check if an array is a pure list (with keys only going from 0, 1, 2, etc.)? Well, the array_is_list($array) function has your back!

$array = [
'Full-stack developer',
'Front-end developer',
'Back-end developer',
// true
$array = [
'a' => 'Full-stack developer',
'b' => 'Front-end developer',
'c' => 'Back-end developer',
// false

Generating callables directly from a function

You can now easily create a callable / callback directly from a function. Which is pretty amazing and very neat.

use App\Models\User;
use App\DataTransferObjects\PaymentData;
function createPayment(int $amount, User $user): PaymentData {
// Do something
$createPayment = createPayment(...);
$paymentData = $createPayment(amount: 100, user: User::first());

Intersection types

The last PHP 8.1 feature might sound somewhat vague, but an example will quickly clear things up. Have you every written a function that accepts a parameter like the one below?

public function createPayments(array|Collection $payments) {

As you see, the $payments parameter can only be an array or a Collection, but not a string or anything else. It should be one or the other. Now you can also define parameter that should be/have both types.

If you're wondering, how can a parameter have two types? I mean, you can never have a string and an array at the same time. The trick here is to typehint interfaces, meaning that you can now specify that you want to receive an object that implements two interfaces:

function generatePaymentLink(Billable&Subscriber $user) {

Now, the $user parameter should be an object that implements both the Billable interface and is a Subscriber.


As you've seen, PHP 8.1 brings several nice new features and quality-of-life improvements. True, many things were already possible before, but the new features allow you to write much cleaner code. (That's why I love PHP 8.0 property promotion so much, because it cleans up your code a lot.)

You can now replace all your getters with readonly properties, use enums natively instead of a package, abandon your custom PHP function to determine whether an array is a list, and more.

There's also another last feature which is nice to mention, but something you'll likely not use that often. That feature is fibers. It allows you to exercise more control over the program flow and you can execute certain things simultaneously. You can check out Brent Roose's post about fibers if you want to learn more about fibers.

Feel free to leave a comment if you have a question or a remark, and I'm looking forward to PHP 8.2 already⚡️😆

Published by Ralph J. Smit on in Php . Last updated on 11 March 2022 .