PHP 8.2 and Readonly Classes
· Jerwin Arnado
Archive note: this is a backdated post, written years later while rebuilding this site. It’s dated to the moment it covers, but the hindsight is real.
Right on the annual schedule, PHP 8.2 shipped December 8. After 8.0’s modernization and 8.1’s enums, 8.2 is a consolidation release — smaller headline, but with one deprecation that will touch more old code than any feature. The working notes:
Readonly classes
8.1 gave us readonly properties; 8.2 lets you mark the whole class:
readonly class Money
{
public function __construct(
public int $amount,
public string $currency,
) {}
}
Every property is implicitly readonly — the value-object pattern now costs one keyword. DTOs, API response objects, config holders: declare intent at the class level and let the engine enforce it. Combined with promoted constructors, PHP’s immutability story went from “discipline and docblocks” to “two keywords” in three releases.
The sleeper: dynamic properties deprecated
This is the change that will actually page people. For PHP’s entire life, this worked:
$user = new User();
$user->nickname = 'jack'; // property that doesn't exist? sure, here you go
In 8.2 it raises a deprecation notice; in 9.0 it will be an error. And it’s the right call — silent dynamic properties are where typos go to become production bugs ($user->emial happily sets a new property instead of failing).
The relief valves: classes that genuinely need the behavior can declare #[\AllowDynamicProperties], anything extending stdClass keeps it, and — important for our world — Eloquent models are unaffected, since attribute access flows through __get/__set magic methods, which were never “dynamic properties” in this sense. Where you will see notices: old utility classes, quick-and-dirty value bags, and that one legacy library nobody has touched since 2016. Run the test suite with deprecations visible; budget an afternoon.
DNF types and standalone null/false/true
Disjunctive Normal Form types allow combining unions and intersections: (Countable&Traversable)|null. Most app code won’t write these, but the libraries underneath you now can, instead of lying in docblocks. Meanwhile null, false, and true become standalone types — so the half of PHP’s standard library that returns string|false can finally be honestly described by tooling. Static analysis gets smarter without you doing anything; that’s the best kind of feature.
The rest worth knowing
#[\SensitiveParameter]— parameters marked with it get redacted in stack traces. Passwords and API keys in trace logs have burned everyone at least once; annotate accordingly.- Constants in traits — small, occasionally exactly what you needed.
- A real
Randomextension — proper, injectable, seedable randomizer objects replacing the global-state functions; testable randomness without hacks. - Quiet deprecation honor roll: the
utf8_encode()/utf8_decode()functions (which never did what their names claimed) are finally on the way out.
Upgrade posture
Same cadence advice as February’s Laravel 9 notes, now in its settled rhythm: language in November-December, framework support lands fast (Laravel already runs on 8.2), ecosystem by Q1. The 8.1 → 8.2 jump is mild — the dynamic-properties audit is the only real work item for most codebases.
Quiet release, healthy language. The interesting question for next year’s edition of this post is whether the chat machine everyone’s playing with will be writing the migration scripts. Half-joking. Check back.