Asymmetric Visibility (PHP 8.4)

Ma Mariya Vinciya Updated 21 Oct 2026
5 min read ·Lesson 171 of 174
In this post

What Is Asymmetric Visibility? In simple terms: Often, everyone should be able to read a property, but only the class itself should be able to change it. An order's status, for example, should be visi

What Is Asymmetric Visibility?

In simple terms: Often, everyone should be able to read a property, but only the class itself should be able to change it. An order's status, for example, should be visible everywhere, but it should change only through methods like pay() and ship() that enforce the rules. Asymmetric visibility lets you declare exactly that: public to read, private to write.

Technically: Since PHP 8.4, a property can have separate visibility for reading and writing. The write ("set") visibility is written in parentheses: public private(set) string $status. The first modifier controls reading; private(set) or protected(set) controls writing. The set visibility may not be wider than the read visibility.

The Problem It Solves

Before PHP 8.4Drawback
Private property + public getter methodBoilerplate: a getter for every readable property
public readonly propertyCan be set only once, so the class itself cannot update it later
Public propertyAnyone can change it to anything

 

With asymmetric visibility, the property is readable like a public property, changeable as often as the class needs, and protected from outside modification.


Syntax

public private(set) string $status;      // read: public,  write: private
public protected(set) int $count;        // read: public,  write: this class and subclasses
protected private(set) array $cache;     // read: protected, write: private
private(set) string $name;               // shorthand: read is public

 


A First Example: An Order Status

<?php
final class Order
{
    public private(set) string $status = 'pending';
    public private(set) array $history = [];

    public function __construct(public readonly int $id) {}

    public function pay(): void
    {
        $this->transition('pending', 'paid');
    }

    public function ship(): void
    {
        $this->transition('paid', 'shipped');
    }

    private function transition(string $from, string $to): void
    {
        if ($this->status !== $from) {
            throw new LogicException("Order {$this->id} cannot go from {$this->status} to $to.");
        }

        $this->history[] = "{$this->status} → $to";
        $this->status = $to;
    }
}

$order = new Order(501);
echo "Status: {$order->status}\n";

$order->pay();
$order->ship();

echo "Status: {$order->status}\n";
echo implode(', ', $order->history) . "\n";

$attempts = [
    'assign status' => fn() => $order->status = 'delivered',
    'append history' => fn() => $order->history[] = 'hacked',
];

foreach ($attempts as $label => $attempt) {
    try {
        $attempt();
    } catch (Error $e) {
        echo "$label: " . $e->getMessage() . "\n";
    }
}

 

Output

Status: pending
Status: shipped
pending → paid, paid → shipped
assign status: Cannot modify private(set) property Order::$status from global scope
append history: Cannot modify private(set) property Order::$history from global scope

 

How It Works

  1. public private(set) makes status and history readable everywhere — $order->status works in templates and API responses without getter methods.
  2. Only code inside Order may write them. The class changes the status repeatedly (pending → paid → shipped), which a readonly property could not do.
  3. The only way to change the status is through methods that enforce the allowed transitions and record the history.
  4. Outside writes are rejected — including indirect modifications such as appending to an array property.

protected(set): Writable by Subclasses

<?php
abstract class Counter
{
    public protected(set) int $count = 0;

    abstract public function record(): void;
}

final class DownloadCounter extends Counter
{
    public function record(): void
    {
        $this->count++;   // allowed: subclass
    }
}

$downloads = new DownloadCounter();
$downloads->record();
$downloads->record();
$downloads->record();

echo "Downloads: {$downloads->count}\n";

try {
    $downloads->count = 1000;
} catch (Error $e) {
    echo $e->getMessage() . "\n";
}

 

Output

Downloads: 3
Cannot modify protected(set) property Counter::$count from global scope

 

protected(set) lets child classes update the value while still preventing outside code from doing so. The error message names Counter, the class that declares the property.


Constructor Promotion

Asymmetric visibility works with promoted constructor parameters:

<?php
final class Customer
{
    public function __construct(
        public private(set) string $name,
        public private(set) string $email,
    ) {}

    public function changeEmail(string $email): void
    {
        if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
            throw new InvalidArgumentException("Invalid email: $email");
        }

        $this->email = strtolower($email);
    }
}

$customer = new Customer('Asha', 'asha@example.com');
$customer->changeEmail('Asha.K@GoCourse.in');

echo "{$customer->name} <{$customer->email}>\n";

 

Output

Asha <asha.k@gocourse.in>

 

Callers read $customer->email directly, but changing it must go through changeEmail(), which validates the new address.


Combining With Property Hooks

Asymmetric visibility controls who may write; property hooks control what happens on write. Together they cover many cases elegantly:

public private(set) string $sku {
    set => strtoupper(trim($value));
}

 

The SKU is publicly readable, writable only inside the class, and always normalized when written (see Property Hooks (PHP 8.4)).


Asymmetric Visibility vs Readonly

public readonlypublic private(set)
Readable from outsideYesYes
Writable from outsideNoNo
Can the class change it later?No — initialized onceYes, as often as needed
Can be combined with property hooksNoYes
Typical useImmutable values (IDs, value objects)State that changes through controlled methods (status, counters)

 

Since PHP 8.4, readonly properties are implicitly protected(set), which lets child classes initialize them; they still can only be initialized once.


Rules

RuleDetail
The property must have a typeUntyped properties cannot use asymmetric visibility
Set visibility cannot be wider than read visibilityprivate public(set) is invalid
Shorthandprivate(set) string $x means public private(set)
Static propertiesSupported for static properties from PHP 8.5
private(set) implies finalSubclasses cannot redeclare a private(set) property, because they could not write it anyway

 


Common Mistakes

Using readonly for Values the Class Must Update

Effect: Cannot modify readonly property when the class tries to change the value later.

Fix: Use public private(set).

Expecting Arrays to Be Modifiable From Outside

Effect: Appending or changing elements from outside throws an error.

Fix: Provide methods such as addItem().

Exposing Writes Through Methods Without Checks

Effect: A setStatus(string $s) method that accepts anything defeats the purpose.

Fix: Write intention-revealing methods (pay(), ship()) that enforce the rules.

Forgetting the Type

Effect: Compile error.

Fix: Declare a type for every property with asymmetric visibility.


Best Practices

  • Use public private(set) for state that others may read but only the object may change.
  • Use protected(set) when subclasses must update the value.
  • Change such properties only through methods that express business actions and enforce rules.
  • Prefer public readonly for values that never change after construction.
  • Combine asymmetric visibility with set hooks for normalization and validation.

Official reference: https\://www.php.net/manual/en/language.oop5.visibility.php#language.oop5.visibility-members-aviz

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else User-defined Functions Slicing Strings PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators AJAX PHP AJAX Database AJAX XML AJAX Live Search AJAX Poll AJAX with JSON Security Introduction Password Hashing SQL Injection Prevention XSS Prevention Garbage Collection Project: Simple Blog PHP What is OOP PHP Classes/Objects The $this Keyword Access Array Items Update Array Items Add Array Items Remove Array Items Sorting Arrays Multidimensional Arrays Array Destructuring Array Spread Operator Search in Arrays Merge Arrays PHP Abstract Classes PHP Interfaces Polymorphism PHP Traits PHP Static Methods PHP Static Properties Late Static Binding Magic Methods Object Cloning MySQL Database MySQL Connect MySQL Create DB MySQL Create Table MySQL Insert Data MySQL Get Last ID MySQL Insert Multiple MySQL Prepared MySQL Select Data MySQL Where Debugging PHP Project: Image Upload Gallery $_SESSION $_ENV PHP RegEx RegEx Patterns Heredoc and Nowdoc RegEx Modifiers preg_match preg_replace preg_split PHP RegEx Functions Nullable Types Mixed and Never Types Nullsafe Operator PHP Attributes PHP Fibers Readonly Classes Property Hooks (PHP 8.4) Asymmetric Visibility (PHP 8.4) Reflection API SPL Data Structures Composer
Ask about this post
AI Ask about this post

Ask questions about Asymmetric Visibility (PHP 8.4) and get answers drawn from it.

Signed-in readers only.