Visibility and Property Hooks
Encapsulation means an object controls its own data: outside code can only use the public operations it offers, so the object can never be put into an invalid state. PHP enforces this with visibility keywords, and PHP 8.4 added two powerful tools — asymmetric visibility and property hooks — that remove most of the boilerplate getters and setters used to need.
public, protected and private
| Keyword | Accessible from |
|---|---|
public | Anywhere |
protected | The class itself and classes that extend it (lesson 37) |
private | Only the class itself |
<?php class Account { private float $balance = 0; public function deposit(float $amount): void { if ($amount <= 0) { throw new InvalidArgumentException("Deposit must be positive"); } $this->balance += $amount; } public function getBalance(): float { return $this->balance; } } $a = new Account(); $a->deposit(50); echo $a->getBalance(), "\n"; $a->balance = 1_000_000;
50
Fatal error: Uncaught Error: Cannot access private property Account::$balance in main.php:24
Stack trace:
#0 {main}
thrown in main.php on line 24The only way to change the balance is deposit(), which validates the amount. That's encapsulation.
Asymmetric visibility (PHP 8.4)
A very common need is "readable by everyone, writable only by the class". Previously that required a private property plus a getter method. Now you can give reading and writing different visibility:
<?php class Account { public private(set) float $balance = 0; public private(set) array $history = []; public function deposit(float $amount): void { $this->balance += $amount; $this->history[] = "+$amount"; } } $a = new Account(); $a->deposit(100); $a->deposit(25); echo $a->balance, " ", implode(" ", $a->history), "\n"; $a->balance = 0;
125 +100 +25
Fatal error: Uncaught Error: Cannot modify private(set) property Account::$balance from global scope in main.php:19
Stack trace:
#0 {main}
thrown in main.php on line 19public private(set) reads as "public to read, private to set". protected(set) lets subclasses write too. It works with promoted constructor parameters as well: public function __construct(public private(set) string $status) {}.
Property hooks (PHP 8.4)
A property can have get and set hooks: code that runs when it's read or written. Outside code still uses plain property syntax, but the class stays in control:
<?php class User { public string $email { set(string $value) { $value = strtolower(trim($value)); if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException("Invalid email: $value"); } $this->email = $value; } } public function __construct(string $email) { $this->email = $email; // goes through the set hook } } $u = new User(" Ana.Lima@Example.COM "); echo $u->email, "\n"; $u->email = "not-an-email";
ana.lima@example.com
Fatal error: Uncaught InvalidArgumentException: Invalid email: not-an-email in main.php:9
Stack trace:
#0 main.php(23): User->$email::set('not-an-email')
#1 {main}
thrown in main.php on line 9Inside a hook, $this->email refers to the stored value itself, so assigning it doesn't call the hook again.
Computed properties
A property with only a get hook and no storage is virtual: it's calculated every time it's read, so it can never be out of date:
<?php class Rectangle { public function __construct(public float $width, public float $height) {} public float $area { get => $this->width * $this->height; } public bool $isSquare { get => $this->width === $this->height; } } $r = new Rectangle(4, 3); echo $r->area, " ", var_export($r->isSquare, true), "\n"; $r->width = 3; echo $r->area, " ", var_export($r->isSquare, true), "\n";
12 false 9 true
Hooks with short syntax
A set hook can be written as an arrow that returns the value to store — handy for normalising input:
<?php class Product { public string $name { set => ucfirst(trim($value)); } public float $price { set => max(0, round($value, 2)); } } $p = new Product(); $p->name = " desk lamp "; $p->price = -5; echo "[{$p->name}] {$p->price}\n"; $p->price = 19.999; echo $p->price, "\n";
[Desk lamp] 0 20
In the short form, $value is the incoming value and the expression's result is what gets stored.
Getters and setters in older code
Before PHP 8.4, the standard pattern was a private property with getX() and setX() methods. You'll see it everywhere in existing code, and it still works perfectly well:
<?php class Thermostat { private float $target = 20; public function getTarget(): float { return $this->target; } public function setTarget(float $target): void { $this->target = max(5, min(30, $target)); } } $t = new Thermostat(); $t->setTarget(35); echo $t->getTarget(), "\n";
30
Design guidelines
- Start with everything
private; widen visibility only when needed. - Prefer methods that express intent —
deposit(),ship(),cancel()— over setters that let callers poke at state. - Use
public private(set)for state the outside world may read but not change. - Use hooks for validation and normalisation that must always happen, and virtual properties for derived values.
- For values that never change after construction, use
readonly(lesson 41).
Try it yourself
Add a percentage virtual property to ProgressBar, and keep current between 0 and max with a set hook.
[#####...............] 25% [####################] 100%