PHP asymmetric property visibility lets you control who can read a property separately from who can change it. Added in PHP 8.4 for object properties, the feature can expose a value publicly while keeping writes inside the class or its child classes.
This design removes many simple getter methods without turning important state into a freely writable public property.
How Asymmetric Visibility Works
A normal property uses one visibility rule for both reading and writing. An asymmetric property adds a second modifier ending in (set). The first modifier controls read access, and the second controls write access.
Syntax:
<?php
class Product
{
// Anyone can read the name, but only this class can change it
public private(set) string $name;
}
?>
You can omit public when the read visibility is public. Therefore, private(set) string $name has the same access rules as public private(set) string $name.
| Declaration | Read access | Write access |
|---|---|---|
| public private(set) | Everywhere | Declaring class only |
| public protected(set) | Everywhere | Declaring class and child classes |
| protected private(set) | Class family | Declaring class only |
Requirement: Separate set visibility requires PHP 8.4 or later and a typed object property. PHP 8.5 also permits it on static properties.
Create a Publicly Readable Property
The most common pattern is a public value that external code may inspect but cannot overwrite.
Example:
<?php
class Wallet
{
public private(set) int $balance;
public function __construct(int $openingBalance)
{
// The declaring class may write the property
$this->balance = $openingBalance;
}
public function deposit(int $amount): void
{
// Keep the validation beside the state change
if ($amount <= 0) {
throw new InvalidArgumentException("Amount must be positive");
}
$this->balance += $amount;
}
}
$wallet = new Wallet(1000);
$wallet->deposit(250);
// Reading from outside the class is allowed
echo $wallet->balance;
?>
Output:
1250
An external assignment such as $wallet->balance = 0; throws an error because the setter is private. Callers must use the class method, so the validation cannot be bypassed.
Allow Child Classes to Write
Use protected(set) when a child class needs to update the property. External code can still read a public property but cannot change it directly.
Example:
<?php
class Employee
{
public protected(set) string $department;
public function __construct(string $department)
{
// The parent class sets the initial value
$this->department = $department;
}
}
class TeamLead extends Employee
{
public function transferTo(string $department): void
{
// A child class may write a protected(set) property
$this->department = $department;
}
}
$lead = new TeamLead("Support");
$lead->transferTo("Engineering");
echo $lead->department;
?>
Output:
Engineering
Use Constructor Property Promotion
You can apply asymmetric visibility to a promoted constructor property. This keeps a small value object concise while protecting later writes.
Example:
<?php
class Order
{
public function __construct(
// The ID is readable after construction but cannot be replaced outside
public private(set) string $id,
public protected(set) string $status = "pending"
) {
}
public function confirm(): void
{
// The class controls valid status changes
$this->status = "confirmed";
}
}
$order = new Order("ORD-1042");
$order->confirm();
echo $order->id . ": " . $order->status;
?>
Output:
ORD-1042: confirmed
Follow the Visibility Rules
- The set visibility must be the same as or more restrictive than the read visibility.
public protected(set)is valid, butprotected public(set)is invalid.- Write
private(set)without spaces inside the parentheses. - A
private(set)property is implicitly final and cannot be redeclared in a child class. - Taking a reference to a property follows its set visibility because a reference could modify the value.
- Changing an element inside an array property also counts as a write and follows set visibility.
Invalid access:
<?php
$order = new Order("ORD-1042");
// This write occurs outside the permitted scope
$order->status = "cancelled";
?>
Output:
Fatal error: Cannot modify protected(set) property Order::$status
Asymmetric Visibility vs Readonly
A readonly property can normally be initialized once and then cannot change. An asymmetric property can change many times, but only from an allowed scope. Choose readonly for immutable state. Choose asymmetric visibility when the object must update its own state while callers receive read access.
Best Practices
- Use descriptive methods for changes that require validation or trigger other work.
- Prefer
private(set)unless child classes have a clear reason to write the property. - Do not expose mutable arrays merely to avoid a method; array element changes still need write permission.
- Document the PHP version requirement when publishing a reusable package.
- Test both valid internal updates and rejected external assignments.
Conclusion
PHP asymmetric property visibility separates read access from write access. With private(set) and protected(set), you can publish useful state while keeping updates under class control. The result is a smaller API, fewer boilerplate getters, and clearer object boundaries.