Articles/PHP/What's New in PHP 8.1

What's New in PHP 8.1

PHP 8.1 is one of the most loved releases of the 8.x line. Here are its headline features with practical examples: enums, readonly properties, first-class callable syntax, fibers, the never return type, and new in initializers.

November 27, 2021·6 min read

PHP 8.1 arrived on 25 November 2021, and it is the release that turned a lot of skeptics into fans. After 8.0 modernized the syntax, 8.1 filled in the features people had been faking for years: real enums, properties you cannot mutate, and a proper concurrency primitive. Here are the ones worth knowing.

Enumerations

Before 8.1, a fixed set of options meant class constants or bare strings, with nothing stopping an invalid value from sneaking through. Enums give you a real type whose instances are the only legal values, and they can carry methods.

PHP
enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
    case Cancelled = 'cancelled';

    public function canCancel(): bool
    {
        return match ($this) {
            self::Pending, self::Paid => true,
            self::Shipped, self::Cancelled => false,
        };
    }
}

$status = OrderStatus::from('paid');
$status->canCancel(); // true

A backed enum maps each case to a scalar, so OrderStatus::from('paid') validates and returns the case, while tryFrom('bogus') returns null instead of throwing. This is the type-safe version of the constants idea I covered in class constant visibility, with behavior attached.

Readonly properties

A property marked readonly can be written once, during initialization, and never again. It is the single cleanest way to build an immutable value object.

PHP
final class Coordinates
{
    public function __construct(
        public readonly float $lat,
        public readonly float $lng,
    ) {}
}

$home = new Coordinates(40.7128, -74.0060);
$home->lat = 41.0; // Error: Cannot modify readonly property

Combined with constructor promotion from 8.0, an immutable object is now a handful of lines, and any attempt to mutate it after construction fails loudly rather than corrupting state somewhere downstream.

First-class callable syntax

Passing a function around used to mean string names or the clunky Closure::fromCallable(). The new ... syntax produces a real closure from any function or method, with full type information.

PHP
$lengths = array_map(strlen(...), ['php', 'is', 'fun']);
// [3, 2, 3]

$normalizer = $formatter->normalize(...);
$clean = array_map($normalizer, $rawValues);

strlen(...) means "the strlen function as a closure," and $formatter->normalize(...) captures a bound method. It is concise, it is analyzable by your IDE and static analysis tools, and it works anywhere a callable is expected.

Fibers

Fibers are PHP 8.1's lightweight, cooperative concurrency primitive. A fiber can pause itself mid-execution and hand control back to the caller, then resume exactly where it left off. They are the engine behind modern async libraries, but the core idea is small enough to demo directly.

PHP
$fiber = new Fiber(function (): void {
    echo "fetching...\n";
    $chunk = Fiber::suspend('need data');
    echo "resumed with: $chunk\n";
});

$request = $fiber->start();   // prints "fetching..." then returns 'need data'
$fiber->resume('here it is'); // prints "resumed with: here it is"

Fiber::suspend() freezes the function and returns its argument to whoever called start() or resume(). You hand a value back in, and execution picks up from the suspension point. You will rarely write this by hand, but understanding it demystifies how async PHP frameworks pull off non-blocking I/O.

The never return type

The never type documents that a function never returns normally: it always throws or exits. That tells both the engine and your tooling that any code after the call is unreachable.

PHP
function abort(int $status, string $message): never
{
    http_response_code($status);
    echo $message;
    exit;
}

$user = find($id) ?? abort(404, 'Not found');

Because the type system knows abort() cannot fall through, the analyzer is satisfied that $user is non-null afterward. It is a tiny annotation that makes guard clauses and redirect helpers type-check cleanly.

New in initializers

You can now use new to create objects in default parameter values, attribute arguments, and other initializer positions. This makes optional dependencies expressible without a null-and-assign dance in the constructor body.

PHP
final class ReportService
{
    public function __construct(
        private Logger $logger = new NullLogger(),
    ) {}
}

$service = new ReportService(); // uses the NullLogger default

If a caller injects a real logger, it wins; otherwise the harmless default is used. The constructor body stays empty, and the default is right there in the signature where you can see it.

PHP 8.1 is where the modern PHP toolkit really came together. Enums and readonly properties in particular changed how people model domains, and you will see both lean on the 8.0 features that came just before. Next up, 8.2 takes the immutability story further and tidies up the type system.