mitmario.dev

Statisch, Konstanten, Enums

PHP Sandbox 4 Min Lesezeit 5 BeispieleLektion 2 von 6

Bisher gehörte alles, was in einer Klasse stand, zu einem einzelnen Objekt: Jede Notiz hat ihren eigenen Titel, jeder Posten seinen eigenen Preis. Manches gehört aber nicht zu einem Objekt, sondern zur Klasse als Ganzes.

Konstanten und static

Was zur Klasse gehoert und nicht zum Objekt
<?php

class Notiz
{
    public const MAX_LAENGE = 20;

    private static int $angelegt = 0;

    public function __construct(public readonly string $titel)
    {
        self::$angelegt++;
    }

    public function kurz(): string
    {
        return substr($this->titel, 0, self::MAX_LAENGE);
    }

    public static function anzahl(): int
    {
        return self::$angelegt;
    }
}

$eins = new Notiz("Milch kaufen");
$zwei = new Notiz("Brot holen und Kaffee mitnehmen");

echo $eins->kurz(), "\n";
echo $zwei->kurz(), "\n";

// Beides ohne ein Objekt in der Hand: die Konstante und der Zaehler.
echo "Obergrenze: ", Notiz::MAX_LAENGE, "\n";
echo "Angelegt:   ", Notiz::anzahl(), "\n";

Eine Klassenkonstante steht mit const in der Klasse und ändert sich nie. Von innen liest du sie mit self::MAX_LAENGE, von außen mit Notiz::MAX_LAENGE, und dafür brauchst du kein Objekt. Der Doppelpunkt-Doppelpunkt ist dabei das Gegenstück zum Pfeil: Der Pfeil geht an ein Objekt, die zwei Doppelpunkte gehen an die Klasse.

Eine statische Eigenschaft gehört ebenfalls der Klasse, kann sich aber ändern. Im Beispiel zählt self::$angelegt mit, wie viele Notizen es insgesamt gibt. Das ist genau eine Zahl für alle Objekte zusammen, nicht eine je Objekt.

Und in einer statischen Methode gibt es kein $this. Sie wird ja an der Klasse aufgerufen, und da ist kein Objekt, auf das $this zeigen könnte.

Im Reiter Debug ist das wörtlich zu sehen. Nach Aufzeichnen steht an der Zeile in anzahl() „Noch keine Variablen”, denn dort gibt es weder Parameter noch $this. Und self::$angelegt taucht in keiner der Listen auf, auch nicht im Konstruktor, wo $this->titel steht: Die Liste zeigt Variablen und das Objekt, und der Zähler gehört keinem von beiden.

Wofür static wirklich gut ist

Eine Erzeugermethode statt eines zweiten Konstruktors
<?php

class Notiz
{
    public function __construct(
        public readonly string $titel,
        public readonly bool $erledigt,
    ) {
    }

    // Ein zweiter Konstruktor waere praktisch. Den gibt es in PHP
    // nicht, eine statische Methode tut dieselbe Arbeit.
    public static function ausZeile(string $zeile): self
    {
        $teile = explode("|", $zeile);

        return new self(trim($teile[0]), trim($teile[1] ?? "") === "x");
    }
}

$eins = new Notiz("Milch kaufen", false);
$zwei = Notiz::ausZeile("Brot holen | x");

foreach ([$eins, $zwei] as $notiz) {
    echo $notiz->titel, ": ", $notiz->erledigt ? "erledigt" : "offen", "\n";
}

PHP erlaubt genau einen Konstruktor je Klasse. Wenn ein Objekt auf zwei Wegen entstehen soll, einmal aus Einzelwerten und einmal aus einer Zeile Text, ist eine statische Erzeugermethode die Antwort. Sie baut das Objekt und gibt es zurück, und der Rückgabetyp self heißt „ein Objekt dieser Klasse”.

Das ist der Fall, für den static gedacht ist. Der Kurs benutzt es sonst sparsam, und der Grund ist derselbe wie bei global in Lektion 6.4: Eine statische Eigenschaft ist ein Wert, an den jede Stelle im Programm herankommt und den jede Stelle ändern kann. Für einen Zähler ist das in Ordnung. Für alles, was das Programm steuert, wird es der Ort, an dem am Ende niemand mehr sagen kann, wer den Wert eigentlich gesetzt hat.

Der Fall, für den man früher Konstanten genommen hat

Drei Konstanten, an die sich niemand haelt
<?php

const STATUS_OFFEN = "offen";
const STATUS_ERLEDIGT = "erledigt";
const STATUS_VERWORFEN = "verworfen";

function beschriftung(string $status): string
{
    if ($status === STATUS_OFFEN) {
        return "noch zu tun";
    } elseif ($status === STATUS_ERLEDIGT) {
        return "fertig";
    }

    return "unbekannt";
}

// Die Konstante ist nur ein Angebot. Der Parameter nimmt jede
// Zeichenkette, und niemand haelt einen davon ab, eine andere
// hinzuschreiben.
echo beschriftung(STATUS_OFFEN), "\n";
echo beschriftung("Offen"), "\n";
echo beschriftung("gestern erledigt"), "\n";

Sehr oft gibt es nicht eine Zahl, sondern eine kleine Liste erlaubter Werte: offen, erledigt, verworfen. Drei Konstanten, und alle sind froh.

Das Beispiel zeigt, warum sie nicht reichen. beschriftung(string $status) nimmt jede beliebige Zeichenkette entgegen, denn STATUS_OFFEN ist ja nur ein anderer Name für "offen". "Offen" mit großem O geht durch, "gestern erledigt" auch, und beide landen still im letzten Zweig. Kein Fehler, keine Warnung, ein falsches Ergebnis. Genau die Sorte Problem aus Lektion 7.1.

Ein Enum ist dieselbe Liste, nur als Typ

Dasselbe als Enum
<?php

enum Status: string
{
    case Offen = "offen";
    case Erledigt = "erledigt";
    case Verworfen = "verworfen";
}

function beschriftung(Status $status): string
{
    return match ($status) {
        Status::Offen => "noch zu tun",
        Status::Erledigt => "fertig",
        Status::Verworfen => "weggelegt",
    };
}

echo beschriftung(Status::Offen), "\n";

// Die ganze Liste, ohne sie ein zweites Mal aufzuschreiben.
foreach (Status::cases() as $fall) {
    echo "  ", $fall->name, " steht fuer ", $fall->value, "\n";
}

// Aus Text wird ein Fall, wenn es einen gibt.
var_dump(Status::tryFrom("erledigt"));
var_dump(Status::tryFrom("Offen"));

try {
    Status::from("Offen");
} catch (ValueError $fehler) {
    echo "from() sagt: ", $fehler->getMessage(), "\n";
}

Ein Enum (seit PHP 8.1) schreibt die Liste einmal auf und macht daraus einen Typ. Status steht dann in der Typangabe, und was kein Status ist, kommt gar nicht erst herein. Der Teil : string dahinter heißt „jeder Fall hat einen Textwert”, den du über ->value bekommst. Ohne ihn gäbe es die Fälle trotzdem, nur eben ohne Wert zum Speichern.

Vier Dinge brauchst du im Alltag:

  • Status::Offen ist ein Fall, und zwar immer derselbe. Ein Vergleich mit === funktioniert deshalb.
  • Status::cases() gibt alle Fälle als Array, in der Reihenfolge, in der sie in der Klasse stehen. Damit kannst du über die Liste laufen, ohne sie abzuschreiben.
  • Status::tryFrom("erledigt") sucht den Fall zu einem Textwert und liefert null, wenn es keinen gibt. Das ist der Weg für Daten von außen, etwa aus einem Formular.
  • Status::from("Offen") tut dasselbe, wirft aber einen ValueError, statt null zu liefern. Das ist der Weg für Daten, denen du vertraust, weil ein Fehler dort ein echter Fehler ist.

Der ->name daneben ist der Name des Falls im Code (Offen), nicht sein Wert. Praktisch für Protokolle, ungeeignet zum Speichern: Ein Wert in der Datenbank soll sich nicht ändern, nur weil du einen Fall umbenennst.

Im Reiter Debug siehst du beides nebeneinander: In der Schleife über cases() steht fall als Status {name: "Offen", value: "offen"}, ein Fall ist also ein Objekt mit genau diesen zwei Angaben, und eben kein Text.

Was „vollständig” bei einem match wirklich heißt

Ein match, dem ein Fall fehlt
<?php

enum Status: string
{
    case Offen = "offen";
    case Erledigt = "erledigt";
    case Verworfen = "verworfen";
}

// Hier fehlt Status::Verworfen, und zwar mit Absicht.
function beschriftung(Status $status): string
{
    return match ($status) {
        Status::Offen => "noch zu tun",
        Status::Erledigt => "fertig",
    };
}

echo beschriftung(Status::Offen), "\n";
echo beschriftung(Status::Erledigt), "\n";

try {
    echo beschriftung(Status::Verworfen), "\n";
} catch (\UnhandledMatchError $fehler) {
    echo "Aufgefallen: ", $fehler->getMessage(), "\n";
}

Über ein Enum lässt sich ein match schreiben, das jeden Fall behandelt und kein default braucht. Genau darauf zielt der ganze Aufwand.

Sei dabei genau: PHP prüft die Vollständigkeit nicht beim Einlesen der Datei. Es merkt die Lücke erst, wenn wirklich ein Fall vorbeikommt, für den nichts dasteht, und wirft dann einen UnhandledMatchError. Der Gewinn ist also kein Fehler vor dem Start, sondern etwas fast genauso Wertvolles: Der vergessene Fall wird laut, statt still in ein default zu rutschen und dort eine harmlos aussehende Antwort zu bekommen.

Ein default in einem match über ein Enum nimmt dir genau diesen Alarm wieder weg. Schreib es deshalb nur dort hin, wo du wirklich einen Sammelfall meinst.

Die Faustregel

Drei Konstanten sind drei Zeichenketten mit besseren Namen. Ein Enum ist ein Typ, und der Unterschied zeigt sich nicht beim Aufschreiben, sondern ein halbes Jahr später, wenn ein vierter Zustand dazukommt und dich jede Stelle im Programm darauf hinweist, dass sie ihn noch nicht kennt.

Zum Mitnehmen

Manches gehört zur Klasse und nicht zum einzelnen Objekt: eine Obergrenze, ein Umrechnungsfaktor, die Liste der erlaubten Zustände. Für die ersten beiden gibt es Konstanten und static. Für die dritte gibt es seit PHP 8.1 etwas Besseres.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 5 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.