PHP 8 — Key Features
debt(d7/e5/b5/t5)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints.tools field is empty. The common mistakes span multiple categories: UnhandledMatchError only surfaces at runtime when a non-covered value hits a match expression; readonly-but-mutable object confusion is invisible until a bug manifests; dynamic property removal is a deprecation/error that surfaces only at runtime on PHP 8.2+. No single linter catches all of these automatically, so d7 is the right anchor.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix mentions Rector with PHP80/81/82 sets, which automates many patterns. However, Rector handles common cases — manual review is still needed for match exhaustiveness, readonly property semantics, and dynamic property declarations across the codebase. This is more than a one-line fix but less than full architectural rework, landing at e5.
Closest to 'persistent productivity tax' (b5). The applies_to covers both web and cli contexts broadly. Misunderstandings (e.g. JIT expectations, readonly semantics) create an ongoing cognitive tax for maintainers who interact with PHP 8 features regularly. The burden is not codebase-defining but does slow down multiple work streams when teams hold incorrect mental models about JIT, readonly, or match exhaustiveness.
Closest to 'notable trap — a documented gotcha most devs eventually learn' (t5). The canonical misconception is that JIT makes all PHP code faster, when in reality typical I/O-bound web apps see negligible benefit. Additionally, readonly-as-immutable is a well-documented but frequently encountered confusion. These are documented gotchas that most PHP developers encounter, matching t5 well.
Also Known As
TL;DR
Explanation
PHP 8.0 (2020): named arguments, union types, match expression (strict, no fall-through, returns a value), nullsafe operator (?->), JIT compiler, constructor property promotion, str_contains/str_starts_with/str_ends_with, throw as expression, attributes. PHP 8.1 (2021): enums, readonly properties, fibers, intersection types, never return type, array_is_list(), first-class callable syntax (strlen(...)), new in initializers. PHP 8.2 (2022): readonly classes, disjunctive normal form (DNF) types, deprecated dynamic properties, true/false/null standalone types. PHP 8.3 (2023): typed class constants, json_validate(), Override attribute, deep-cloning of readonly properties. PHP 8.4 (2024): property hooks, asymmetric visibility (public get / protected set), updated array functions (array_find, array_any, array_all), HTML5 parser for DOM extension.
Common Misconception
Why It Matters
Common Mistakes
- Using match without covering all cases — match throws UnhandledMatchError if no arm matches and there is no default; always include a default arm or ensure exhaustive coverage.
- Confusing readonly properties with immutable objects — readonly prevents reassignment after initialisation but does not deep-clone objects; a readonly property holding an object can still have its object's properties mutated.
- Using dynamic properties removed in PHP 8.2 — assigning to undeclared properties throws a deprecation in 8.1 and an error in 8.2; declare all properties explicitly.
- Expecting JIT to speed up database-heavy web requests — benchmark before enabling JIT; the overhead of JIT compilation can slow simple scripts.
Code Examples
// PHP 7 style — verbose, error-prone
function createUser(string $name, ?string $email = null, int $role = 0): User {
$this->name = $name;
$this->email = $email;
$this->role = $role;
}
$status = 'active';
$label = $status === 'active' ? 'Active' : ($status === 'pending' ? 'Pending' : 'Unknown');
// PHP 8 style — concise, type-safe
enum Status { case Active; case Pending; case Suspended; }
class User {
public function __construct(
public readonly string $name,
public readonly ?string $email = null,
public readonly Status $status = Status::Active,
) {}
}
$label = match($status) {
Status::Active => 'Active',
Status::Pending => 'Pending',
default => 'Unknown',
};