Eine Kundenklasse mit privatem $email, dazu getEmail() und setEmail() mit Prüfung: Für eine einzige Eigenschaft kommen schnell zwanzig Zeilen zusammen. Genau dieses Muster zeigt das Tutorial zu Klassen und Objekten als klassischen Weg zur Kapselung. Seit PHP 8.4 sitzt die Logik an der Eigenschaft selbst. PHP Property Hooks hängen einen get-Block und einen set-Block an die Property, und von außen bleibt es bei der gewohnten Zuweisung $kunde->email = '...'.
Ob eine Property mit Hook ihren Wert speichert oder bei jedem Lesen neu berechnet, sieht man ihr auf den ersten Blick nicht an.
Was sind PHP Property Hooks? Idee und RFC
Ein Hook ist Code, der an einer Property hängt und Zugriffe abfängt. Der get-Hook läuft beim Lesen, der set-Hook beim Schreiben. In Lehrbüchern heißen Setter auch Mutator-Methoden; PHP Property Hooks übernehmen deren Aufgabe, ohne dass eine Methode entsteht, die der Aufrufer kennen muss. Das Feature kam mit PHP 8.4. Das RFC dazu stammt von Ilija Tovilo und Larry Garfield und orientiert sich nach eigener Aussage am stärksten an Kotlin, mit Anleihen bei C# und Swift.
So sieht die Kundenklasse aus der Einleitung mit einem set-Hook aus:
<?php
class Kunde
{
public string $email {
set {
$neu = strtolower(trim($value));
$ok = filter_var(
$neu,
FILTER_VALIDATE_EMAIL
);
if ($ok === false) {
throw new InvalidArgumentException(
"Keine E-Mail-Adresse: $value"
);
}
$this->email = $neu;
}
}
}
$kunde = new Kunde();
$kunde->email = ' Info@Example.ORG ';
echo $kunde->email; /* info@example.org */
Innerhalb des Hooks greift $this->email direkt auf den gespeicherten Wert zu und löst den Hook nicht erneut aus. Jede andere Zuweisung, auch eine aus einer Methode derselben Klasse, läuft dagegen durch den set-Hook. Welcher Weg bei einem Zugriff genommen wird, zeigt das Schema:
flowchart TD
A[Zugriff auf die Property] --> B{Lesen?}
B -->|ja| C{get-Hook da?}
C -->|ja| D[get-Hook liefert Wert]
C -->|nein| E[gespeicherter Wert]
B -->|nein, schreiben| F{set-Hook da?}
F -->|ja| G[set-Hook prüft und wandelt]
G --> H[speichern oder Exception]
F -->|nein| I{Property virtuell?}
I -->|ja| J[Error, read-only]
I -->|nein| K[Wert direkt speichern]
Syntax der Property Hooks in PHP
Es gibt genau zwei Hooks, get und set. Beide kennen eine Kurzform mit Pfeil und eine Blockform mit geschweiften Klammern:
class Example
{
public string $name {
get => expression;
set => expression;
}
}
/* Property hooks were added to PHP 8.4.
This feature lets a class intercept property
access: reading the property calls get,
writing it calls set. For simple logic they
replace getter and setter methods (mutator
methods), which are verbose.
get => expr short get, returns expr
get { return; } block get, must return
set => expr short set, stores expr
set { ... } block set, uses $value
set(T $v) { ... } own name, wider type ok
A property is virtual when no hook
reads or writes $this->name itself.
A virtual property has no default value.
With property hooks, most getters and setters
become plain property access. Developers wrap
the logic around the property, callers keep
the cleaner $obj->name syntax.
The design is inspired by languages like
Kotlin, with ideas from C# and Swift. */
Im set-Hook steht der neue Wert in $value, ohne dass man ihn deklariert. Wer einen eigenen Namen will, schreibt set(string $neu); der Typ darf dabei weiter sein als der der Property, etwa string|DateTimeImmutable für eine Datums-Property, die auch Text annimmt. Ein unverträglicher Typ endet in Type of parameter $value of hook X::$n::set must be compatible with property type. Die Kurzform set => ausdruck speichert das Ergebnis des Ausdrucks immer in der eigenen Property. Andere Hooks als get und set gibt es nicht, ein isset => ... bricht mit einem Parse error ab.
PHP 8.4 vorausgesetzt: der Parse error davor
Läuft der Server noch mit PHP 8.3, kommen PHP Property Hooks gar nicht erst zur Ausführung. Gemessen mit dem offiziellen Docker-Image für 8.3, an der Kundenklasse vom Anfang:
Parse error: syntax error, unexpected token "{",
expecting "," or ";" in ... on line 4
Die Meldung nennt keine Version, deshalb sucht man den Fehler leicht im eigenen Code. Steht der Hook in einer Konstruktor-Promotion, endet die Meldung stattdessen auf expecting ")". Eine Versionsprüfung hilft nur, wenn sie in einer anderen Datei steht als die Klasse mit den Hooks. PHP parst eine Datei vollständig, bevor die erste Zeile läuft, und eine Prüfung oben in derselben Datei kommt nie zum Zug:
<?php
/* index.php, ohne eigene Hooks */
if (PHP_VERSION_ID < 80400) {
exit('Property Hooks brauchen PHP 8.4');
}
require __DIR__ . '/src/Kunde.php';
Bei vielen Hostern lässt sich die Version im Kundenmenü umstellen. Unter PHP 8.5 verhalten sich alle Beispiele dieses Tutorials wie unter 8.4.
Der set-Hook: Werte beim Schreiben prüfen
Der häufigste Einsatz von PHP Property Hooks ist eine Prüfung, die ungültige Werte gar nicht erst in das Objekt lässt. Wirft der set-Hook eine Exception, bleibt der alte Wert stehen:
<?php
class Konto
{
public int $saldo = 0 {
set {
if ($value < 0) {
throw new InvalidArgumentException(
'Saldo darf nicht negativ sein'
);
}
$this->saldo = $value;
}
}
}
$konto = new Konto();
$konto->saldo = 50;
try {
$konto->saldo = -5;
} catch (InvalidArgumentException $e) {
echo $e->getMessage();
}
echo $konto->saldo; /* 50 */
Der Typ der Property gilt auch für den Hook. Ein Text wie 'abc' scheitert mit TypeError: Konto::$saldo::set(): Argument #1 ($value) must be of type int, string given. Ohne declare(strict_types=1) wandelt PHP dagegen '42' still in die Zahl 42 um, genau wie bei einem Funktionsparameter.
Hooks in der Konstruktor-Promotion
Hooks lassen sich auch an einen Parameter der Konstruktor-Promotion schreiben. Die Grundlagen zu Typen und Konstruktor-Promotion erklärt das Tutorial zu Typed Properties. Der set-Hook läuft schon beim Erzeugen des Objekts:
<?php
class Produkt
{
public function __construct(
public string $sku {
set => strtoupper(trim($value));
},
public float $preis = 0.0,
) {}
}
$p = new Produkt(' ab-12 ', 9.9);
echo $p->sku; /* AB-12 */
Einen solchen Parameter mit nur einem get-Hook nimmt PHP zwar an, er scheitert aber bei new mit Error: Property A::$x is read-only. Er ist virtuell, und der Konstruktor versucht, ihm einen Wert zuzuweisen.
Virtuelle Properties: berechnen statt speichern
PHP Property Hooks können auch Werte liefern, die nirgends gespeichert sind. Eine Property ist virtuell, wenn keiner ihrer Hooks auf $this->name der eigenen Property zugreift. Sie hat dann keinen eigenen gespeicherten Wert im Objekt und eignet sich für Werte, die sich aus anderen ableiten. Ein Bruttopreis, der nie vom Nettopreis abweichen darf, ist der typische Fall:
<?php
class Preis
{
public function __construct(
public float $netto,
public float $steuersatz = 0.19,
) {}
public float $brutto {
get => round(
$this->netto * (1 + $this->steuersatz),
2
);
}
}
$p = new Preis(100.0);
echo $p->brutto; /* 119 */
Schreiben lässt sich $brutto nicht, der Versuch endet in Error: Property Preis::$brutto is read-only. Einen Standardwert verbietet PHP für virtuelle Properties ebenfalls. Bei der Ausgabe verhalten sich die Funktionen unterschiedlich, und das überrascht beim ersten Export:
| Ausgabe | virtueller Wert enthalten? |
json_encode() | ja, {"netto":100,"steuersatz":0.19,"brutto":119} |
get_object_vars() | ja |
foreach über das Objekt | ja |
var_export() | ja |
var_dump() und print_r() | nein |
| Cast in ein Array | nein |
serialize() | nein |
Für Umrechnungen in beide Richtungen taugt die Kurzform des set-Hooks nicht. Wer set => $this->celsius = ... schreibt, bekommt keine virtuelle Property: Die Kurzform speichert das Ergebnis der Zuweisung zusätzlich in der eigenen Property, isVirtual() meldet dann false. Mit der Blockform bleibt sie virtuell:
<?php
class Temperatur
{
public float $celsius = 0.0;
public float $fahrenheit {
get => $this->celsius * 9 / 5 + 32;
set {
$this->celsius = ($value - 32) * 5 / 9;
}
}
}
$t = new Temperatur();
$t->fahrenheit = 212;
echo $t->celsius; /* 100 */
Grenzen von PHP Property Hooks
Einige Kombinationen mit PHP Property Hooks lehnt die Sprache grundsätzlich ab. Alle Meldungen sind unter Windows und Linux mit PHP 8.4 geprüft:
| Fall | Meldung | Ausweg |
readonly mit Hook, auch in einer readonly class | Hooked properties cannot be readonly | Asymmetric Visibility, siehe unten |
static mit Hook | Cannot declare hooks for static property | statische Methode |
$obj->liste[] = $x | Indirect modification of ... is not allowed | Array kopieren und neu zuweisen |
Referenz mit &$obj->n | Indirect modification of Z::$n is not allowed | Wert zurückgeben statt Referenz |
unset($obj->x) | Cannot unset hooked property Q::$x | Property nullable machen |
| Standardwert an virtueller Property | Cannot specify default value for virtual hooked property | Wert im get-Hook liefern |
&get zusammen mit set | Get hook of backed property ... may not return by reference | auf einen der beiden verzichten |
Bei readonly liegt der Grund in der Prüfung selbst. PHP erkennt einen noch beschreibbaren readonly-Wert daran, dass der gespeicherte Wert nicht initialisiert ist. Eine virtuelle Property hat gar keinen gespeicherten Wert, und ein get-Hook würde verwischen, was „initialisiert“ überhaupt bedeutet. Das RFC macht die Kombination deshalb zu einem Fehler beim Übersetzen.
Die Array-Zeile trifft in der Praxis am häufigsten. Für $b->positionen[] = 'Kabel' bräuchte PHP eine Referenz auf das gespeicherte Array, und die gibt eine Property mit Hook nicht heraus. Das gilt schon bei einem einzelnen get-Hook. Der Ausweg ist unspektakulär:
<?php
class Bestellung
{
public array $positionen = [] {
set => array_values($value);
}
}
$b = new Bestellung();
$liste = $b->positionen;
$liste[] = 'Kabel';
$b->positionen = $liste; /* set-Hook läuft */
Wer oft anhängt, gibt der Klasse eine Methode hinzufuegen(). Ein &get ohne set-Hook lässt das Anhängen zwar wieder zu, umgeht dann aber jede Prüfung. Anders beim Hochzählen: $z->n++ funktioniert, weil PHP dafür erst den get-Hook und dann den set-Hook aufruft.
Vererbung: parent::$x::get() und final
Eine Kindklasse kann die Hooks ihrer Elternklasse erweitern. Der Aufruf läuft nicht über eine Methode, sondern über den Namen der Property:
<?php
class Feld
{
public string $titel = '' {
set => trim($value);
}
}
class PflichtFeld extends Feld
{
public string $titel = '' {
set {
parent::$titel::set(ucwords($value));
}
get => parent::$titel::get() . ' *';
}
}
$f = new PflichtFeld();
$f->titel = ' name ';
echo $f->titel; /* Name * */
Den Standardwert = '' wiederholt die Kindklasse mit Absicht. Ohne ihn verliert die neu deklarierte Property ihren Startwert, und der erste Lesezugriff endet in Typed property ... must not be accessed before initialization. Soll ein Hook gar nicht überschrieben werden, steht final set in der Elternklasse. Eine Kindklasse, die es trotzdem versucht, scheitert mit Cannot override final property hook Basis::$x::set().
Property Hooks in Interfaces und abstrakten Klassen
Wie Properties in Interfaces und abstrakte Properties in das jeweilige Konzept passen, zeigen die Tutorials zu Interfaces und zu abstrakten Klassen. Für PHP Property Hooks bringen beide eigene Regeln mit. Ein Interface verlangt keine bestimmte Umsetzung, sondern nur, dass gelesen oder geschrieben werden kann:
<?php
interface HatName
{
public string $name { get; }
}
class Kategorie implements HatName
{
public string $name = 'Kabel';
}
class Kunde implements HatName
{
public function __construct(
public string $vorname,
public string $nachname,
) {}
public string $name {
get => "$this->vorname $this->nachname";
}
}
Beide Klassen erfüllen den Vertrag, und eine readonly-Property aus der Konstruktor-Promotion täte es ebenso. Verlangt das Interface { get; set; }, reicht ein reiner get-Hook nicht mehr. Die Meldung spricht dann von einer abstrakten Methode, weil PHP den fehlenden set-Hook wie eine solche behandelt: Class E contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (HatWert::$wert::set). Eine Property ohne Hook-Angabe ist in Interfaces nicht erlaubt (Interfaces may only include hooked properties), und eine private Umsetzung lehnt PHP ab.
In einer abstrakten Klasse gilt dasselbe mit abstract davor. abstract public string $typ { get; } verlangt von jeder Kindklasse eine lesbare Property, und eine schlichte public string $typ = 'PKW'; erfüllt das bereits. Fehlt sie, meldet PHP Class Rad contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Fahrzeug::$typ::get).
PHP Property Hooks, Getter und Setter im Vergleich
PHP Property Hooks ersetzen Getter und Setter nicht überall. Oft passt einer von vier anderen Wegen besser:
| Ansatz | Passt für | Haken |
| Property Hooks | Prüfen, Bereinigen, Berechnen beim Zugriff | kein readonly, keine Arrays per [] |
| Getter und Setter | aufwendige Logik, mehrere Parameter, sprechende Namen | viel Code für wenig Inhalt |
readonly | Werte, die nach dem Konstruktor nie wechseln | auch die Klasse selbst kann nicht mehr schreiben |
public private(set) | außen lesbar, nur innen schreibbar | keine Prüfung beim Schreiben |
__get() und __set() | dynamische Namen, Altcode | unsichtbar für IDE und statische Analyse |
Die Asymmetric Visibility mit public private(set) kam ebenfalls mit PHP 8.4 und ist oft die bessere Wahl als PHP Property Hooks, wenn nur der Schreibzugriff von außen verboten werden soll. Ein Hook wäre dafür ein Umweg. Der Schreibversuch von außen meldet Cannot modify private(set) property Ticket::$status from global scope. Wer von magischen Methoden umsteigt, kann beides mischen: __get() wird für eine Property mit Hook nicht aufgerufen, für unbekannte Namen weiterhin.
Zur Laufzeit gemessen mit fünf Millionen Lesezugriffen:
| Zugriff | Windows 8.4.8 | Linux 8.4.17 |
| einfache Property | 50 ms | 23 ms |
| Getter-Methode | 145 ms | 72 ms |
| get-Hook | 129 ms | 62 ms |
Ein Hook ist also etwas schneller als ein Getter, aber gut zweieinhalbmal so langsam wie der direkte Zugriff. Für eine Property ohne Logik gehört deshalb kein Hook dazu.
Für Werkzeuge, die Objekte aus der Datenbank befüllen, liefert Reflection alles Nötige. hasHooks() und isVirtual() geben Auskunft über die Property, getRawValue() und setRawValue() lesen und schreiben am Hook vorbei. Ein Hydrator vermeidet so, dass jede gespeicherte Zeile erneut durch die Prüfung läuft:
<?php
$prop = new ReflectionProperty(Kunde::class, 'email');
$prop->setRawValue($kunde, 'Alt@Example.ORG');
echo $kunde->email;
/* Alt@Example.ORG, der set-Hook lief nicht */
Fazit
PHP Property Hooks verlegen Prüfung und Berechnung dorthin, wo der Wert liegt. Ein set-Hook bereinigt oder lehnt ab, ein get-Hook formatiert oder berechnet, und eine Property ohne Zugriff auf den eigenen Wert speichert gar nichts. Voraussetzung ist PHP 8.4 auf dem Server, sonst steht ein Parse error vor allem anderen.
Die meisten Überraschungen kommen von drei Stellen: Die Kurzform von set speichert immer, Arrays lassen sich nicht per [] erweitern, und readonly verträgt sich nicht mit Hooks. Wo nur der Schreibzugriff von außen stört, reicht private(set), und wo eine Property keine Logik trägt, bleibt sie am schnellsten ohne Hook.