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

KeywordAccessible from
publicAnywhere
protectedThe class itself and classes that extend it (lesson 37)
privateOnly the class itself
PHP
<?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;
Output
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 24

The 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
<?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;
Output
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 19

public 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
<?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";
Output
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 9

Inside 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
<?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";
Output
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
<?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";
Output
[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
<?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";
Output
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.

Try it yourself · main.php
Edit the code, copy it into main.php, then run it with php main.php (PHP 8.5). How to install PHP
Output
[#####...............] 25%
[####################] 100%