Asymmetric Visibility (PHP 8.4)
debt(d5/e3/b3/t5)
Closest to 'specialist tool catches it' (d5). The detection_hints list rector and phpstan as the tools, both specialist static-analysis tools rather than the default compiler or linter. The common mistakes (confusing with readonly, using static properties) are caught by phpstan type-checking or rector rules, not by a default linter or compile-time error.
Closest to 'simple parameterised fix' (e3). The quick_fix is 'Use public private(set) or protected(set)' — essentially replacing a private property + public getter pair with an asymmetric visibility declaration. This is a small, localised refactor within one class/component, not a single-line swap but not cross-cutting either.
Closest to 'localised tax' (b3). Asymmetric visibility applies within the declaring class and its subclasses. It replaces a well-understood pattern (private property + public getter), so the structural burden is localised. The rest of the codebase is largely unaffected unless the property is inherited widely.
Closest to 'notable trap' (t5). The canonical misconception is conflating asymmetric visibility with readonly — the misconception field states exactly this. A competent PHP developer familiar with readonly (introduced in 8.1) will likely assume the two are equivalent, but asymmetric visibility allows internal mutation after construction. This is a documented gotcha (t5) but not so severe as to contradict an entire paradigm.
Also Known As
TL;DR
Explanation
Asymmetric visibility uses the syntax: public private(set) string $name — public read access, private write access. Shorthand: public private(set) (long form) or public(get) private(set). Available modifiers: public, protected, private for both get and set independently. This replaces the common pattern of private property + public getter + no setter (enforced readonly) or private property + public getter + private setter. Works naturally with constructor promotion.
Common Misconception
Why It Matters
Common Mistakes
- Confusing with readonly — readonly cannot be written after construction; asymmetric visibility allows internal mutation.
- Not knowing that set visibility must be equal or more restrictive than get visibility.
- Using the long form public(get) private(set) when public private(set) is the shorthand.
- Asymmetric visibility on static properties — not supported in PHP 8.4.
Code Examples
// PHP 8.3 boilerplate — private + getter:
class User {
private string $name;
private int $loginCount = 0;
public function __construct(string $name) {
$this->name = $name;
}
public function getName(): string { return $this->name; }
public function getLoginCount(): int { return $this->loginCount; }
public function recordLogin(): void { $this->loginCount++; }
}
// PHP 8.4 asymmetric visibility:
class User {
public private(set) int $loginCount = 0;
public function __construct(
public private(set) string $name,
) {}
public function recordLogin(): void {
$this->loginCount++;
}
}
$user = new User('Alice');
echo $user->name;
echo $user->loginCount;
$user->name = 'Bob'; // Error