Property Hooks and Asymmetric Visibility
A property hook lets a property run code whenever it is read (get) or written (set), without needing a separate getter/setter method or an underlying private property. For a simple one-expression transformation, use the short arrow form. Inside set, $value is the incoming value being assigned; the arrow form’s expression becomes the value that gets stored.
<?php
class User {
public string $email = '' {
get => strtoupper($this->email);
set => strtolower($value);
}
}
$u = new User();
$u->email = 'JOHN@EXAMPLE.COM'; // stored lowercased by the set hook
echo $u->email, "\n"; // read back uppercased by the get hook
?>
<?php
class User {
public string $email = '' {
get => strtoupper($this->email);
set => strtolower($value);
}
}
$u = new User();
$u->email = 'JOHN@EXAMPLE.COM'; // stored lowercased by the set hook
echo $u->email, "\n"; // read back uppercased by the get hook
?>
JOHN@EXAMPLE.COM
For anything more than a single expression, use the full block-body form, with an explicit $this->propName = … assignment inside set and a return inside get. Inside either hook, referring to the property’s own name (eg. $this->name inside the hooks of $name) accesses the raw stored value directly, bypassing the hooks — so hooks can freely read and update their own backing storage without recursing into themselves.
<?php
class Person {
public string $name {
get {
return strtoupper($this->name);
}
set (string $value) {
$this->name = ucfirst(strtolower($value));
}
}
}
$p = new Person();
$p->name = 'jOHN';
echo $p->name, "\n";
?>
<?php
class Person {
public string $name {
get {
return strtoupper($this->name);
}
set (string $value) {
$this->name = ucfirst(strtolower($value));
}
}
}
$p = new Person();
$p->name = 'jOHN';
echo $p->name, "\n";
?>
JOHN
A property hooked with only a get (no backing storage, and no set) is a virtual property — it is computed on the fly from other properties, and does not occupy any storage of its own.
<?php
class Circle {
public function __construct(public float $radius) {
}
public float $area {
get => M_PI * $this->radius ** 2; // computed, never stored
}
}
$c = new Circle(2);
echo round($c->area, 2), "\n";
?>
<?php
class Circle {
public function __construct(public float $radius) {
}
public float $area {
get => M_PI * $this->radius ** 2; // computed, never stored
}
}
$c = new Circle(2);
echo round($c->area, 2), "\n";
?>
12.57
PHP 8.4 also adds asymmetric visibility: a property can declare a different (more restrictive) visibility for writing than for reading, eg. public private(set) or public protected(set). The property reads as public, but only code inside the declaring class (or, for protected(set), its subclasses) may write to it. Since PHP 8.5, static properties can be asymmetrically visible too.
<?php
class Account {
public private(set) string $status = 'active'; // publicly readable, privately writable
public function suspend(): void {
$this->status = 'suspended'; // allowed: write from inside the class
}
}
class Counter {
public static private(set) int $count = 0; // PHP 8.5: asymmetric visibility on a static property
public static function increment(): void {
self::$count++;
}
}
$a = new Account();
$a->suspend();
echo $a->status, "\n";
try {
$a->status = 'hacked'; // write from outside the class
} catch (\Error $e) {
echo $e->getMessage(), "\n";
}
Counter::increment();
Counter::increment();
echo Counter::$count, "\n";
try {
Counter::$count = 100; // write from outside the class
} catch (\Error $e) {
echo $e->getMessage(), "\n";
}
?>
<?php
class Account {
public private(set) string $status = 'active'; // publicly readable, privately writable
public function suspend(): void {
$this->status = 'suspended'; // allowed: write from inside the class
}
}
class Counter {
public static private(set) int $count = 0; // PHP 8.5: asymmetric visibility on a static property
public static function increment(): void {
self::$count++;
}
}
$a = new Account();
$a->suspend();
echo $a->status, "\n";
try {
$a->status = 'hacked'; // write from outside the class
} catch (\Error $e) {
echo $e->getMessage(), "\n";
}
Counter::increment();
Counter::increment();
echo Counter::$count, "\n";
try {
Counter::$count = 100; // write from outside the class
} catch (\Error $e) {
echo $e->getMessage(), "\n";
}
?>
suspended
Cannot modify private(set) property Account::$status from global scope
2
Cannot modify private(set) property Counter::$count from global scope
Property hooks and asymmetric visibility solve similar problems — controlling how a property is read and written — and are often combined: eg. a public private(set) property with a get hook that formats its value for display.