Theme-Editor

PHP8 Feautures

Igäl

Mitglied
Registriert
17.10.26
Beiträge
15
Reaktionspunkte
3
Punkte
3
1/3
Themensteller
Liebe Gemeinde

Wie gesagt fliesst meine Freizeit wieder stark in mein Hobbyprojekt. Es ist eine abgespeckte Rule Engine für Dungeons & Dragons 2024. Abgespeckt deswegen, weil ich mit meiner Gruppe weiterhin am Tisch würfle und weiterhin am Tisch Schaden berechne und manuell abziehe. Also eher ein Charakterbogen+ und ein Management Tool für Zustände, Inventar, Gear etc.

Mein altes Framework basierte noch auf PHP 4 und 5 und wurde punktuell erweitert. Grossmehrheitlich ist es aber Spaghetticode im OOP-Pelz. Entsprechend ist das Refactoring mit allem was PHP 8+ zu bieten hat ein riesen Schritt und eigentlich eine neue Programmiersprache, die ich mir aneignen muss.

Aus diesem Grund bin ich interessiert an Austausch. Und auch, weil wir ja jetzt diese Forum wieder etwas mit Inhalt füllen wollen, richtig? Entsprechend möchte ich wissen, wie eure Erfahrungen mit gewissen Feautures sind und ob ihr sie in Gebrauch habt. Falls ja, warum, falls nein, warum nicht?

Unlängst bin ich auf zwei Themen gestossen, die meine Architektur vermutlich prägen werden, sofern sich deren Einsatz bewährt. Die Rede ist von asymmetric visibility (für Entities) und Wither-Methoden (für DTOs) .

Architektonisch habe ich mich dazu entschieden, dass meine Repositories jeweils Entitys oder Config-Container befüllen. In einem Domain Driven Design im Backend und einem Data Driven Design in der Datenbank kommen da ziemlich viele solcher Klassen zusammen. Entsprechend schreibt man sich mit getter- und setter-Methoden die Finger wund. Genau für diesen Fall habe ich die asymmetric visibility entdeckt.
PHP:
class someEntity {
    public function __construct(
        public private(set) int $someId,
        public private(set) string $someTitle,
        public private(set) array $someAttributes
    ) {}
}
Das eliminiert mir auf einen Schlag drei Getter-Methoden und ich muss nur noch die Setter schreiben.

In diesem Zusammenhang nimmt mich wunder, ob ihr das ähnlich macht oder ob ihr eure Variablen mit public mutable belässt oder gar auf private setzt und getter-Methoden einfügt. Auch auf das Warum bin ich neugierig, weil ich in PHP8 noch wenig Erfahrung habe und eventuell auch gewisse Fallstricke übersehe.

Das andere sind meine DTOs, an welche ich den Anspruch habe, dass sie die Datenintegrität 100%ig gewährleisten. Diese werden, wo vorhanden, aus den Config-Containern generiert. Das ist kein Problem, wenn ein DTO mit den finalen Daten instanziert werden kann, wird aber zum Problem, wenn es nachträgliche Änderungen im DTO gibt.
PHP:
class someDTO    {
    public function __construct(
        public readonly int $someId,
        public readonly string $someTitle,
        public readonly array $someAttributes
    ) {}
}
Man denke beispielsweise an eine Edit-Funktion für News, wo der Controller das Formular für die Anzeige rendert, aber noch keine Daten hat, um die value-Attribute der Form-Felder zu füttern. Meine Herangehensweise ist, dass ich das DTO für das Frontend einfach ersetze, indem ich eine Wither-Methode im DTO aufrufe, die quasi bewirkt, dass sich das DTO als Kopie neu aufsetzt, jedoch einzelne Parameter mit geänderten Werten instanziert.

Das erhält mir die Datenintegrität, jedoch finde ich es etwas unschön, dass ich in derselben Methode (bspw. einer Funktion in einem Controller) ein DTO erstelle, Daten modifiziere und dann gleich nochmals ein DTO erstelle, das die Daten des zuvor erstellten DTOs überschreibt. Aber ich sehe keine Alternative, wenn ich fully protected DTOs bereitstellen möchte.

Was sind eure Erfahrungen, respektive Lösungsansätze mit diesem Pattern?

Kennt ihr irgendwelche Gamechanger aus PHP8 (oder auch 6 und 7 🙂)? Aktuell bin ich dankbar für alles, was mir meinen alten Spaghetticode sinnvoll entflechten hilft.

So long,

dä Igäl.
 
Bin eher Webseiten-Bastler als Backend-Architekt, aber für reine DTOs würde ich readonly-Klassen nehmen, dann brauchst du da gar kein private(set). Und mit clone() samt Property-Array in PHP 8.5 werden Wither ziemlich kurz. private(set) finde ich vor allem bei den Entities spannend, wo sich intern doch noch was ändern soll.
 
Themensteller
Genau so mache ich das. DTOs sind mit readonly protected, Entity-Variablen und Variablen aus Configs (bearbeitbare Vorgänger meiner DTOs, die danach als DTO komplett fixiert werden) werden mit public private(set) instanziert.

Clone() schaue ich mir gerne an. Aktuell arbeiten meine Wither noch mit Elementkopie und einem Array als Übergabeparameter für den gezielten Austausch von Werten.

PHP:
class someDTO    {
    public function __construct(
        public readonly int $someId,
        public readonly string $someTitle,
        public readonly array $someAttributes
    ) {}
   
    public function with(array $new): self {
        return new self(
            someId: $new['newId'] ?? $this->someId,
            someTitle: $new['newTitle'] ?? $this->someTitle,
            someAttributes: $new['newAttributes'] ?? $this->someAttributes
        );
    }
}
 
Zuletzt bearbeitet:
Kleiner Fehler im Beispiel: der Parameter heisst $news, benutzt wird aber $new. Da das ?? den Fehler schluckt, bleiben dann einfach still die alten Werte drin, das sucht man ewig.

Mit dem Array als Übergabe hätte ich generell etwas Bauchweh, weil man sich bei den Keys leicht vertippt und das nirgends auffällt. Ich hab mir für die paar Felder, die sich wirklich ändern, lieber einzelne Methoden gebaut, also withTitle(string $title) usw. Ist mehr Schreibarbeit, dafür prüft PHP den Typ und die IDE kennt die Namen.

Mit 8.5 wird das in der Methode dann ein Einzeiler:

return clone($this, ['someTitle' => $title]);

Das klappt auch mit readonly, solange es innerhalb der Klasse passiert. Läuft dein Projekt schon auf 8.5 oder noch auf 8.4?
 
Themensteller
Danke für den Hinweis. War lediglich freihändiger Code für die Veranschaulichung im Thread. Habe das aber noch angepasst.

Ja, habe unlängst umgesattelt, weil ich unbedingt mit der asymmetric visibility und property hooks arbeiten wollte und dann halt die aktuellste Stable genommen. Wie gesagt werde ich clone() sicherlich noch genauer anschauen. Aber hatte bislang noch keine Zeit, weil Hobbyprogrammierer und... naja... das Leben... du weisst schon 🙂

Wither pro Feld fand ich einfach wieder zu viel Boilerplate. Ich denke, dass Fehler in der Schreibweise auffallen, wenn die Funktion gut getestet wird. Da ich noch recht am Anfang des Projektes stehe, hatte ich als Anwendungsbereich bis jetzt eigentlich nur das Befüllen von bestehenden FormElement-DTOs mit Einträgen ab DB (da fällt es auf, wenn die Einträge nicht da sind), das ergänzen von Error-Messages pro Feld bei Regelverletzung (fällt im Testing auch auf) und das hinzufügen von Entities in einem List-DTO (fällt auch auf, wenn keine Listeneinträge erscheinen).

Deshalb scheint wohl clone() schon das Mittel der Wahl zu sein für die Zukunft.
 
Genau so mache ich das. DTOs sind mit readonly protected, Entity-Variablen und Variablen aus Configs (bearbeitbare Vorgänger meiner DTOs, die danach als DTO komplett fixiert werden) werden mit public private(set) instanziert.

Clone() schaue ich mir gerne an. Aktuell arbeiten meine Wither noch mit Elementkopie und einem Array als Übergabeparameter für den gezielten Austausch von Werten.
Dein Beispiel lässt sich auch ohne das clone von 8.5 schöner schreiben. 😀 kai_web hats eh schon geschrieben, das array hat viele Probleme: vertippser, typsicherheit..

`readonly` gleich auf die Klasse, wenn alles readonly ist. Gleich auch ein final dazu. Wegnehmen ist leicht, falls du wirklich mal musst, aber bei welchem DTO brauchst du Vererbung?
Das attributes-Array mittels phpdoc näher bezeichnen hilft auch, v.a. wenn man dann die IDE den Code prüfen lässt bzw PHPStan nutzt.
Die with-Methode kann auch einfach benannte Parameter nutzen, dann hast du die Typen festgelegt, und der Aufruf mittels Array klappt ja auch noch.

Sobald ein Parameter allerdings null sein dürfte, klappt das nicht mehr gut, weil du ihn mit dieser with-Methode nicht überschreiben kannst. Bei deiner Array-Variante könntest du auf Existenz prüfen 😀.

PHP:
final readonly class SomeDTO
{
    /**
     * @param array<string, string|int|bool> $attributes
     */
    public function __construct(
        public int $id,
        public string $title,
        public array $attributes
    )
    {
    }

    /**
     * @param array<string, string|int|bool> $attributes
     */
    public function with(?int $id = null, ?string $title = null, ?array $attributes = null): self
    {
        return new self(
            id: $id ?? $this->id,
            title: $title ?? $this->title,
            attributes: $attributes ?? $this->attributes
        );
    }
}

$some = new SomeDTO(1, 'Title', ['weasel' => true]);

$other = $some->with(attributes: ['wiesel' => false ]);

// oder wenn die Daten in einem Array sind, aus Gründen:
$data = ['title' => 'Blubb', 'attributes' => [ 'weasel' => false, 'critter' => true ]];
$yetAnother = $some->with(...$data);
 
Themensteller
Ich habe mir das jetzt so zusammengebastelt
PHP:
// Ein DTO, das die benötigten Daten für ein einzelnes Formularelement (input, select etc.) enthält.
declare(strict_types=1);

namespace moli\dnd2\Core\UI\Form;

final readonly class FormElement {
/*********************************************************************************************************/
    public function __construct(
        public string $type,
        public string $name,
        public string $label = '',
        public mixed $value = '',
        public array $attributes = [],
        public array $options = [],
        public string $errorMessage = ''
        ) {}
/*********************************************************************************************************/
    public function with(mixed ...$changes): self    {
        return new self(
            type:    $changes['type'] ?? $this->type,
            name:    $changes['name'] ?? $this->name,
            label:     $changes['label'] ?? $this->label,
            value:    $changes['value'] ?? $this->value,
            attributes: $changes['attributes'] ?? $this->attributes,
            options: $changes['options'] ?? $this->options,
            errorMessage: $changes['errorMessage'] ?? $this->errorMessage
        );
    }
}

In der Validierung werden, je nach applizierten Rules, Errors auf die Elemente gemappt. Dafür muss das DTO erneuert werden. Das sieht im FormHandler so aus:
PHP:
// redeclaration of DTO ListElement after validation
if(!empty($this->formErrors)) {
    foreach($this->formErrors as $elementName => $errorMessage) {
        $element = $this->formElements[$elementName];

        $this->formElements[$elementName] = $element->with(
            errorMessage: $errorMessage
        );
    }
}

Das ganze mit Clone zu lösen, war mir dann doch zu umständlich, da readonly properties ausschliesslich innerhalb der __clone()-Methode überschrieben werden dürfen. Das hätte bedeutet, dass ich die Vorteile der Constructor Property Promotion teilweise hätte aufgeben müssen und gezwungen gewesen wäre, im Constructor Hilfsvariablen zu initalisieren. Also habe ich Boilerplate gegen Bolierplate abgewogen und nehme halt die Redeklaration des Constructors im Wither in Kauf.

Clone rockt wahrscheinlich dann, wenn man danach mit beiden Objekten weiterfahren möchte. Hier kürze ich mir die Sache möglichst mit named properties, rest parameters und (je nachdem) argument unpacking ab.
 
Zuletzt bearbeitet:
Bei deiner Version verlierst du jegliche Typ-Informationen wegen des mixed, und IDE support ist auch schlechter. Ich würd dir echt empfehlen auch ein Analysetool wie phpstan, psalm oder mago zu nutzen. Die meckern gleich, wenn die Typen nicht zusammenpassen und du kommst nicht erst zur Laufzeit drauf. 🙂

Bzgl Clone: da hast du evt was verwechselt. das clone von PHP8.5 kann auch readonly Properties überschreiben, sofern du dich innerhalb der Klasse befindest - wie protected, und du brauchst die Magic __clone Methode nicht. Das folgende Snippet geht problemlos.

PHP:
<?php

final readonly class Test
{
    public function __construct(public string $name, public int $age) {}

    public function withAge(int $age): self { return clone($this, ['age' => $age]); }

    public function __toString(): string { return "$this->name ($this->age)"; }
}


$wsl = new Test('wiesel', 40);

echo $wsl . "\n";

$aelteresWiesel = $wsl->withAge(41);

echo $aelteresWiesel . "\n";

Zu deinem Form-Beispiel: fühlt sich bissl an, als würde deine Klasse zu viel vermischen. Form-Definition und Validierungsergebnis.

Bei deinem 2. Beispiel kannst du dir das empty übrigens sparen. foreach macht eh nichts, wenn das Array leer ist.

Ach und...
wenn man danach mit beiden Objekten weiterfahren möchte.
Dein with erzeugt auch ein neues Objekt, und du hättest am Ende beide zur Hand.
 
Themensteller
Ich bin sehr dankbar für solche Präzisierungen. Mir schwirrt der Kopf von all den neuen Konzepten und Möglichkeiten.

Die beiden Snippets oben sind aus verschiedenen Klassen. Mein FormHandler ist tatsächlich für die Validierung zuständig. Die Definition passiert jedoch in den Controller-Klassen.

Ich nehme an, dass ich die Typsicherheit wieder erlange, wenn ich nicht mit den Splat-Operators arbeite, sondern die Parameter wie althergebracht in die Funktion übergebe. Richtig? Wie ich aber auch sehe, bevorzugst du individuelle Wither-Funktionen, je Feld, das geändert werden soll. Habe ich das korrekt interpretiert?
 
Zurück
Oben