Six lines of getter and setter ceremony around one strtolower. PHP 8.4 is in release candidates, and property hooks delete exactly that:
class Customer
{
public string $email {
set => strtolower(trim($value));
}
}
Assignment stays assignment, $customer->email = $input, and normalization happens on the way in. For this exact case, one value, no dependencies, I am convinced. The property remains a property.
Where I get careful is everything past normalization. A setter method is ugly, but it announces itself. setEmail() in a call stack tells you code ran. $obj->email = $x looks free, and with hooks it is not. Put validation that throws into a hook, and every plain assignment becomes a possible exception, invisible at the call site. Put a side effect in, and you have built magic the next person discovers through a debugger at midnight. We spent years removing __get and __set from codebases for this exact crime. Hooks are their respectable cousins, better typed, analyzable, and the temptation is identical.
My other question is ORM hydration. Doctrine and friends write properties through reflection, bypassing whatever the class thinks. Whether hooks fire during hydration, and whether you want them to, will produce subtle bugs and long GitHub threads. No hooks on entities from me until the libraries say something official.
So the boundary I will try to hold: a hook may compute or normalize a value. The moment the logic wants a dependency, can fail, or changes anything outside the object, it is an action, and actions deserve a verb. recalculateTotal() is honest. A property that secretly recalculates is not.
If you would not put it in a plain getter in 2015, do not put it in a hook in 2024. I wrote plenty of bad getters in 2015.