Chapter 20: Modern PHP Features
PHP continues to evolve into a modern, expressive language with robust typing, metadata, asynchronous patterns, and safer defaults. This chapter highlights major features introduced from PHP 8 onward, including attributes, enums, readonly constructs, fibers, callable improvements, and key deprecations you should prepare for when upgrading.
Attributes and Metadata
Attributes provide native structured metadata for classes, functions, methods, parameters, and properties. They replace docblock annotations and are available at runtime through reflection.
Declaring and applying attributes
Define an attribute as a class marked with #[Attribute]. Apply it with square brackets to targets. Attributes can carry constructor arguments for configuration.
<?php
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class Route
{
public function __construct(
public string $path,
public array $methods = ['GET']
) {}
}
#[Route('/users', methods: ['GET', 'POST'])]
class UserController
{
// ...
}
Reading attributes with reflection
Use ReflectionClass, ReflectionMethod, or ReflectionProperty to inspect applied attributes and their arguments.
$ref = new ReflectionClass(UserController::class);
foreach ($ref->getAttributes(Route::class) as $attr) {
$route = $attr->newInstance();
echo $route->path; // /users
}
Enums and Backed Enums
Enums introduce a fixed set of named values. They improve clarity, prevent invalid strings, and simplify comparison logic. A backed enum associates each case with a scalar value, typically a string or int.
Defining pure enums
Pure enums have only symbolic names. Use them for small finite sets like states or modes.
enum Status
{
case Draft;
case Published;
case Archived;
}
Using backed enums with scalar values
Backed enums carry a built-in scalar that can map to storage values such as database fields.
enum Role: string
{
case User = 'user';
case Admin = 'admin';
}
$role = Role::from('admin'); // Role::Admin
echo $role->value; // admin
Enumerating and switching over cases
You can iterate over all enum cases or match specific ones with match expressions.
foreach (Role::cases() as $r) {
echo $r->name . "\n";
}
$message = match ($role) {
Role::Admin => 'Welcome admin',
Role::User => 'Hello user',
};
value accessors to keep schema compatible with plain strings.
Readonly Classes and Properties
Readonly properties prevent modification after initialization. They make objects immutable by contract. A readonly class extends this guarantee to all properties.
Using readonly properties
Mark properties as readonly to restrict assignment after construction. Attempting to reassign triggers an error.
class Point
{
public function __construct(
public readonly int $x,
public readonly int $y,
) {}
}
$p = new Point(3, 4);
// $p->x = 5; // Error: Cannot modify readonly property
Declaring fully readonly classes
A readonly class implicitly applies the modifier to every property. This helps define pure value objects with minimal boilerplate.
readonly class Dimensions
{
public function __construct(
public int $width,
public int $height,
) {}
}
Fibers and Cooperative Concurrency
Fibers introduce lightweight execution contexts that pause and resume. They allow non-blocking frameworks to structure asynchronous code without callbacks or promises. Each fiber runs until it yields control back to the scheduler.
Creating and resuming a fiber
Fibers wrap a callable. Use start() to begin execution and resume() to continue after yielding.
$fiber = new Fiber(function (): void {
echo "Start\n";
$value = Fiber::suspend('pause');
echo "Resumed with: $value\n";
});
$result = $fiber->start(); // Start
echo "Fiber returned: $result\n";
$fiber->resume('done'); // Resumed with: done
Scheduling fibers for cooperative tasks
Frameworks such as amphp or ReactPHP use fibers to build asynchronous servers. Fibers enable readable, sequential style while remaining non-blocking underneath.
First-class Callables and Improvements
PHP 8.1 introduced first-class callable syntax and improved Closure behavior. This simplifies passing methods or functions as variables without using call_user_func().
Creating first-class callables
Use the ... syntax to reference a function or method directly. The result is a Closure you can store or call later.
function greet(string $name): string {
return "Hello, $name";
}
$fn = greet(...);
echo $fn('World'); // Hello, World
Referencing object and static methods
You can obtain callables for instance or static methods using the same syntax.
class Printer
{
public function echoLine(string $msg): void { echo $msg . "\n"; }
public static function shout(string $msg): void { echo strtoupper($msg); }
}
$p = new Printer();
$instanceFn = $p->echoLine(...);
$staticFn = Printer::shout(...);
$instanceFn('hello');
$staticFn('attention');
Using Closure::fromCallable() for dynamic references
Closure::fromCallable() safely converts a callable array or string into a closure when syntax sugar is not possible.
$callable = [new Printer(), 'echoLine'];
$closure = Closure::fromCallable($callable);
$closure('Dynamic call');
Deprecations and Migration Tips
With modernization, PHP deprecates older behaviors to promote consistency and safety. Staying up to date avoids surprises when upgrading major versions.
Common deprecated patterns
Recent releases have deprecated dynamic property creation, mbstring.func_overload, and implicit float to int conversions. These changes encourage explicit definitions and stricter typing.
class Legacy {
// public $newProp; // define explicitly
}
$l = new Legacy();
$l->extra = 123; // Deprecated: dynamic property creation
Adapting code for stricter types
Gradually add scalar and return types. Where compatibility is uncertain, use union types or mixed. Use declare(strict_types=1) at the top of new files for predictable behavior.
declare(strict_types=1);
function add(int|float $a, int|float $b): float {
return $a + $b;
}
Planning upgrades and testing on multiple versions
Use tools like rector, phpcompatibility/php-compatibility, or phpstan to check readiness. Run your test suite against new PHP versions early to catch regressions before production.
Modern PHP favors safety, readability, and predictability. By embracing attributes, enums, readonly design, cooperative concurrency, and first-class callables, you prepare your applications for current and future versions of the language with confidence.