PHP 8.4 Property Hooks: Alles wat je moet weten
Property hooks zijn de grootste taalwijziging van PHP 8.4, uitgebracht op 21 november 2024. Ze lossen een probleem op waar vrijwel elke PHP-codebase mee worstelt: getters en setters die alleen bestaan omdat je misschien ooit logica nodig hebt bij het lezen of schrijven van een property. In dit artikel lopen we door hoe hooks werken, wanneer je ze gebruikt, en waar de valkuilen zitten.
Het probleem dat property hooks oplossen
Tot PHP 8.4 had je twee smaken. Je maakte een property public, en dan kon iedereen er alles in zetten. Of je maakte hem private en schreef er een getX() en setX() omheen. Die tweede variant is veiliger, maar levert enorme hoeveelheden boilerplate op. En omdat je achteraf niet makkelijk van een publieke property naar een methode kunt gaan zonder alle aanroepende code aan te passen, schreven de meeste teams uit voorzorg overal getters en setters.
Property hooks breken die keuze open. Een property kan publiek blijven, terwijl je toch logica uitvoert op het moment dat hij gelezen of geschreven wordt. De aanroepende code merkt er niets van: $user->email blijft $user->email.
De basis: een set-hook
Een hook staat tussen accolades direct achter de property-declaratie. De meest gebruikte is de set-hook, waarmee je een waarde valideert of normaliseert voordat hij wordt opgeslagen:
final class User
{
public string $email {
set(string $value) {
if (! filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Ongeldig e-mailadres: {$value}");
}
$this->email = strtolower($value);
}
}
}
$user = new User();
$user->email = 'Jan@Example.NL';
echo $user->email; // jan@example.nl
Binnen de hook verwijst $this->email naar de onderliggende opslag van de property zelf. Je komt dus niet in een oneindige lus terecht, zoals bij __set() wel kon gebeuren.
Het type en de naam van de parameter mag je weglaten. Dan heet hij automatisch $value en krijgt hij het type van de property:
public string $naam {
set {
$this->naam = trim($value);
}
}
Get-hooks en virtuele properties
Een get-hook bepaalt wat er teruggegeven wordt bij het lezen. Voor korte expressies is er een pijlnotatie:
final class Temperatuur
{
public function __construct(
public float $celsius,
) {}
public float $fahrenheit {
get => $this->celsius * 9 / 5 + 32;
}
}
$t = new Temperatuur(21.5);
echo $t->fahrenheit; // 70.7
$fahrenheit is hier een virtuele property: de hook verwijst nergens naar $this->fahrenheit, dus PHP reserveert er ook geen geheugen voor. Er valt ook niets naar te schrijven; $t->fahrenheit = 80 geeft een Error. Wil je dat wel, dan voeg je een set-hook toe die de waarde terugrekent naar $this->celsius.
Een property die in een van zijn hooks naar zichzelf verwijst, heet een backed property. Die heeft wél opslag en kan een standaardwaarde krijgen. Een virtuele property kan dat niet.
Hooks in interfaces en abstracte klassen
Dit is misschien wel de belangrijkste verandering voor architectuur. Interfaces kunnen sinds PHP 8.4 properties bevatten, mits je aangeeft welke hooks beschikbaar moeten zijn:
interface HeeftNaam
{
public string $naam { get; }
}
final class Klant implements HeeftNaam
{
public function __construct(
public string $naam,
) {}
}
final class Medewerker implements HeeftNaam
{
public function __construct(
private string $voornaam,
private string $achternaam,
) {}
public string $naam {
get => "{$this->voornaam} {$this->achternaam}";
}
}
Het contract zegt alleen dat $naam leesbaar moet zijn. De ene klasse voldoet daaraan met een gewone publieke property, de andere met een virtuele property. Voor de code die tegen HeeftNaam programmeert maakt dat niets uit. Vroeger had je hier een getNaam()-methode voor nodig.
Property hooks en constructor promotion
Hooks werken ook op gepromote constructorparameters, al wordt de constructor dan snel onleesbaar:
final class Product
{
public function __construct(
public string $sku {
set => strtoupper($value);
},
) {}
}
Let op de pijlnotatie bij set: de uitkomst van de expressie wordt de nieuwe waarde. Dat is een verkorte vorm van $this->sku = strtoupper($value).
Combineren met asymmetric visibility
PHP 8.4 bracht tegelijk asymmetric visibility: aparte zichtbaarheid voor lezen en schrijven. Samen met hooks vervangt dat een groot deel van de klassieke getters:
final class Bestelling
{
public private(set) string $status = 'nieuw';
public function verzend(): void
{
$this->status = 'verzonden';
}
}
$bestelling = new Bestelling();
echo $bestelling->status; // nieuw
$bestelling->status = 'betaald'; // Error: kan alleen binnen de klasse
Iedereen mag de status lezen, maar alleen de klasse zelf mag hem veranderen. Gecombineerd met een set-hook houd je ook interne wijzigingen in toom.
Waar je op moet letten
Property hooks zijn krachtig, maar er zitten een paar scherpe randjes aan die je in de praktijk tegenkomt:
- Geen
readonly. Een property met hooks kan niet tegelijkreadonlyzijn. Wil je een waarde die maar één keer gezet mag worden, combineer danpublic private(set)met een controle in de klasse, of blijf bij readonly zonder hooks. - Arrays wijzigen werkt niet zoals je verwacht. Bij een backed property met een
set-hook geeft$obj->items[] = $xeen fout, omdat PHP de hook dan niet kan aanroepen. Je moet de hele array opnieuw toewijzen, of een methode alsaddItem()aanbieden. - Hooks zijn geen plek voor zware logica. Een property-lees die stilletjes een databasequery uitvoert, verrast de volgende developer. Gebruik hooks voor validatie, normalisatie en afgeleide waarden, niet voor I/O.
- Bestaande frameworks. Serializers, ORM's en hydrators die via reflectie properties vullen, moeten hooks ondersteunen. Controleer in de changelog van je ORM en serializer vanaf welke versie hooks ondersteund worden voordat je ze in je entities introduceert; met oudere versies loop je tegen verrassingen aan.
- Overerving. Een child-klasse kan een hook van de parent overschrijven, en via
parent::$prop::get()de originele hook aanroepen. Dat is handig, maar maakt de flow snel lastig te volgen.
Wanneer gebruik je ze wel en niet?
Een praktische vuistregel die we in projecten hanteren:
- Wel: validatie van invoer (e-mail, bedragen, enums als string), normaliseren (trimmen, lowercase), afgeleide waarden die goedkoop te berekenen zijn, en properties in interfaces.
- Niet: lazy loading met I/O, side effects zoals events dispatchen of logging, en logica die afhangt van externe services.
- Twijfel: value objects. Daar is een readonly klasse met een named constructor vaak duidelijker dan een muteerbare property met hooks.
Migreren vanaf getters en setters
Heb je een bestaande codebase vol getX()/setX()-paren, dan hoef je niet alles in één keer om te zetten. Een veilige volgorde:
- Upgrade eerst naar PHP 8.4 zonder iets aan je klassen te veranderen. Dat is al werk genoeg; zie onze uitleg over een PHP-versiemigratie.
- Begin bij nieuwe code en DTO's, waar geen bestaande aanroepers zijn.
- Zet getters die alleen
return $this->xdoen om naarpublic private(set). Laat de getter voorlopig bestaan en markeer hem als@deprecated. - Gebruik statische analyse (PHPStan of Psalm) om de laatste aanroepen van de oude methoden te vinden voordat je ze verwijdert.
Samenvatting
Property hooks maken publieke properties weer bruikbaar zonder dat je de controle opgeeft. Met een set-hook valideer je invoer, met een get-hook bereken je afgeleide waarden, en via interfaces leg je contracten vast zonder getters. Combineer ze met asymmetric visibility en je schrijft aanzienlijk minder boilerplate. Houd hooks wel klein en zonder side effects, en controleer of je serializer en ORM ze ondersteunen.
Meer achtergrond over de rest van het typesysteem lees je in onze documentatie over enumeraties en in ons artikel over PHP 8.1: enums, readonly properties en fibers.