PHP 8.1 New Features: A Practical Guide to Enums and Readonly Properties

12 min read 2,268 words
PHP 8.1 New Features: Enums, Readonly Properties and Fibers Explained featured image

What PHP 8.1 Delivered for Developers

PHP 8.1 arrived in November 2021 with a set of features that changed how developers write and maintain PHP code. The release built on PHP 8.0's foundation of JIT compilation and named arguments, adding syntax improvements that make applications more type-safe, more expressive, and easier to maintain over time. For anyone working with PHP on business applications, web services, or custom development projects, understanding these PHP 8.1 features helps you write better code and recognise modern patterns when reading existing codebases.

Some PHP 8.1 additions became standard practice almost immediately. Enums and readonly properties appear frequently in well-structured applications built since that release. Other features like Fibers and intersection types serve more specific purposes but are worth understanding when you encounter them in code or need their particular capabilities.

PHP Enums: Type-Safe Fixed Values

Before PHP 8.1, representing a fixed set of values typically involved class constants or interface constants. This approach worked but lacked the type enforcement that Enums provide. A function accepting a string parameter could receive any string value. An Enum parameter restricts input to only the defined cases, and the PHP type system enforces this restriction at compile time and runtime.

PHP Enums fall into two categories. Backed Enums associate each case with a scalar value, making them useful for database storage, API responses, and anywhere you need both type safety and a serialisable value.

enum Status: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';

    public function label(): string
    {
        return match($this) {
            self::Draft => 'Draft',
            self::Published => 'Published',
            self::Archived => 'Archived',
        };
    }
}

You can use this Enum anywhere you would use a type hint. The type system enforces that only valid Enum values can be assigned, which catches mistakes early rather than letting invalid data propagate through the application.

function publish(Post $post): void
{
    if ($post->status !== Status::Draft) {
        throw new InvalidArgumentException('Can only publish draft posts');
    }
    $post->status = Status::Published;
}

Pure unit Enums have no associated scalar value. They work well for internal application states, configuration options, or anywhere you need the type safety without a corresponding database column.

enum OrderStatus
{
    case Pending;
    case Processing;
    case Shipped;
    case Delivered;
    case Cancelled;
}

Enums can also implement interfaces, giving you flexibility when building domain models. This combination of type safety and interface flexibility makes Enums particularly valuable in business applications where data integrity matters.

For those working with newer PHP versions, the evolution continues with PHP 8.3, which introduced typed class constants that complement Enum functionality. You can compare the approaches in the PHP 8.3 changes overview.

Readonly Properties and Readonly Classes in PHP 8.1

Readonly properties solve a common problem in PHP development: preventing accidental modification of values that should remain constant after initialisation. Once assigned, a readonly property cannot be changed anywhere in your code. Any attempt to modify it throws an Error, making the immutability enforced rather than convention-based.

This is particularly useful for value objects and data transfer objects where immutability matters. Financial data, configuration values, and domain entities often benefit from being immutable once created.

readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency
    ) {}
}

$price = new Money(100, 'GBP');
$price->amount = 200; // Error: Cannot modify readonly property

You can apply the readonly keyword to individual properties or to an entire class. When applied to the class, all properties become readonly automatically, and the class cannot have writable properties at all.

readonly class Point3D
{
    public function __construct(
        public float $x,
        public float $y,
        public float $z,
    ) {}
}

Readonly properties work especially well with constructor property promotion. This feature, introduced in PHP 8.0, combines parameter declaration and property assignment in one step, reducing boilerplate significantly for classes that primarily hold data.

For applications processing financial calculations, API response objects, or any domain where mutation should be explicit and controlled, readonly properties and classes make your intent clear. They also prevent bugs that arise from accidental property modification in code paths you may not immediately recognise.

Intersection Types for Multiple Type Constraints

PHP 8.1 introduced intersection types, which allow a parameter to satisfy multiple type constraints simultaneously. This differs from union types, which accept any one of several types. Intersection types require a value to match all declared types at once.

function countItems(Countable&Iterable $items): int
{
    return count($items);
}

countItems($array);      // ArrayObject is both Countable and Iterable
countItems($generator);  // Generator is both Countable and Iterable

Intersection types are less frequently needed than union types, but they are valuable in generic contexts and when working with interfaces that define complementary capabilities. A function requiring an object that implements multiple interfaces can express this constraint directly without wrapper classes or additional type checking.

One important restriction applies: you cannot combine intersection types with union types in the same parameter declaration. For mixed type requirements, consider whether the function needs refactoring or whether a separate method handling each case makes more sense.

The Never Return Type in PHP 8.1

The never return type indicates that a function never returns normally. It either throws an exception, calls exit, or terminates in some other way that does not return control to the caller. This is useful for functions that halt execution unconditionally.

function redirect(string $url): never
{
    header('Location: ' . $url);
    exit;
}

function fail(string $message): never
{
    throw new RuntimeException($message);
}

The never type helps static analysis tools understand control flow. If a function is declared to return never, the analysis tool knows that any code after a call to that function is unreachable. This enables better dead code detection and more accurate static analysis across your codebase.

When a function is declared with never as its return type, the calling code will not continue past that point. This helps identify logic errors where code paths assume execution continues after a function that actually terminates.

Array Unpacking with String Keys

PHP 8.1 resolved a long-standing limitation with the spread operator. Previously, array unpacking with string keys produced a parse error. PHP 8.1 allows spreading arrays with string keys, making array merging more flexible and consistent with how numeric key merging already worked.

$base = ['name' => 'Original', 'status' => 'active'];
$additional = ['status' => 'updated', 'version' => 2];
$result = [...$base, ...$additional];

// Result: ['name' => 'Original', 'status' => 'updated', 'version' => 2]

When duplicate string keys exist, the later value overwrites the earlier one. This is consistent with how array merging works in PHP generally. Understanding this behaviour helps avoid surprises when merging configuration arrays or combining data from multiple sources.

This improvement is particularly useful when combining configuration from multiple sources, merging default values with user-provided overrides, or building request data from multiple input streams. It removes the need for workarounds like array_merge() when the spread operator would be more natural.

First-Class Callable Syntax

PHP 8.1 introduced first-class callable syntax, allowing you to pass callable objects using a familiar syntax. Instead of using the Closure::fromCallable() method, you can now reference callable instances directly with the ... syntax.

class ApiClient
{
    public function fetch(string $endpoint): mixed
    {
        // Implementation
    }
}

$client = new ApiClient();

// PHP 8.0 and earlier
$fetch = Closure::fromCallable([$client, 'fetch']);

// PHP 8.1+
$fetch = $client->fetch(...);

The ... syntax creates a callable that preserves the bound object. This is cleaner than the previous approach and makes the intent more obvious when extracting methods for use as callbacks in event systems, middleware, or asynchronous processing pipelines.

Performance Improvements in PHP 8.1

PHP 8.1 continued the performance work from PHP 8.0. The JIT compiler received refinements, and several internal operations were optimised. For typical web applications, these improvements are noticeable without requiring any code changes.

Array operations saw meaningful speedups. Functions like array_sum, array_map, and array_filter perform better in PHP 8.1. For applications that process large datasets or perform array-heavy operations, these improvements compound across many function calls.

The JIT compiler enhancements are most visible in long-running processes and computational tasks. For request-response web applications, the benefits are more modest but still present. The performance gains are incremental but consistent, meaning applications generally run faster with no changes required.

When planning a PHP version upgrade across multiple major versions, the PHP 8.3 changes guide covers performance considerations in more detail, including how the JIT has evolved in recent releases.

Fibers for Cooperative Multitasking

PHP 8.1 introduced Fibers, which provide a way to pause and resume execution at arbitrary points within a call stack. Unlike Generators, which can only yield upward to the caller, Fibers can suspend from any depth in the call stack and resume from the same point.

$fiber = new Fiber(function (): void {
    echo "Starting fiber\n";
    Fiber::suspend();
    echo "Resuming fiber\n";
});

echo "Before start\n";
$fiber->start();
echo "After start\n";
$fiber->resume();
echo "After resume\n";

Fibers are primarily relevant to library and framework authors building async runtimes, coroutine schedulers, or similar concurrency primitives. Application developers interact with Fibers indirectly through higher-level abstractions built on top of them.

If you are building or maintaining code that handles asynchronous operations, understanding what Fibers enable helps when evaluating frameworks and their underlying approaches to concurrency. Many modern PHP frameworks use Fibers internally to manage async operations without exposing the complexity to application code.

Deprecated Features to Address Before PHP 9

PHP 8.1 deprecated several features that will be removed in PHP 9.0. Addressing these deprecations proactively prevents a more difficult upgrade path later. Running your application in a development environment with deprecation warnings enabled reveals code that needs attention before the next major version.

Dynamic properties were deprecated in PHP 8.1. Assigning properties to objects that do not declare them is now deprecated, except for stdClass and classes marked with the AllowDynamicProperties attribute. This change improves performance and reduces the risk of typos creating unexpected properties. In PHP 9.0, it will become an Error.

// Deprecated pattern in PHP 8.1
$object->newProperty = 'value'; // Deprecated in 8.1, Error in 9.0

// Fix: declare the property explicitly
class User
{
    public string $declaredProperty = '';
}
$object->declaredProperty = 'value';

The legacy MySQL extension (ext/mysql with mysql_* functions) was deprecated. If you maintain code using these functions, migrate to mysqli or PDO as a priority. Both modern extensions support prepared statements, which protect against SQL injection, and both offer object-oriented interfaces that work well with modern PHP practices. The securing PHP applications guide covers input handling and prepared statements in more detail.

Returning by reference from generators was deprecated. Generator functions should return values by yielding rather than by returning references. This simplifies the generator model and aligns with how generators are typically used.

For context on how PHP has deprecated and removed features over time, the PHP 7.4 features overview covers earlier changes that shaped modern PHP practices.

Upgrading to PHP 8.1 from Earlier Versions

The upgrade path from PHP 8.0 to 8.1 is straightforward for most applications. The key steps are running your test suite under PHP 8.1 before deploying to production, reviewing any deprecation warnings, and checking that your framework and dependencies support the new version.

# Enable all warnings in php.ini for development
error_reporting = E_ALL

# Run tests with deprecation display
./vendor/bin/phpunit --display-deprecations

Check your framework documentation for version requirements. Laravel 9 and later support PHP 8.1. Symfony 5.4 and later support PHP 8.1. Most actively maintained packages have moved to PHP 8.1 as a minimum requirement in their current releases.

For new projects, PHP 8.1 is a solid starting point. The type system features reduce bugs and make code easier to understand. For existing projects on PHP 8.0, the upgrade is low-risk and the performance benefits apply immediately without any code changes.

Frequently Asked Questions

What is the main advantage of PHP Enums over class constants?
Enums provide type safety that class constants do not. When a function expects an Enum parameter, the type system ensures only valid Enum cases can be passed. With class constants, a function expecting a specific constant value can receive any value of the underlying type, which can introduce bugs if invalid values are passed accidentally.
Can readonly properties be used with dependency injection?
Yes, readonly properties work well with dependency injection. Constructor injection is the natural fit since the dependency injection container assigns the dependency once during object construction. The property then remains immutable for the lifetime of the object, which matches how most dependencies are used and provides clarity about their behaviour.
How do intersection types differ from union types?
Union types accept any one of several types. Intersection types require a value to match all declared types simultaneously. A parameter typed as Countable&Iterable must be both Countable and Iterable, not either one or the other. This distinction matters when you need to ensure an object implements multiple interfaces rather than just one of several alternatives.
Is PHP 8.1 still relevant given PHP 8.2 and 8.3 releases?
PHP 8.1 is still fully supported and widely used. Many applications and hosting environments run PHP 8.1, and it receives security updates under the standard PHP support timeline. The features introduced in 8.1 remain valuable, and understanding them helps when working with code written for that version or maintaining applications that have not yet upgraded to newer PHP versions.
What should I check before upgrading to PHP 8.1?
Run your test suite under PHP 8.1 first. Check for deprecation warnings in your output, particularly around dynamic properties and any mysql_* function calls. Verify that your framework version and key dependencies support PHP 8.1. Review the changelog for any behaviour changes that might affect your application before deploying to production.
How do Enums compare to PHP 8.2 and 8.3 readonly classes?
PHP 8.2 introduced readonly improvements and first-class Enums have been available since 8.1. Each version builds on the type safety features of the previous release. The PHP 8.2 features guide covers readonly classes and constants in traits that extend what 8.1 started.