What PHP 8.1 Brought to the Table
PHP 8.1 landed in November 2021, and it continued the forward momentum that PHP 8.0 started. The release introduced meaningful syntax improvements that make code more expressive and easier to reason about. For developers working with PHP on web projects, server-side applications, or custom development work, these features reduce boilerplate and improve type safety in ways that matter day to day.
Several features from PHP 8.1 have become standard practice in modern PHP codebases. Enums and readonly properties, in particular, appear frequently in well-structured applications. Understanding what these features do helps when maintaining existing code or building new projects with current PHP practices.
PHP Enums: Fixed Sets of Values with Type Safety
Before PHP 8.1, representing a fixed set of values typically involved class constants or interface constants. While this worked, it 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 type system enforces this at compile time and runtime.
Backed Enums associate each case with a scalar value. This makes 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',
};
}
}
Use the 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 are useful when the enum cases are self-sufficient without additional data. These 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, which gives you flexibility when building domain models. If you need to understand how PHP has evolved with Enums and other type improvements, there is a useful comparison of PHP 8 features available in the PHP 8.4 property hooks guide.
Readonly Properties and Readonly Classes
Readonly properties solve a common problem: preventing accidental modification of values that should remain constant after initialisation. Once assigned, a readonly property cannot be changed. Any attempt to modify it throws an Error. This is particularly useful for value objects and data transfer objects where immutability matters.
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
The readonly keyword can be applied to individual properties or to an entire class. When applied to the class, all properties become readonly. PHP 8.1 introduced readonly classes, which combine this behaviour with the requirement that the class cannot have writable properties at all.
readonly class Point3D
{
public function __construct(
public float $x,
public float $y,
public float $z,
) {}
}
Readonly is especially powerful with constructor property promotion, which combines parameter declaration and property assignment in one step. This reduces boilerplate significantly for classes that primarily hold data.
For applications processing financial data, configuration values, or any domain where mutation should be explicit and controlled, readonly properties and classes make the intent clear and prevent accidental changes that could introduce bugs.
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.
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
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 code 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.
When a function is declared with never as its return type, the type system also understands that the calling code will not continue past that point. This can help 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.
$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 merging 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.
First-Class Callable Syntax
PHP 8.1 introduced first-class callable syntax, allowing you to pass callable objects using a familiar callable array syntax. Instead of using the Closure::fromCallable() method, you can now reference callable instances directly.
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.
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, the 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 PHP 8 upgrade guide covers performance considerations in more detail for those planning a version jump.
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.
Deprecated Features to Address
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.
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.
Returning by reference from generators was deprecated. Generator functions should return values by yielding rather than by returning references.
For a broader view of how PHP has deprecated and removed features over time, the PHP 7.4 features overview provides context on earlier changes that shaped modern PHP.
Upgrading to PHP 8.1
The upgrade path from PHP 8.0 to 8.1 is smooth 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.