PHP 8.4 introduces asymmetric visibility — the ability to declare different access levels for reading and writing a property using the public private(set) / protected private(set) syntax. This replaces the common pattern of a public getter method plus a private property, making class definitions more concise while maintaining encapsulation. It is especially useful in WordPress plugin value objects and entities where external code should be able to read but never directly write a property.
Problem: PHP class properties are either fully public (readable and writable by any code) or fully private (inaccessible from outside the class) — there is no native way to make a property publicly readable but only privately writable without verbose getter methods.
Solution: Use PHP 8.4 Asymmetric Visibility — declare a property with separate read and write visibility: public private(set) string $status makes the property readable from anywhere but writable only within the class. Use public protected(set) to allow subclasses to write. This replaces the getter-only pattern without boilerplate methods.
The examples below show the syntax for asymmetric visibility, how it compares to the old getter pattern, a WordPress post entity using it, and how it combines with constructor promotion and readonly.
count++; // OK — internal write
}
}
$c = new Counter();
echo $c->count; // 0 — public read works
$c->increment();
echo $c->count; // 1
// $c->count = 5; // Fatal: Cannot modify private(set) property from outside
// ── 2. Old pattern vs new ─────────────────────────────────────────────────
// Before (PHP 8.3 and earlier):
class PostOld {
private int $id;
private string $title;
public function __construct( int $id, string $title ) {
$this->id = $id;
$this->title = $title;
}
public function getId(): int { return $this->id; }
public function getTitle(): string { return $this->title; }
}
// After (PHP 8.4 asymmetric visibility):
class Post {
public function __construct(
public private(set) int $id,
public private(set) string $title,
public private(set) string $status = 'draft',
) {}
public function publish(): void {
$this->status = 'publish'; // internal write — allowed
}
}
$post = new Post( 42, 'Hello World' );
echo $post->id; // 42 — readable
echo $post->title; // "Hello World"
// $post->title = 'x'; // Fatal
// ── 3. WordPress entity with asymmetric visibility ────────────────────────
class WPPostEntity {
public private(set) int $id;
public private(set) string $title;
public private(set) string $status;
public private(set) array $meta = [];
private function __construct( WP_Post $post ) {
$this->id = $post->ID;
$this->title = $post->post_title;
$this->status = $post->post_status;
$this->meta = get_post_meta( $post->ID );
}
public static function find( int $id ): ?self {
$post = get_post( $id );
return $post instanceof WP_Post ? new self( $post ) : null;
}
public function updateTitle( string $title ): void {
$this->title = sanitize_text_field( $title );
wp_update_post( [ 'ID' => $this->id, 'post_title' => $this->title ] );
}
}
$entity = WPPostEntity::find( 42 );
echo $entity?->title; // readable
// $entity->title = 'hack'; // Fatal — asymmetric visibility enforced
NOTE: Asymmetric visibility only controls write access from outside the class; subclasses can write protected(set) properties but not private(set) ones — choose protected(set) if you intend for child classes to be able to update the property internally.