Articles/PHP/What's New in PHP 8.2

What's New in PHP 8.2

PHP 8.2 polished the type system and the immutability story. Here are its headline features with practical examples: readonly classes, DNF types, standalone null, false and true types, the new Random extension, constants in traits, and sensitive parameter redaction.

December 10, 2022·6 min read

PHP 8.2 shipped on 8 December 2022. It is a quieter release than 8.1, more about refinement than headline features, but the refinements are good ones: whole classes can be readonly, the type system gained new building blocks, and a couple of long-standing rough edges around traits and security finally got sanded down. Here is what stands out.

Readonly classes

PHP 8.1 gave us readonly properties. PHP 8.2 lets you mark an entire class readonly, which applies the modifier to every property at once. For a value object where nothing should ever change, this is the whole declaration.

PHP
readonly class Money
{
    public function __construct(
        public int $amountCents,
        public string $currency = 'USD',
    ) {}
}

$price = new Money(1999, 'EUR');
$price->amountCents = 0; // Error: Cannot modify readonly property

No need to repeat readonly on each property the way the 8.1 example did. The class declaration says it once, and the engine enforces it everywhere, including for any dynamic properties (which are also forbidden, as we will see below).

Disjunctive normal form types

DNF types let you combine union (|) and intersection (&) types in a single declaration, using parentheses to group the intersections. This expresses constraints that neither alone could.

PHP
interface Identifiable { public function id(): int; }
interface Timestamped { public function createdAt(): int; }

function audit((Identifiable&Timestamped)|null $entity): void
{
    if ($entity === null) {
        return;
    }
    log("#{$entity->id()} at {$entity->createdAt()}");
}

The signature reads as "either something that is both Identifiable and Timestamped, or null." Before 8.2 you could not mix the two operators, so a case like this meant giving up and falling back to a looser type.

Standalone null, false, and true types

null, false, and true became types you can use on their own. They are most useful as precise return types for functions that, by contract, only ever yield one of those values.

PHP
final class Cache
{
    public function clear(): true
    {
        // ... always succeeds, or throws
        return true;
    }

    public function missingKey(): null
    {
        return null;
    }
}

A function returning : false or : true tells the caller far more than : bool does, and pairs well with union types when a method returns either a real value or a sentinel false.

The new Random extension

PHP 8.2 introduced an object-oriented Random extension built around a Randomizer class and pluggable engines. It replaces the old global Mersenne Twister functions with something testable and explicit, and it bundles handy helpers.

PHP
$randomizer = new \Random\Randomizer();

$roll = $randomizer->getInt(1, 6);
$shuffled = $randomizer->shuffleArray(['spades', 'hearts', 'clubs', 'diamonds']);
$winner = $randomizer->pickArrayKeys($players, 1)[0];

Because the engine is injectable, you can pass a seeded engine in a test and get deterministic "random" results, then swap in a cryptographically secure engine in production. No more global state quietly deciding your dice rolls.

Constants in traits

Traits can now declare constants. Previously only properties and methods were allowed, which forced awkward workarounds when a trait's behavior depended on a fixed value. Now the constant lives with the code that uses it.

PHP
trait HasHttpStatus
{
    public const DEFAULT_STATUS = 200;

    public function statusOr(int $given): int
    {
        return $given ?: self::DEFAULT_STATUS;
    }
}

final class JsonResponse
{
    use HasHttpStatus;
}

echo JsonResponse::DEFAULT_STATUS; // 200

The constant is accessed through the using class, keeping the trait self-contained instead of leaning on the consumer to define the value.

Sensitive parameter redaction

Stack traces are great for debugging and terrible for secrets, because an exception thrown deep in a call stack can dump a password or API token straight into your logs. The #[SensitiveParameter] attribute tells PHP to redact that argument from traces.

PHP
function authenticate(
    string $username,
    #[SensitiveParameter] string $password,
): bool {
    throw new RuntimeException('auth service down');
}

When that exception bubbles up, the trace shows the username but replaces the password with Object(SensitiveParameterValue). It is a one-line change that closes a genuinely common leak, with no impact on how the function behaves.

PHP 8.2 is the kind of release that makes the previous year's features feel finished: readonly graduated from properties to whole classes, the type system filled in its gaps, and a couple of papercuts around traits and logging got fixed. Next, 8.3 keeps the momentum with typed constants and some sharp tooling additions.