Home: Roshan Kc

← All posts

PHP · Language 8 min read

Property Hooks and Asymmetric Visibility: PHP 8.4 Changed How You Write Classes

Direct answer: PHP 8.4 (November 2024) added property hooks, which put get and set logic directly on a property, and asymmetric visibility, such as public private(set), which makes a property readable everywhere but writable only inside the class. Together they replace most getter/setter boilerplate. 8.4 also brought lazy objects, a standards-compliant HTML5 parser, array_find() and friends, and new without extra parentheses.

Property hooks are the biggest change to how PHP classes are written since constructor promotion in PHP 8.0. This post shows the before and after, and where hooks do and don't belong. It's part of my PHP in 2026 series.

The problem: getters and setters everywhere#

For years the "correct" PHP class looked like this. Private properties, plus a method pair for each one, just in case validation was needed someday:

class User
{
    private string $email;

    public function getEmail(): string
    {
        return $this->email;
    }

    public function setEmail(string $email): void
    {
        $this->email = strtolower(trim($email));
    }
}

$user->setEmail(' Roshan@Example.com ');
echo $user->getEmail();

Property hooks#

With 8.4, the logic lives on the property itself, and callers just use the property:

class User
{
    public string $email {
        set => strtolower(trim($value));
    }
}

$user->email = ' Roshan@Example.com ';
echo $user->email; // "roshan@example.com"

The short set => form assigns the result of the expression to the property. A full block lets you validate:

class Product
{
    public int $priceInCents {
        set {
            if ($value < 0) {
                throw new InvalidArgumentException('Price cannot be negative');
            }
            $this->priceInCents = $value;
        }
    }
}

Virtual (computed) properties#

A property with only a get hook that never touches its own value is virtual. It takes no storage and is computed on every read:

class Person
{
    public function __construct(
        public string $firstName,
        public string $lastName,
    ) {}

    public string $fullName {
        get => "{$this->firstName} {$this->lastName}";
    }
}

Hooks in interfaces#

Interfaces can now require properties, not just methods:

interface HasSlug
{
    public string $slug { get; }
}

Any class can satisfy that with a plain public property, a readonly property, or a get hook. Your public API stops depending on how the value is stored.

Asymmetric visibility#

Often you don't need logic, you just need "anyone can read, only I can write". Before 8.4 that meant a private property plus a getter. Now it's one declaration:

class Order
{
    public private(set) string $status = 'pending';

    public function markPaid(): void
    {
        $this->status = 'paid';
    }
}

echo $order->status;      // fine
$order->status = 'paid';  // Error: cannot modify private(set) property

protected(set) works the same way for subclasses, and it combines with constructor promotion:

public function __construct(
    public private(set) int $id,
) {}

readonly vs private(set): readonly means "written once, never again". private(set) means "only this class may write, as often as it needs". Use readonly for value objects and private(set) for entities whose state changes through methods. PHP 8.5 extended asymmetric visibility to static properties too.

When not to use hooks#

  • Heavy logic. A set hook that calls the database or sends emails is a method in disguise. Make it a method so the cost is visible at the call site.
  • Eloquent models. Laravel model attributes are magic (__get/__set), not real properties, so keep using Eloquent's accessors, mutators and casts there. Hooks shine on DTOs, value objects, service classes and plain domain models.
  • Surprising side effects. If reading a property can throw or change state, callers will be surprised. Keep hooks small and predictable.

The rest of PHP 8.4 worth knowing#

FeatureExample / why it matters
array_find(), array_find_key(), array_any(), array_all()array_find($users, fn($u) => $u->isAdmin()) replaces many loops with an early break
new without parenthesesnew Request()->withHeader(...) instead of (new Request())->withHeader(...)
Lazy objectsReflectionClass::newLazyGhost() / newLazyProxy() defer expensive construction until first use. Built for DI containers and ORMs.
HTML5 DOM parserDom\HTMLDocument::createFromString($html) parses HTML the way browsers do, with querySelector() support
#[\Deprecated]Mark your own functions and methods as deprecated and PHP emits proper deprecation notices
BcMath\NumberArbitrary-precision numbers as objects with normal operators. Good for money calculations.
mb_trim(), mb_ucfirst(), mb_lcfirst()Multibyte-safe versions of long-missing string functions

One deprecation to fix: implicitly nullable parameters. function find(User $user = null) now triggers a notice. Write ?User $user = null instead. Rector fixes this automatically across a codebase; see the upgrade playbook.

Key takeaways#

  • Property hooks put get/set logic on the property itself, replacing most getter/setter methods.
  • Get-only hooks create virtual computed properties, and interfaces can now declare properties.
  • Asymmetric visibility (public private(set)) gives read-public, write-private properties without a getter.
  • Keep hooks small. Don't use them on Eloquent models or for logic with heavy side effects.
  • PHP 8.4 gets active support until December 31, 2026 and security fixes until December 31, 2028.

Sources#