Getters are not dead. A lot of them just stopped earning their keep.
PHP 8.4 shipped property hooks and asymmetric visibility. It shortens classes where you have repeated the same shape for years: private field, public getter, setter with trim and validation.
If you are still on 8.3, these two features are often enough reason to upgrade.
Property hooks, briefly
A hook attaches logic to reading or writing a property. IDEs and static analysis see a property, not a pair of magic methods hiding behind __get.
final class Email
{
public string $value {
set(string $value) {
$normalized = strtolower(trim($value));
if (!filter_var($normalized, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email');
}
$this->value = $normalized;
}
}
public function __construct(string $value)
{
$this->value = $value;
}
}
Normalization stays next to the field. Callers write $email->value, and the domain rule does not leak into an application service.
Asymmetric visibility without boilerplate
Want a property that is publicly readable but only writable inside the class? In 8.4 you do not need a getter just for that.
final class Order
{
public function __construct(
public private(set) string $id,
public private(set) string $status,
) {}
public function markPaid(): void
{
$this->status = 'paid';
}
}
From the outside you read $order->status. Status changes go through a method that knows the transition rules. The code looks like data, while behavior stays enclosed.
Where hooks help, and where they get in the way
They fit value objects, DTOs with light normalization, and models where a getter only returns a field.
They age badly when:
settalks to a database or HTTP (hard to test, hard to read),- the property is an array and you expect full reference semantics,
- the team is not on PHP 8.4 everywhere yet, and polyfills cannot recreate language syntax anyway.
A hook is not for orchestration. It is for invariants and simple value transforms.
Migrate without a big bang
Do not rewrite the whole model over a weekend. Start with new classes and with getters/setters that only add noise. In Symfony and Doctrine, check that hydration and proxies do not assume the old API shape. With serializers (symfony/serializer, JMS), confirm public hooked properties behave the way your tests expect.
Write one shorter value object and keep the rule closer to the data. The rest usually follows.