PHP 8.4 und das neue Property Hooks Feature

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.

Kommentar verfassen

Diese Seite verwendet Akismet, um Spam zu reduzieren. Erfahre, wie deine Kommentardaten verarbeitet werden..