<< Home

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.

💡 Modern PHP emphasizes explicitness. Type declarations, attributes, and readonly modifiers all make your intent clearer to both humans and tools.

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
}
⚠️ Avoid mixing attributes and docblock annotations for the same purpose. Pick one convention and use it consistently.

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',
};
💡 When storing enums in databases, use backed enums with 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,
  ) {}
}
⚠️ Readonly objects protect state, not deep immutability. If a property holds a mutable object, that object can still change internally.

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.

💡 Fibers do not provide parallelism. They let code yield during I/O so other tasks can progress cooperatively.

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');
⚠️ When passing methods as callables, be careful with scope and late static binding. Always test within the context you intend to execute.

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.

💡 Read each release’s migration guide carefully. Many deprecations emit notices long before removal, giving you time to adapt incrementally.

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.