In PHP 8.4 and later, what does asymmetric visibility such as public private(set) do, and how does it differ from readonly?
answer
- separate scope for reading and writing
- set scope never wider than get
- typed properties only
- array appends count as writes
- private(set) implies final
basics
~20 sSince PHP 8.4, a property can have separate read and write visibility: public private(set) lets anyone read it but only the class write it, as often as it likes. readonly instead allows a single write ever.
solid answer
~40 sPHP 8.4 added **asymmetric visibility**: `public private(set) int $seatsLeft` is readable everywhere and writable only inside its class; `public protected(set)` also lets subclasses write. `private(set)` alone is shorthand for `public private(set)`. Unlike `readonly`, the owning scope can write the property **any number of times**, so it suits mutable state that must change only through the class's methods. Rules: the property must be **typed**, the set visibility cannot be wider than the read visibility (`private protected(set)` is rejected), a `private(set)` property is **implicitly final**, and taking a reference or modifying an array element counts as a **write**, so outside code calling `$b->legs[] = $leg` gets `Cannot indirectly modify private(set) property ...`. Readonly is effectively a write-once property with `protected(set)` since 8.4, and the two can be combined. PHP 8.5 extended set visibility to **static** properties.
code
php · 24 lines<?php
declare(strict_types=1);
final class Itinerary
{
public private(set) array $legs = [];
public function addLeg(string $flightNo): void
{
$this->legs[] = $flightNo; // allowed inside the class
}
}
$it = new Itinerary();
$it->addLeg('LH400');
try {
$it->legs[] = 'LH401'; // an append is a write
} catch (Error $e) {
echo $e->getMessage(), PHP_EOL;
// Cannot indirectly modify private(set) property Itinerary::$legs from global scope
}
echo count($it->legs), PHP_EOL; // 1go deeper
Recall that public private(set) means everyone can read the property but only its class can change it.
Explain the rules: typed only, set scope not wider than read, private(set) implies final, and array writes and references count as writes.
Choose between asymmetric visibility, readonly and getters for a class's state, and remember objects behind the property remain mutable.
Set guidance on exposing state through properties versus methods across teams, considering API stability when classes are extended.
## The feature Before PHP 8.4 a property had one visibility for both reading and writing. Exposing a value read-only meant a private property plus a getter method. **Asymmetric visibility** lets the declaration say it directly: ```php final class Flight { public function __construct( public private(set) int $seatsLeft, ) {} public function reserve(int $n): void { if ($n > $this->seatsLeft) { throw new DomainException('Not enough seats'); } $this->seatsLeft -= $n; // allowed: class scope } } ``` Outside code reads `$flight->seatsLeft` like any public property; `$flight->seatsLeft = 99` throws `Error: Cannot modify private(set) property Flight::$seatsLeft from global scope`. ## The rules - **Syntax**: `{read visibility} {set visibility}(set)`. When the read visibility is public it may be omitted: `private(set) int $x` equals `public private(set) int $x`. No spaces inside: `private( set )` is a parse error. - **Set is never wider than read**: `public protected(set)` and `protected private(set)` are fine; `private protected(set)` fails with `Visibility of property ... must not be weaker than set visibility`. - **Typed properties only**: an untyped property with a set visibility fails with `Property with asymmetric visibility ... must have type`. - **`private(set)` implies `final`**: a subclass cannot redeclare the property. - **References and array writes follow the set visibility**, because both can modify the value. Outside code cannot do `$f->legs[] = $leg` or `$r = &$f->legs` on a `private(set)` array: `Cannot indirectly modify private(set) property ...`. - **Statics**: PHP 8.5 allows set visibility on static properties too (`public private(set) static int $calls`). ## Compared with readonly | | `public private(set)` | `public readonly` | |---|---|---| | who may write | the class (or subclasses with `protected(set)`) | the class and subclasses (implicit `protected(set)` since 8.4) | | how many writes | any number | one initializing write | | default value allowed | yes | no | | property hooks allowed | yes | no | | typical use | encapsulated mutable state | immutable value objects | The two combine: `public private(set) readonly string $pnr` is write-once and initializable only by the declaring class. ## Compared with getters A getter method (`seatsLeft(): int`) gives the same read-only view but adds a method per field and a call syntax that differs from a property. Asymmetric visibility keeps property syntax and lets tools see a real typed property. When reading needs **logic**, not just access control, a property **hook** is the matching tool, and the two can be combined on one property. ## Replacing a getter A common refactoring in PHP 8.4 codebases: 1. Before: `private int $seatsLeft;` plus `public function getSeatsLeft(): int`. 2. After: `public private(set) int $seatsLeft;`, and callers read `$flight->seatsLeft`. 3. Keep mutators such as `reserve()` as methods, because they carry validation and intent. The change is visible to callers (a method call becomes a property read), so in a library it is an API change. Inside an application it removes a method per field without losing the write protection. If validation must run on every write, add a `set` hook to the same property instead of returning to setter methods. ## Pitfalls 1. Objects in a `private(set)` property are still mutable through their own methods; set visibility guards the slot, not the object. 2. A `public private(set) array` exposes the whole array for reading; callers get a copy, but large arrays are still exposed data. 3. Widening in subclasses is allowed where the parent is not final: a child may redeclare `public protected(set)` as `public`, which reopens writes. Use `private(set)` or `final` when that must not happen. ## Summary Asymmetric visibility separates who may read from who may write a typed property. It allows repeated writes from the owning scope, unlike readonly's single write, blocks outside array appends and references, and makes `private(set)` properties final.
- Why is a `private(set)` property automatically final?If a subclass could redeclare it, the child could widen the set visibility and write a value the parent class believes only it controls. Making `private(set)` imply `final` keeps the parent's write guarantee intact, so a child class cannot redeclare the property at all.
- Can outside code append to a `public private(set) array` property?No. Modifying an array element needs write access, so it follows the set visibility. The append throws `Cannot indirectly modify private(set) property ... from global scope`, while reading the whole array stays allowed. Expose a method such as `addLeg()` for controlled changes.
A museum display case works like public private(set): every visitor can look at what is inside, but only staff hold the key to rearrange or add items. A readonly property is instead a case sealed once at opening day that not even staff may reopen.
saying these in an interview costs you the question
- private(set) makes the property write-once, just like readonly.
- Asymmetric visibility works on untyped properties too.
- Outside code can still append to a public private(set) array.
- protected public(set) is valid and lets anyone write a protected property.
- Asymmetric visibility makes the object stored in the property immutable.