Abschnitt 13 · Lektion 1
Konstruktor und Sichtbarkeit
Klassen hattest du schon einmal, am Ende von Abschnitt 6: Die Klasse ist der Bauplan, new macht
daraus ein Objekt, mit -> kommst du an seine Eigenschaften und Methoden, und $this ist in einer
Methode das Objekt, an dem sie gerade läuft. Das reichte dort, um Daten und die Funktionen darauf
zusammenzulegen, und seither hast du es bei Ausnahmen, bei Composer und im ganzen Web-Teil
wiedergesehen.
Nach drei Abschnitten im Web spielt dieser wieder auf der Kommandozeile. Was du siehst, steht im Terminal, und der Browser bleibt leer, bis auf ein einziges Beispiel in Lektion 13.3.
In Lektion 6.7 stand der Konstruktor schon da, mit dem Versprechen, dass die Erklärung hier kommt.
Bis dahin könntest du ein Objekt auch ohne ihn bauen: new, und danach jede Eigenschaft von Hand
füllen. Das funktioniert, hat aber ein Loch: Zwischen dem new und der letzten Zuweisung gibt es
einen Moment, in dem das Objekt halbfertig ist.
Ein halbfertiges Objekt ist kein Zwischenstand, sondern ein Fehler
<?php
class Posten
{
public string $titel;
public int $menge;
public float $preis;
}
$kaffee = new Posten();
$kaffee->titel = "Kaffee";
$kaffee->menge = 2;
$kaffee->preis = 4.90;
echo $kaffee->titel, ": ", $kaffee->menge, " Stueck\n";
// Und hier derselbe Aufbau, nur ist eine Zeile abhanden gekommen.
$tee = new Posten();
$tee->titel = "Tee";
$tee->menge = 3;
try {
$zeile = $tee->titel . ": " . $tee->preis;
echo $zeile, "\n";
} catch (Error $fehler) {
echo "Kaputt: ", $fehler->getMessage(), "\n";
} Beim zweiten Posten fehlt eine Zeile, und PHP bricht ab:
Typed property Posten::$preis must not be accessed before initialization. Das klingt umständlich
und sagt genau das Richtige: Die Eigenschaft hat einen Typ, null ist keine Zahl, also steht dort
noch überhaupt kein Wert.
Das ist immer noch besser als das, was ein Array getan hätte, nämlich stillschweigend null liefern
und weiterrechnen. Es bleibt trotzdem ein Fehler, der erst beim Benutzen auffällt, und zwar weit weg
von der Stelle, an der die Zuweisung vergessen wurde.
Im Reiter Debug kannst du dem halben Objekt zusehen. Nach Aufzeichnen wächst kaffee Zeile
für Zeile, erst Posten {}, dann Posten {titel: "Kaffee"} und so weiter, bis alle drei Werte in
der Klammer stehen. Bei tee bleibt die Klammer an der Zeile mit try bei
Posten {titel: "Tee", menge: 3} stehen: preis fehlt, und zwei Schritte weiter steht fehler mit
der Meldung in der Liste.
Gefangen heißt dabei: kein Absturz. php index.php; echo $? im Terminal schreibt unter die beiden
Zeilen eine 0. Nimm das try samt catch probeweise heraus, und aus der 0 wird eine 255, mit der
Meldung auf dem Fehlerkanal statt hinter „Kaputt:”.
Der Konstruktor macht die Angaben zur Pflicht
<?php
class Posten
{
public string $titel;
public int $menge;
public float $preis;
public function __construct(string $titel, int $menge, float $preis)
{
$this->titel = $titel;
$this->menge = $menge;
$this->preis = $preis;
}
}
$kaffee = new Posten("Kaffee", 2, 4.90);
echo $kaffee->titel, ": ", $kaffee->menge, " Stueck\n";
// Und jetzt der Versuch von eben, mit einer fehlenden Angabe.
try {
$tee = new Posten("Tee", 3);
} catch (ArgumentCountError $fehler) {
echo "Abgelehnt: ", get_class($fehler), "\n";
} __construct ist eine Methode mit besonderem Namen: PHP ruft sie beim new von selbst auf, und was
in den Klammern hinter new Posten(...) steht, landet in ihren Parametern. Die zwei Unterstriche
sind PHPs Kennzeichen für „diese Methode rufst nicht du auf, sondern die Sprache”.
Der Gewinn steht in der letzten Zeile des Beispiels. new Posten("Tee", 3) scheitert sofort, mit
einem ArgumentCountError, und zwar an der Stelle, an der die Angabe fehlt. Nicht später, nicht in
einer Rechnung, sondern hier.
Damit ist der Konstruktor mehr als eine Abkürzung. Er ist die Liste dessen, was ein Objekt braucht, um überhaupt zu existieren, und PHP setzt sie durch.
Die kurze Form, die seit PHP 8 die halbe Klasse spart
<?php
class Posten
{
public function __construct(
public string $titel,
public int $menge,
public float $preis,
) {
}
public function summe(): float
{
return $this->menge * $this->preis;
}
}
$kaffee = new Posten("Kaffee", 2, 4.90);
$tee = new Posten(titel: "Tee", preis: 2.50, menge: 3);
echo $kaffee->titel, ": ", number_format($kaffee->summe(), 2, ",", "."), "\n";
echo $tee->titel, ": ", number_format($tee->summe(), 2, ",", "."), "\n"; Sieh dir das vorige Beispiel noch einmal an: Jede Eigenschaft steht dreimal da. Einmal als Deklaration oben, einmal als Parameter, einmal in der Zuweisung. Das ist Schreibarbeit, in der keine einzige Entscheidung steckt, und drei Stellen, die auseinanderlaufen können.
Seit PHP 8.0 gibt es dafür die Konstruktor-Promotion. Du schreibst die Sichtbarkeit direkt vor den Parameter, und PHP legt daraus die Eigenschaft an und weist sie zu. Der Konstruktorkörper bleibt leer. Aus zehn Zeilen werden sechs, und der Typ steht weiterhin da: gespart wird die Wiederholung, nicht die Zusage.
Im Reiter Debug sieht man den Unterschied an der Länge der Liste. Mit dem Konstruktor aus dem
vorigen Beispiel stehen dort die drei Zeilen $this->titel = $titel und so weiter, mit den
Parametern titel="Kaffee", menge=2, preis=4.9 daneben. In der kurzen Form gibt es keine
einzige davon, denn der Konstruktor hat keinen Körper, und an der Zeile nach dem new steht das
Objekt fertig da: kaffee=Posten {titel: "Kaffee", menge: 2, preis: 4.9}.
Nebenbei siehst du im Beispiel die benannten Argumente aus Lektion 6.3 wieder:
new Posten(titel: "Tee", preis: 2.50, menge: 3) hält sich nicht an die Reihenfolge und ist
trotzdem eindeutig. Bei drei Werten, die von außen alle gleich aussehen, ist das mehr wert als bei
einer gewöhnlichen Funktion.
Wer darf das anfassen?
<?php
class Posten
{
public function __construct(
public string $titel,
private int $menge,
public readonly float $preis,
) {
}
public function summe(): float
{
return $this->menge * $this->preis;
}
}
$kaffee = new Posten("Kaffee", 2, 4.90);
// 1. public: geht einfach, und genau das ist das Problem.
$kaffee->titel = "Kaffee, gemahlen";
echo "Titel jetzt: ", $kaffee->titel, "\n";
// 2. private: von hier draussen nicht einmal sichtbar.
try {
$kaffee->menge = -3;
} catch (Error $fehler) {
echo "Abgelehnt: ", $fehler->getMessage(), "\n";
}
// 3. readonly: sichtbar, aber einmal gesetzt und fertig.
try {
$kaffee->preis = 0.01;
} catch (Error $fehler) {
echo "Abgelehnt: ", $fehler->getMessage(), "\n";
}
echo "Summe bleibt: ", number_format($kaffee->summe(), 2, ",", "."), "\n"; Vor jeder Eigenschaft und jeder Methode steht ein Wort, das sagt, von wo aus sie erreichbar ist:
publicheißt: von überall. Auch von einer Stelle, die du nie gesehen hast.privateheißt: nur innerhalb dieser Klasse. Von außen ist die Eigenschaft nicht einmal sichtbar, der Zugriff ist einError.protectedliegt dazwischen: diese Klasse und alles, was von ihr erbt. Warum das eine eigene Stufe verdient, wird in Lektion 13.3 klar, vorher brauchst du es nicht.
Im Beispiel stehen die Fälle nebeneinander. Der public-Titel lässt sich von außen überschreiben,
und niemand hält dich auf. Bei private bekommst du Cannot access private property Posten::$menge,
bei readonly Cannot modify readonly property Posten::$preis. Beide sind ein Error, beide sind
mit catch (Error $fehler) zu fangen, so wie in Lektion 7.2 gelernt.
Die richtige Vorgabe ist private, nicht public. Das klingt streng und hat einen einfachen
Grund: Was von außen erreichbar ist, wird von außen benutzt, und danach kannst du es nicht mehr
ändern, ohne fremden Code kaputtzumachen. Was private ist, gehört dir allein. Du machst etwas auf,
wenn jemand es wirklich braucht, und nicht vorsichtshalber.
readonly: einmal setzen, dann steht es fest
readonly ist die dritte Antwort und oft die passendste. Die Eigenschaft ist von außen lesbar,
aber nach dem Erzeugen nicht mehr änderbar. Genau das will man bei den meisten Daten: ein Preis auf
einer Rechnung, eine Bestellnummer, ein Datum. Sie sollen sichtbar sein und sich nicht bewegen.
Zwei Dinge musst du dazu wissen. readonly gibt es seit PHP 8.1, und es verlangt eine
Typangabe: public readonly float $preis geht, public readonly $preis nicht. Gesetzt werden
darf die Eigenschaft genau einmal, und zwar aus der Klasse heraus, also im Konstruktor.
Und wenn du beides kombinierst, private readonly, gewinnt die Sichtbarkeit. Von außen kommt dann
die Meldung über die private Eigenschaft, denn PHP prüft zuerst, ob dich die Eigenschaft überhaupt
etwas angeht, und erst danach, ob sie sich noch ändern lässt. Beides zusammen ist trotzdem sinnvoll:
private hält Fremde draußen, readonly hält dich selbst davon ab, den Wert später aus Versehen
innerhalb der Klasse umzubiegen.
Zum Mitnehmen
Zwei Fragen muss jede Klasse beantworten: Was braucht ein Objekt, damit es überhaupt vollständig ist, und wer darf danach noch daran herumdrehen? Auf die erste antwortet der Konstruktor, auf die zweite drei Schlüsselwörter.
Jetzt du
Basis Konto, kostenlosZu 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 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.