Property hooks дозволяють оголосити логіку читання й запису прямо на властивості. Замість пари getName()/setName() клас має public string $name { get => ...; set => ...; }, а зовнішній код працює зі звичайним доступом до поля. Обійти сеттер неможливо: будь-яке присвоєння, зокрема в промоутнутому конструкторі, проходить через хук, тому інваріант виконується завжди.
На відміну від __get/__set хуки статично типізовані й привʼязані до конкретної властивості. IDE підказує тип, PHPStan перевіряє тіло хука, а рантайм не платить за виклик магічного методу на кожне звернення. Якщо хук get не читає саму властивість, вона стає віртуальною й не займає памʼяті, як обчислюване fullName.
Інтерфейси теж отримали цю можливість: public string $label { get; } у контракті замінює метод getLabel(). Клас може реалізувати вимогу і хуком, і звичайною публічною властивістю.
Практична порада: не переписувати всі геттери на хуки заради стилю. Найбільшу користь дають місця, де інваріант уже порушувався через прямий запис, і value objects, де валідація має жити поруч із даними.
final class Product
{
public function __construct(
public string $name {
set(string $value) {
if (trim($value) === '') {
throw new InvalidArgumentException('Name cannot be empty.');
}
$this->name = trim($value);
}
},
public int $priceCents { set => max(0, $value); },
) {}
// Віртуальна властивість: памʼять не виділяється, значення обчислюється
public string $label { get => $this->name.' — '.number_format($this->priceCents / 100, 2).' грн'; }
}
interface HasLabel
{
public string $label { get; } // інтерфейс вимагає властивість, а не метод getLabel()
}
$p = new Product(' Кава ', 1990);
$p->name = ''; // InvalidArgumentException: сеттер неможливо обійти
echo $p->label; // Кава — 19.90 грн
Згадайте, що масова міграція геттерів на hooks заради стилю не окупається — переписують те, де вже болять інваріанти. І назвіть другу фічу 8.4 поруч: asymmetric visibility.