Das neue PHP Release 8.4 wird am 21. November 2024 veröffentlicht und bringt einige coole neue Features mit sich. Eines der wohl beeindruckendsten Features sind Property Hooks. In diesem Artikel werde ich Euch zeigen, was Property Hooks sind und wie Du sie in Deinen zukünftigen Projekten nutzen kannst.
Was sind Property Hooks
Mit Property Hooks kannst Du zukünftig individuelle Getter and Setter Logik für Objekt Eigenschaften definieren, ohne Getter und Setter Methoden für Deine Eigenschaften schreiben zu müssen. Klingt erstmal schräg, ergibt aber durchaus Sinn. Mit Property Hooks kannst Du nämlich direkt in die Definition von Klasseneigenschaften entsprechende Getter und Setter Logik schreiben. Vielleicht kennst Du Property Hooks ja schon von anderen Programmiersprachen, wie z.B. #C.
Wenn Du Laravel Entwickler bist, werden Dich Property Hooks wahrscheinlich stark an Accessors und Mutators erinneren. Property Hooks sind grob beschrieben das gleiche – nur in cool und vor allem nativ.
Schauen wir uns anhand von einfachen Beispielen mal an, wie man Property Hooks benutzt.
Der „get“ Hook
Man kann zukünftig einen get
Hook definieren, der das bisher gekannte Read-Verhalten von PHP beim Lesen von Klasseneigenschaften überschreibt.
Stell Dir ein einfaches Value Object User
vor, in dem die Volljährigkeit geprüft werden soll. Diese Klasse akzeptiert das Geburtsdatum im Konstruktor. Früher hätte man hier eine isAdult Methode definiert, die anhand des Geburtsdatums ausgerechnet hätte, ob jemand volljährig ist. Mit PHP 8.4 kann man das anders machen.
<?php
declare(strict_types=1);
namespace Marcel;
use DateTimeImmutable;
readonly class User
{
public bool $isAdult {
get {
$today = new DateTimeImmutable();
return $today->diff($this->birthday)->y >= 18;
}
}
public function __construct(
public string $name,
public DateTimeImmutable $birthday,
) {}
}
$user = new User(
name: 'Marcel',
birthday: new DateTimeImmutable('1979-12-19'),
);
echo $user->name; // Marcel
echo $user->isAdult; // true
Im oben gezeigten Beispiel definieren wir eine Eigenschaft isAdult
, um die Volljährigkeit eines Users festzustellen. Diese Eigenschaft besitzt die Definition eines get Hooks, der einen Wert zurück geben muss, der der Definition der Eigenschaft entspricht. In diesem Fall muss also ein bool
Wert zurück gegeben werden. Unterscheidet sich der Return Type des get Hooks zur Type Definition der Eigenschaft, würde eine Exception geworfen werden.
Das Beispiel erzeugt darüber hinaus eine virtuelle Eigenschaft. Diese Eigenschaft wird erst erzeugt, wenn sie über $user->isAdult
und somit über den get
Hook aufgerufen wird. Virtuelle Eigenschaften zeichnet aus, dass sie sich im Hook nicht selbst referenzieren. Es wird also im get
Hook nicht auf $this->isAdult
zugegriffen. Virtuelle Eigenschaften besitzen keinen set
Hook. Jeglicher Versuch die Eigenschaft mit einem Wert zu belegen, endet in einer Exception.
Wir können das auch noch verkürzen und weitere neue Features von PHP 8.4 nutzen, indem wir einfach Arrow Funktionen und die Initialisierung von Objekten ohne Klammerung benutzen.
<?php
declare(strict_types=1);
namespace Marcel;
use DateTimeImmutable;
readonly class User
{
public bool $isAdult {
get => new DateTimeImmutable()->diff($this->birthday)->y >= 18;
}
public function __construct(
public string $name,
public DateTimeImmutable $birthday,
) {}
}
Weniger Code, der gewartet werden muss und trotzdem funktioniert. Ich weiß nicht, wie es Euch gerade geht. Aber ich mag Property Hooks als neues Feature gerade sehr.
Typenkompatibilität für „get“ Hooks
Wie zuvor bereit beschrieben, ist es elementar wichtig, dass der zurückgegebene Wert des get
Hooks dem Typen der Eigenschaft entsprechen muss.
Wenn strict_types
nicht genutzt wird, wird versucht den zurückgegebenen Wert in den Typen der Eigenschaft zu wandeln. Hier arbeitet dann das typische PHP Type Juggling. Wenn man also einen Integer Wert für eine als String definierte Eigenschaft zurück geben möchte, wird der Integer Wert in einen String Wert gewandelt.
Wird strict_types
genutzt und der Return Type entspricht nicht der Typendefinition der Eigenschaft, greift das Type Juggling nicht. Es wird ein TypeError geworfen.
Der „set“ Hook
PHP 8.4 Property Hooks erlauben es Dir auch einen set
Hook zu definieren. Dieser wird dann immer benutzt, wenn ein Wert für eine Eigenschaft gesetzt werden soll.
Für den set
Hook gilt erstmal das gleiche, wie für den get
Hook. Sobald er für eine Eigenschaft definiert ist, überschreibt dieser das eigentliche PHP Verhalten, wenn ein Wert für eine Eigenschaft gesetzt werden soll.
Zusätzlich gilt, dass sowohl eine komplexe Funktions-Definition, als auch eine Arrow Funktion als set Hook genutzt werden kann.
Wenden wir das einfach mal auf unsere User
Klasse an.
<?php
declare(strict_types=1);
namespace Marcel;
use DateTimeImmutable;
readonly class User
{
public bool $isAdult {
get => new DateTimeImmutable()->diff($this->birthday)->y >= 18;
}
public string $name {
set(string $name) => ucfirst($name);
}
public DateTimeImmutable $birthday {
set(string $birthday) {
$this->birthday = new DateTimeImmutable($birthday);
}
}
public function __construct(
string $name,
string $birthday,
) {
$this->name = $name;
$this->birthday = $birthday;
}
}
$user = new User(
name: 'Marcel',
birthday: '1979-12-19',
);
echo $user->name; // Marcel
echo $user->birthday->format('d.m.Y'); // 19.12.1979
Wie wir in dem oben gezeigten Beispiel sehen können, haben wir einen set
Hook für die name
Eigenschaft definiert. Diese nutzt eine Arrow Funktion, welche den ersten Buchstaben des Namens als Großbuchstaben umwandelt. Darüber hinaus haben wir einen set
Hook für die birthday
Eigenschaft definiert, die mit dem übergebenen String ein DateTimeImmutable
Objekt setzt.
Typenkompatibilität für „set“ Hooks
Wenn die Eigenschaft eine Typendeklaration besitzt, muss der dazugehörige set
Hook einen damit kompatiblen Typen gesetzt haben. Folgendes Beispiel würde einen fatalen Fehler erzeugen, weil der set
Hook für die name
Eigenschaft keine Typendeklaration bestitzt, die Eigenschaft selbst aber schon.
<?php
declare(strict_types=1);
namespace Marcel;
use DateTimeImmutable;
readonly class User
{
public bool $isAdult {
get => new DateTimeImmutable()->diff($this->birthday)->y >= 18;
}
public string $name {
set ($name) => ucfirst($name);
}
public DateTimeImmutable $birthday {
set (string $birthday) {
$this->birthday = new DateTimeImmutable($birthday);
}
}
public function __construct(
string $name,
string $birthday,
) {
$this->name = $name;
$this->birthday = $birthday;
}
}
Würde man die oben gezeigte User Klasse benutzen, würde es in folgenden Fehler enden.
Fatal error: Type of parameter $name of hook User::$name::set must be compatible with property type
Property Hooks mit Promoted Properties
Promoted Properties sind seit PHP 8.0 ein oft genutztes Feature, welches Code reduziert und übersichtlicher macht. Selbstverständlich können Property Hooks auch für Promoted Properties genutzt werden. Die Syntax würde dann wie folgt aussehen.
<?php
declare(strict_types=1);
namespace Marcel;
use DateTimeImmutable;
readonly class User
{
public bool $isAdult {
get => new DateTimeImmutable()->diff($this->birthday)->y >= 18;
}
public function __construct(
public string $name {
set ($name) => ucfirst($name);
},
public string $birthday {
set (string $birthday) {
$this->birthday = new DateTimeImmutable($birthday);
}
) {}
}
In diesem Beispiel werden die Eigenschaften für die User Klasse über den Konstruktor definiert. Zukünftig können auch hier Property Hooks mit notiert werden. Dabei kann man natürlich auch virtuelle Eigenschaften, wie isAdult
mit Property Hooks außerhalb des Konstruktors definieren.
Property Hooks und write-only Eigenschaften
Write-only mit PHP? Das klingt erstmal schräg, oder? Mit Property Hooks kann man einen solchen Zustand für eine Eigenschaft definieren.
<?php
declare(strict_types=1);
namespace Marcel;
class User
{
public string $fullName {
set (string $name) {
[ $firstName, $lastName ] = explode(' ', $name);
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}
public string $firstName {
set (string $name) => $this->firstName = ucfirst($name);
}
public string $lastName {
set (string $name) => $this->lastName = ucfirst($name);
}
public function __construct(
string $fullName,
) {
$this->fullName = $fullName;
}
}
$user = new User('marcel maaß');
echo $user->fullName; // wird einen Fehler werfen
Wenn der oben gezeigte Code ausgeführt wird, werden wir den folgenden Fehler sehen, sofern wir die fullName
Eigenschaft anzeigen wollen.
Fatal error: Uncaught Error: Property User::$fullName is write-only
Was ist passiert? Im set
Hook für die fullName
Eigenschaft wird kein Wert für diese Eigenschaft gesetzt. Referenziert mal also in einem set Hook nicht auf die Eigenschaft, handelt es sich um eine write-only Eigenschaft. Soll der Wert für diese Eigenschaft gelesen werden, landet man zwangsläufig in einem fatalen Fehler.
Property Hooks und readonly Eigenschaften
Wenn wir schon über write-only Zustände reden, schauen wir uns natürlich auch readonly Eigenschaften an. Im folgenden Beispiel wird die fullName Eigenschaft aus den Eigenschaften für Vor- und Nachnamen zusammengesetzt. Ich möchte in diesem Fall nicht, dass die FullName Eigenschaft direkt gesetzt werden kann.
<?php
declare(strict_types=1);
namespace Marcel;
class User
{
public string $fullName {
get {
return $this->firstName . ' ' . $this->lastName;
}
}
public function __construct(
public readonly string $firstName,
public readonly string $lastName,
) {
$this->fullName = 'Fehler!'; // wird einen Fehler werfen
}
}
Ähnlich wie wir es anfangs schon mit der birthday
Eigenschaft getan haben, besitzt die fullName
Eigenschaft in diesem Fall nur einen get
Hook. Innerhalb des Hooks wird nicht auf die fullName
Eigenschaft referenziert und somit handelt es sich um eine readonly Eigenschaft, die, sofern man für diese einen Wert setzen möchte, einen Fehler verursacht.
Uncaught Error: Property User::$fullName is read-only
Einschränkungen für das readonly Keyword
Natürlich gibt es für die Nutzung von Property Hooks auch Einschränkungen. Das readonly
Keyword kann lediglich bei Klassendefinitionen, aber nicht für die Definition von Eigenschaften benutzt werden. Folgende Klassendefinition wäre valide.
<?php
declare(strict_types=1);
namespace Marcel;
readonly class User
{
public string $firstName {
set(string $name) => ucfirst($name);
}
public string $lastName {
set(string $name) => ucfirst($name);
}
public function __construct(
string $firstName,
string $lastName,
) {
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}
Es ist nach wie vor möglich komplette Klassen als readonly
zu definieren. Folgendes Beispiel würde nicht funktionieren.
<?php
declare(strict_types=1);
namespace Marcel;
readonly class User
{
public readonly string $fullName {
get => $this->firstName . ' ' . $this->lastName;´
}
public function __construct(
string $firstName,
string $lastName,
) {
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}
Mit der gezeigten Klassendefinition würden wir folgenden Fehler erzeugen.
Fatal error: Hooked properties cannot be readonly
Interfaces und Hooked Properties
Eine der herausragendsten Eigenschaften von Hooked Properties ist die Definition in Interfaces. Mit PHP 8.4 kann man also schon in Interfaces festlegen, welche Eigenschaften einer Klasse, die dieses spezifische Interface implementieren, über get oder set Hooks verfügen sollen.
<?php
declare(strict_types=1);
namespace Marcel;
use DateTimeImmutable;
interface UserInterface
{
public bool isAdult { get; }
public string $name { get; set; }
public DateTimeImmutable $birthday { get; }
}
In einem Interface kann man ab PHP 8.4 für jede Eigenschaft festlegen, welche Property Hooks für welche Eigenschaft verwendet werden sollen. Bei dem oben gezeigten Interface mag man jetzt denken, dass für jede der Eigenschaften get und set Hooks geschrieben werden müssen. Aber auch hier gibt es Dinge, die man beachten muss.
<?php
declare(strict_types=1);
namespace Marcel;
use DateTimeImmutable;
class User implements UserInterface
{
public bool $isAdult {
get => new DateTimeImmutable()->diff($this->birthday)->y >= 18;
}
public string $name {
get => strtoupper($this->name),
set (string $name) => $this->name = ucfirst($name)
}
public DateTimeImmutable $birthday;
public function __construct(
string $name,
DateTimeImmutable $birthday
) {
$this->name = $name;
$this->birthday = $birthday;
}
}
Die oben gezeigte Klasse wäre absolut valide. Die isAdult
Eigenschaft besitzt einen get
Hook wie im Interface definiert. Die name
Eigenschaft besitzt einen get
und einen set
Hook. But wait! Was ist eigentlich mit der birthday
Eigenschaft? Immerhin hat diese ja gar keinen get
Hook, wie im Interface beschrieben. Dennoch ist sie valide, weil sie als public
definiert ist und somit den Vorgaben des Interfaces entspricht.
Würden die im Interface definierten Hooks für die Eigenschaften nicht in der User Klasse implementiert werden, würde ein Fehler wie folgt aussehen.
Fatal error: Class User contains 1 abstract methods and must therefore be declared abstract or implement the remaining methods (UserInterface::$name::set)
Abstrakte Klassen und Hooked Properties
Ähnlich wie bei Interfaces kann man Hooked Properties auch in abstrakten Klassen implementieren. Abstraktionen sind immer dann sinnvoll, wenn man eine Basis für alle Klassen, die diese Abstraktion erweitern, bilden möchte. Somit muss man nicht in jeder Klasse Eigenschaften definieren, sondern definiert sie nur einmal in der abstrakten Klasse, von der sich alle anderen Klassen ableiten.
Folgendes Beispiel wäre denkbar.
<?php
declare(strict_types=1);
namespace Marcel;
use DateTimeImmutable;
abstract class AbstractUser implements UserInterface
{
abstract public string $name {
get => strtoupper($this->name),
set;
}
abstract public bool isAdult { get; }
abstract public DateTimeImmutable $birthday { get; }
}
Wie man sieht, funktionieren Property Hooks auch mit abstrakten Klassen wunderbar.
Fazit
Hoffentlich konnte ich Euch mit diesem Artikel über PHP 8.4 Property Hooks einen kleinen Einblick geben, was Euch ab dem 21. November 2024 erwartet. Mit den gezeigten Beispielen habe ich Euch vielleicht davon überzeugt Property Hooks in Euren zukünftigen Projekten zu nutzen.
Ich persönlich sehe PHP 8.4 Property Hooks als ein sehr gutes, zuende gedachtes Feature, welches ich definitiv benutzen werde. Zu Beginn wirkten sie noch ein wenig unhandlich und ungewohnt. Aber je mehr man mit ihnen spielt und einiges ausporbiert, desto mehr fühlen sich Property Hooks einfach als perfekte Erweiterung an.
Natürlich bin ich auch gespannt, wie und vor allem wie schnell die großen PHP Frameworks dieses Feature zukünftig implementieren werden.
Würdest Du Hooked Properties nutzen? Lass mich mit einem Kommentar wissen, wie Dir dieser Artikel gefallen hat und ob und vor allem wie Du Property Hooks zukünftig nutzen möchtest.