diff --git a/templates/Format/EndGuide b/templates/Format/EndGuide new file mode 100644 index 0000000..08d8a1f --- /dev/null +++ b/templates/Format/EndGuide @@ -0,0 +1,143 @@ +END-GUIDE-STIL (HTML/CSS → PDF via WeasyPrint) + +FORMAT +- A4 Hochformat, ~120–150 Seiten +- @page { size: A4; margin: 18mm; } +- Cover-Seite + automatisches Inhaltsverzeichnis +- Footer: Seitenzahl Mitte, "PHP EndGuide" rechts + +UMFANG (höchste Guide-Stufe) +- 17 Teile, 97 durchnummerierte Kapitel +- Anspruch: vollständig, "lieber zu viel als zu wenig" +- Von der ersten Zeile bis zu Experten-Themen (Architektur, Async, Betrieb) +- Zielversion PHP 8.5 + +ZIELGRUPPE — VOM ANFÄNGER ZUM EXPERTEN +- Beginnt bei null, steigert sich progressiv über die Teile +- Frühe Teile erklären jeden Begriff; spätere setzen Vorheriges voraus +- Verweise nach hinten ("eigener Teil") erlaubt – der Guide wird durchgelesen + oder nachgeschlagen + +KERNPRINZIP — TITEL · ERKLÄRUNG · BEISPIEL (das definierende Muster) +- JEDES Konzept in genau dieser Reihenfolge: + 1. Titel: reiner Konzept-/Verb-/Typname. Nichts sonst. + 2. Erklärung: 1–3 Sätze Prosa. Nennt die Funktionen/Bausteine und was sie tun. + 3. Beispiel: lauffähiger Code mit sichtbarem Ergebnis als Kommentar. +- Die Überschrift trägt NIE die Funktionsliste oder die Definition: + - schlecht: "Ersetzen: str_replace & substr_replace" + "object – Instanz einer Klasse" + - gut: "Ersetzen" + Erklärung nennt str_replace/substr_replace + Beispiel + "object" + Erklärung "Instanz einer Klasse" + Beispiel + +INHALTLICHE PRINZIPIEN +- Vollständigkeit: aufzählbare Mengen IMMER komplett (alle Operatoren, alle + Zuweisungs-Kurzformen, alle Vergleiche, alle Sortiervarianten …). Nur sehr + große Mengen (z. B. Datums-Formatzeichen) als Auswahl + Verweis auf die Doku. +- Mechanik zeigen: Beispiele sind echte, lauffähige Mini-Demos MIT Ausgabe – + nicht nur Signaturen oder leere Interfaces. Bei Mustern/Interfaces: + Vertrag + konkrete Implementierung + Aufruf mit sichtbarem Ergebnis. +- Tiefe vor Aufzählung: erst erklären warum/wann, dann zeigen. +- Keine Performance-Zahlen ohne Quelle. + +CODE-KONVENTIONEN +- Bezeichner englisch: Variablen, Funktionen, Klassen, Methoden, Konstanten, + Enum-Fälle. +- Deutsch bleibt: Fließtext, Überschriften, Code-Kommentare, Ausgabetexte, + Beispieldaten ("Anna", "Köln", "Hallo Welt"). +- Unverändert: PHP-eigene Namen (count, $_GET, int …); SQL-Tabellen-/ + Spaltennamen in Strings (= Daten, kein PHP-Bezeichner). +- Ergebnis-Kommentar an jedem Beispiel: // 19.99 € +- Neutrale Daten: Namen Anna/Ben/Tom, Orte Köln/Hamburg, mail@example.com. + Keine personenbezogenen oder echten Daten. +- Moderne Syntax bevorzugen (match, Enums, Constructor Promotion, readonly, + Property Hooks, Pipe |> …). Versionsabhängiges kennzeichnen ("seit 8.4", + "PHP 8.5"). + +STRUKTUR +1. Cover: Logo, Titel "PHP — Der EndGuide", Subtitle, Badge "17 Teile · 97 Kapitel" +2. Inhaltsverzeichnis (automatisch aus der Struktur erzeugt) +3. Pro Teil: Trennseite (Teil-Nummer · Titel · Beschreibung · Kapitel-Liste) +4. Pro Kapitel: Kapitel-Nummer + Titel, kurzer Lead-Satz, dann H2-Sektionen + nach dem Muster Titel·Erklärung·Beispiel + +NUMMERIERUNG +- Teile (1–17) und Kapitel (durchlaufend 1–97) sind GETRENNTE Zähler. +- Kapitel laufen über den ganzen Guide durch – sie starten NICHT je Teil neu. +- "Teil 7" ≠ "Kapitel 7". + +ELEMENTE +- Fließtext: justify; Erklärungen als Prosa, Aufzählungen sparsam. +- Codeblöcke: dunkler Hintergrund, Syntax-Highlighting, mit Ergebnis-Kommentaren. +- Inline-Code: heller Hintergrund, Hauptfarbe – für jeden Fachbegriff und + Funktionsnamen im Fließtext. +- Callouts in 3 Varianten: tip (✓), warn (!), note (i). Sparsam, höchstens + einer pro Sektion. +- KEINE Übersichts- oder Referenztabellen. Jede Funktion/jedes Feature bekommt + Erklärung + Beispiel inline. + +TYPOGRAFIE & FARBEN (max 3 + Neutrals) +- Hauptfarbe: PHP-Violett; dunkle Code-Blöcke (#1e2a3a-Familie). +- Body Serif; Überschriften Sans-Serif bold; Code Monospace. +- max. 3 Schriftgrößen pro Sektion. + +CALLOUT-NUTZUNG +- tip (✓): Best Practice / Empfehlung. +- warn (!): Sicherheits- oder Stolperfalle (SQL-Injection, Session-Fixation, + ungeprüfte Uploads …). +- note (i): Hintergrund / Abgrenzung ("ORM oder pures PDO?", "nebenläufig ≠ + parallel"). + +BUILD-ARCHITEKTUR (strukturgetrieben) +- Eine Quelle der Wahrheit: STRUCTURE in build.py = Liste aus + (Teil-Kicker, Titel, Beschreibung, [Kapiteltitel]). Daraus entstehen + Reihenfolge, Nummern, Inhaltsverzeichnis und Trennseiten automatisch. +- Inhalt in Modulen content_partN.py: CHAPTERS = { "Kapiteltitel": body_html }. +- Highlight-Span-Klassen im Code: + c=Kommentar, k=Keyword, s=String/Zahl, f=Funktion, + t=Typ/Klasse/Konstante/Enum-Fall, v=Variable, a=Attribut. + Inline-Code im Text: . +- Escaping im Code: < > & als < > & (z. B. ->, =>, &&). +- scrub() filtert beim Bauen personenbezogene Reste (Namen/Orte/Mails) heraus. + +VERMEIDEN +- Funktionsliste oder Definition in der Überschrift (Titel bleibt rein). +- Übersichts-/Referenztabellen ohne Erklärtext. +- Deutsche Code-Bezeichner. +- Personenbezogene oder echte Daten. +- Unvollständige aufzählbare Mengen. +- Reine Interface-/Signatur-Stubs ohne Aufruf + Ausgabe. +- Performance-Zahlen ohne Quelle. +- Codeblock oder Callout über einen Seitenumbruch zerrissen + (page-break-inside: avoid). + +GENERIERUNG & PRÜFUNG +1. python3 build.py → guide.html (meldet Kapitel/Teile + Fortschritt %) +2. weasyprint guide.html guide.pdf +3. Auslieferung nach /mnt/user-data/outputs: cp html und cp pdf als GETRENNTE + Befehle ausführen – NICHT mit && nach einem grep verketten (grep gibt bei 0 + Treffern Exit 1, bricht die Kette ab → eine Datei würde stillschweigend fehlen). +4. In /outputs verifizieren: + - pdfinfo PHP-EndGuide-Neu.pdf | grep Pages (Seitenzahl) + - grep -c 'toc-part' (= 17 Teile) + - grep -c 'chapter-num' (= 97 Kapitel) + - grep -c '' (= 0) + - grep -coE 'echte Namen/Orte/Mails' (= 0) +5. Stichproben rendern (pdftoppm) und ansehen: + - Überschrift = reiner Titel, Erklärung darunter, dann Beispiel? + - Code englisch, Erklärung und Kommentare deutsch? + - SQL-Strings als Daten unverändert? + - Codeblöcke/Callouts nicht über Seitenumbruch zerrissen? + - Cover, Inhaltsverzeichnis und Teil-Trennseiten korrekt? + +CHECKLISTE PRO ABSCHNITT +- [ ] Überschrift ist ein reiner Titel (keine Funktionsliste, keine Definition) +- [ ] Erklärung in Prosa vorhanden, nennt die Bausteine +- [ ] lauffähiges Beispiel mit Ergebnis-Kommentar +- [ ] keine Tabelle +- [ ] englische Bezeichner, deutsche Erklärung/Kommentare/Daten +- [ ] aufzählbare Menge vollständig +- [ ] neutrale Beispieldaten + +INSTALLATION +- pip install weasyprint +- apt install poppler-utils (pdfinfo, pdftoppm, pdftotext) \ No newline at end of file diff --git a/templates/Format/image.png b/templates/Format/image.png deleted file mode 100644 index 32cbf61..0000000 Binary files a/templates/Format/image.png and /dev/null differ diff --git a/templates/Referenz/EndGuide b/templates/Referenz/EndGuide new file mode 100644 index 0000000..0046f19 --- /dev/null +++ b/templates/Referenz/EndGuide @@ -0,0 +1,3909 @@ +``` +PHP – Der EndGuide +
+ +

PHPDer EndGuide

+
Von der ersten Zeile bis in jede Nische – in durchdachter Progression und durchgehend nach einem Prinzip: jedes Thema wird erklärt und sofort mit lauffähigem Beispiel gezeigt. Modernes PHP 8.5.
+
+ PHP 8.5 · Stand 2026 + 17 Teile · 97 Kapitel + Erklärung + Beispiel je Konzept +
+
<?php
+

Inhalt

+
Teil 1 · Fundament
+
1PHP einrichten & ausführen
+
2Variablen & Datentypen
+
3Operatoren & Ausdrücke
+
4Bedingungen & Verzweigungen
+
5Schleifen
+
6Klassische Kontrollstrukturen
+
7Funktionen
+
Teil 2 · Daten verarbeiten: Strings, Arrays & Muster
+
8Strings im Detail
+
9Arrays
+
10Reguläre Ausdrücke: Grundlagen
+
11Reguläre Ausdrücke in der Praxis
+
Teil 3 · Code strukturieren
+
12Code aufteilen: include & require
+
13Namespaces
+
14Composer & Autoloading
+
15Fehler & Exceptions
+
Teil 4 · Das Typsystem & moderne Sprache
+
16Typen & strict_types
+
17Union-, Nullable- & spezielle Typen
+
18Enums
+
19Moderne Syntax
+
Teil 5 · Objektorientierung: Grundlagen
+
20Klassen & Objekte
+
21Sichtbarkeit & Kapselung
+
22Konstruktoren modern
+
23Vererbung
+
24Abstrakte Klassen & Interfaces
+
25Traits
+
26Statisches & Konstanten
+
27Magische Methoden
+
Teil 6 · Objektorientierung: fortgeschritten
+
28Collections & Generics-Denken
+
29Iteratoren & Generatoren
+
30Closures & Bindung
+
31Attribute
+
32Reflection
+
Teil 7 · Die Standardbibliothek
+
33Mathematik & Zahlen
+
34Datum & Zeit
+
35Dateien & Dateisystem
+
36Streams & Filter
+
37SPL-Datenstrukturen
+
Teil 8 · Datenformate & Serialisierung
+
38JSON
+
39XML
+
40CSV & Tabellendaten
+
41serialize & JsonSerializable
+
42Weitere Formate: INI, YAML, Base64
+
Teil 9 · Datenbanken mit PDO
+
43Verbindung & Grundlagen
+
44Abfragen & Prepared Statements
+
45Ergebnisse verarbeiten
+
46Transaktionen
+
47Datenbankdesign-Grundlagen
+
48ORM-Konzepte & Doctrine
+
Teil 10 · Web-Entwicklung mit purem PHP
+
49Wie PHP & Webserver zusammenspielen
+
50Request & Superglobals
+
51Formulare verarbeiten
+
52Eingabevalidierung & Filter
+
53Sessions
+
54Cookies
+
55Datei-Uploads
+
56HTTP-Clients & APIs
+
Teil 11 · Sicherheit
+
57Das Grundprinzip: niemals vertrauen
+
58SQL-Injection im Detail
+
59XSS & CSRF im Detail
+
60Authentifizierung & Passwörter
+
61Sessions & Cookies absichern
+
62Kryptografie & Sodium
+
Teil 12 · Sauberes Design
+
63SOLID-Prinzipien
+
64Dependency Injection
+
65Wertobjekte & DTOs
+
66Fehlerbehandlung als Architektur
+
67Häufige Entwurfsmuster
+
Teil 13 · Entwurfsmuster & Architektur
+
68Erzeugungsmuster
+
69Strukturmuster
+
70Verhaltensmuster
+
71MVC & Schichtenarchitektur
+
72Domain-Driven Design
+
73Event-getriebene Architektur
+
74Hexagonale Architektur
+
Teil 14 · Qualität & Performance
+
75Testen mit PHPUnit
+
76Statische Analyse
+
77Code-Style & Tooling
+
78Debugging & Xdebug
+
79Performance & OPcache
+
80Speicher & Referenzen
+
Teil 15 · CLI, Prozesse & asynchrones PHP
+
81CLI-Programme bauen
+
82Prozesse & FFI
+
83Das klassische Modell & Fibers
+
84Event-Loops & ReactPHP
+
85Swoole, Amp & FrankenPHP
+
86Echte Parallelität
+
Teil 16 · Das Ökosystem
+
87Composer in der Tiefe
+
88Die PSR-Standards
+
89Symfony im Überblick
+
90Laravel im Überblick
+
91Wichtige Bibliotheken
+
Teil 17 · In Produktion
+
92Projektstruktur & Organisation
+
93Deployment & Betrieb
+
94Logging & Monitoring
+
95Internationalisierung
+
96Migration: von PHP 5 zu 8.5
+
97Weiterlernen & Community
+
Teil 1

Fundament

Der Einstieg: PHP einrichten, die Bausteine der Sprache und der vollständige Kontrollfluss – genug, um echte kleine Programme zu schreiben.
1 · PHP einrichten & ausführen2 · Variablen & Datentypen3 · Operatoren & Ausdrücke4 · Bedingungen & Verzweigungen5 · Schleifen6 · Klassische Kontrollstrukturen7 · Funktionen
Kapitel 1

PHP einrichten & ausführen

PHP ist eine serverseitige Skriptsprache: Der Code läuft auf dem Server und erzeugt meist HTML für den Browser. Ebenso treibt PHP Kommandozeilen-Programme, Skripte und APIs an. In diesem Kapitel bringen wir das erste Skript zum Laufen.

+ +

PHP-Tags

+

PHP-Code steht zwischen <?php und ?>. Alles außerhalb der Tags wird unverändert ausgegeben – so verzahnt sich PHP mit HTML:

+
<p>Statischer Text.</p>
+<?php echo "Dynamisch aus PHP."; ?>
+

In reinen PHP-Dateien (ohne HTML) lässt man das schließende ?> bewusst weg – das verhindert versehentliche Leerzeichen in der Ausgabe.

+ +

Etwas ausgeben

+
echo "Hallo Welt\n";     // gibt Text aus
+print "geht auch\n";     // wie echo, liefert zusätzlich 1 zurück
+var_dump([1, 2]);          // Debug-Ausgabe mit Typ und Struktur
+ +

Anweisungen & Semikolon

+

Jede Anweisung endet mit einem Semikolon; Zeilenumbrüche sind für PHP bedeutungslos:

+
$a = 5;
+$b = 10; echo $a + $b;   // 15
+ +

Kommentare

+
// einzeilig
+# ebenfalls einzeilig
+/* mehrzeilig –
+   über mehrere Zeilen */
+ +

Ein Skript ausführen

+

Zwei Wege führen ein Skript aus. Über die Kommandozeile direkt:

+
# Datei ausführen
+php skript.php
+

Oder mit dem eingebauten Entwicklungs-Webserver – ideal zum Ausprobieren ohne Apache/Nginx:

+
# bedient http://localhost:8000 aus dem aktuellen Ordner
+php -S localhost:8000
+ +
+
i
+
Version & Konfigurationphp -v zeigt die installierte Version (dieser Guide setzt PHP 8.5 voraus). Globale Einstellungen stehen in der php.ini; ihren Pfad verrät php --ini. Den eingebauten Server nutzt man nur zur Entwicklung, nie in Produktion.
+
Kapitel 2

Variablen & Datentypen

Eine Variable ist ein benannter Behälter für einen Wert. PHP ist dynamisch typisiert: Der Typ ergibt sich aus dem Wert und darf wechseln. Es gibt acht Grundtypen – hier jeder einzeln mit Beispiel.

+ +

Variablen

+

Ein $, dann der Name. Zuweisen mit =:

+
$name = "Anna";
+$age = 30;
+echo "$name ist $age";   // Anna ist 30
+ +

int

+

Ganzzahlen ohne Nachkommastellen – auch hexadezimal, binär oder mit Unterstrichen lesbar geschrieben:

+
$n = 42;
+var_dump($n);          // int(42)
+var_dump(0xFF, 0b101, 1_000_000);   // hex, binär, mit Trennstrichen lesbar
+ +

float

+

Zahlen mit Nachkommastellen (Fließkommazahlen):

+
$pi = 3.14;
+var_dump($pi);         // float(3.14)
+ +

string

+

Zeichenketten, also beliebiger Text:

+
$s = "Text";
+var_dump($s);          // string(4) "Text"
+ +

bool

+

Ein Wahrheitswert mit nur zwei möglichen Inhalten, true oder false:

+
$ok = true;
+var_dump($ok);         // bool(true)
+ +

null

+

Steht für „kein Wert" – eine Variable, die bewusst nichts enthält:

+
$empty = null;
+var_dump($empty);       // NULL
+ +

array

+

Liste oder Schlüssel-Wert-Sammlung; ein eigenes Kapitel widmet sich ihr vollständig:

+
$list = [1, 2, 3];
+var_dump(count($list));   // int(3)
+ +

object

+

Eine konkrete Instanz einer Klasse; die Objektorientierung vertieft das ein eigener Teil:

+
$obj = new stdClass();
+$obj->x = 1;
+echo $obj->x;        // 1
+

Die beiden verbleibenden Grundtypen – callable (aufrufbares) und resource (Handle auf z. B. eine Datei) – begegnen uns später bei Funktionen und Dateien.

+ +

Typ prüfen & umwandeln

+
echo gettype(42);          // integer
+var_dump(is_int(42));      // bool(true)
+var_dump((int) "13 Äpfel");   // int(13) – explizite Umwandlung
+var_dump((bool) 0);          // bool(false)
+ +

Typ-Jonglage

+

PHP wandelt Typen bei Bedarf automatisch um – bequem, aber eine Fehlerquelle. Der Operator entscheidet, wie ein Wert gelesen wird:

+
echo "5" + 3;     // 8   – der String wird zur Zahl
+echo "5" . 3;     // 53  – der Punkt verkettet zu Text
+ +

Konstanten

+

Ein Wert, der sich nach dem Festlegen nicht mehr ändert:

+
const VAT = 0.19;
+define("VERSION", "1.0");
+echo VAT;          // 0.19
+echo VERSION;       // 1.0
+ +
+
+
Aussagekräftige NamenVariablennamen sind in PHP case-sensitiv ($name$Name) und beginnen mit Buchstabe oder Unterstrich. Wähle sprechende Namen ($gesamtPreis statt $gp) – Code wird viel öfter gelesen als geschrieben.
+
Kapitel 3

Operatoren & Ausdrücke

Operatoren verknüpfen Werte zu neuen Werten – für jede Berechnung, jeden Vergleich, jede Bit-Manipulation. Hier vollständig, jeder mit Beispiel.

+ +

Arithmetik

+
echo 7 + 3;     // 10
+echo 7 - 3;     // 4
+echo 7 * 3;     // 21
+echo 7 / 2;     // 3.5
+echo 7 % 3;     // 1   (Rest der Division, „Modulo")
+echo 2 ** 8;    // 256 (Potenz)
+ +

Zuweisungs-Kurzformen

+

Zu jedem binären Operator gibt es eine zusammengesetzte Zuweisung – sie wendet den Operator an und schreibt zurück. $x += 3 ist $x = $x + 3:

+
$x = 10;
+$x += 3;    // 13   Addition
+$x -= 1;    // 12   Subtraktion
+$x *= 2;    // 24   Multiplikation
+$x /= 4;    // 6    Division
+$x %= 4;    // 2    Modulo
+$x **= 3;   // 8    Potenz
+

Dazu die Kurzformen für Strings und Null-Koaleszenz sowie – analog – für jeden Bit-Operator:

+
$s = "Hallo";
+$s .= " Welt";     // "Hallo Welt"  – String anhängen
+
+$name = null;
+$name ??= "Gast";   // "Gast" – nur zuweisen, wenn null/ungesetzt
+
+$f = 0b0011;
+$f &= 0b0110;     // UND-Zuweisung
+$f |= 0b1000;     // ODER-Zuweisung
+$f ^= 0b0001;     // XOR-Zuweisung
+$f <<= 1;         // links schieben
+$f >>= 2;         // rechts schieben
+ +

Inkrement & Dekrement

+

Um 1 erhöhen/verringern. Die Stellung entscheidet, ob vor oder nach der Auswertung verändert wird:

+
$i = 5;
+echo $i++;   // 5  – erst ausgeben, dann erhöhen ($i ist danach 6)
+echo ++$i;   // 7  – erst erhöhen, dann ausgeben
+$i--;        // verringern
+ +

Vergleiche

+

== prüft den Wert mit Typumwandlung, === zusätzlich den Typ:

+
var_dump(1 == "1");    // true  – Werte gleich
+var_dump(1 === "1");   // false – verschiedene Typen
+

Für „ungleich" gibt es != (und gleichbedeutend <>) sowie streng !==:

+
var_dump(5 != 3);     // true
+var_dump(5 <> 3);     // true  – identisch zu !=
+var_dump(5 !== "5");   // true  – Typ verschieden
+

Die Größenvergleiche und der Raumschiff-Operator, der -1, 0 oder 1 liefert:

+
var_dump(3 < 5, 5 >= 5);   // true, true
+echo 3 <=> 5;             // -1  (links kleiner; 0 = gleich, 1 = größer)
+ +

Logik

+
$a = true;  $b = false;
+var_dump($a && $b);   // false (und)
+var_dump($a || $b);   // true  (oder)
+var_dump(!$a);        // false (nicht)
+

Es gibt auch die Wortformen and, or, xor. Achtung: Sie binden schwächer als = – eine berüchtigte Falle:

+
$ok = true && false;    // false  (&& bindet stärker als =)
+$ok = true and false;   // true!  (= greift vor „and")
+var_dump(true xor false);   // true  – genau einer ist wahr
+ +

Bit-Operatoren

+

Arbeiten direkt auf den Bits einer Ganzzahl – für Flags, Masken, hardwarenahe Rechnung:

+
echo 6 & 3;     // 2   UND:  110 & 011 = 010
+echo 6 | 3;     // 7   ODER: 110 | 011 = 111
+echo 6 ^ 3;     // 5   XOR:  110 ^ 011 = 101
+echo ~5;        // -6  NICHT (alle Bits kippen)
+echo 1 << 4;    // 16  links schieben  (×2 je Stelle)
+echo 32 >> 2;   // 8   rechts schieben (÷2 je Stelle)
+ +

String-, ternärer & Null-Koaleszenz-Operator

+
echo "Hallo" . " " . "Welt";   // Hallo Welt (verketten)
+echo $age >= 18 ? "erwachsen" : "minderjährig";   // ternär
+echo $name ?? "Gast";     // erster nicht-null Wert
+ +
+
!
+
Zwei Fallen== wandelt Typen um und überrascht (vergleiche im Zweifel mit ===). Und and/or binden schwächer als die Zuweisung, weshalb $x = $a or $b selten das tut, was man denkt – nutze && und ||.
+
Kapitel 4

Bedingungen & Verzweigungen

Verzweigungen entscheiden, welcher Code unter welchen Umständen läuft. PHP bietet von der klassischen if-Kette bis zum modernen match mehrere Werkzeuge.

+ +

if / elseif / else

+

Der erste zutreffende Block läuft, der Rest wird übersprungen:

+
$points = 75;
+if ($points >= 90) {
+    echo "Sehr gut";
+} elseif ($points >= 50) {
+    echo "Bestanden";   // läuft
+} else {
+    echo "Durchgefallen";
+}
+ +

Der ternäre Operator

+

Eine kompakte if/else-Form, die einen Wert liefert:

+
$status = $age >= 18 ? "erwachsen" : "minderjährig";
+

Die Kurzform ?: nimmt den linken Wert, wenn er „truthy" ist:

+
$name = $input ?: "Gast";   // $input, sonst "Gast"
+ +

Null-Koaleszenz

+

Liefert den ersten Wert, der existiert und nicht null ist – ideal für Standardwerte ohne Fehler bei fehlenden Schlüsseln:

+
$page = $_GET["seite"] ?? "start";   // kein Warning, wenn nicht gesetzt
+ +

match

+

Seit PHP 8 kompakt, typsicher (===) und mit Rückgabewert. Anders als switch gibt es kein versehentliches Durchfallen:

+
$tag = 3;
+$name = match($tag) {
+    1, 2, 3, 4, 5 => "Werktag",
+    6, 7          => "Wochenende",
+    default        => "ungültig",
+};
+echo $name;   // Werktag
+

match kann auch ohne Argument als saubere if-Kette dienen, indem die Bedingungen direkt geprüft werden:

+
$note = match(true) {
+    $points >= 90 => "1",
+    $points >= 50 => "3",
+    default      => "5",
+};
+ +
+
+
match vor switchFür Mehrfachverzweigungen mit Rückgabewert ist match meist die bessere Wahl: typsicher, ausdrucksstark und ohne break. Den klassischen switch – und wann er noch sinnvoll ist – behandelt das nächste Kapitel.
+
Kapitel 5

Schleifen

Schleifen wiederholen Code. PHP hat vier Formen; welche passt, hängt davon ab, ob du über eine Sammlung läufst oder einen Zähler steuerst.

+ +

while

+

Wiederholt, solange die Bedingung wahr ist – sie wird vor jedem Durchlauf geprüft:

+
$i = 1;
+while ($i <= 3) {
+    echo $i;   // 1, 2, 3
+    $i++;
+}
+ +

do-while

+

Wie while, aber die Prüfung erfolgt am Ende – der Block läuft also mindestens einmal:

+
$attempt = 0;
+do {
+    $attempt++;
+} while ($attempt < 3);
+echo $attempt;   // 3
+ +

for

+

Drei Teile in einer Zeile: Initialisierung, Bedingung, Schritt. Ideal, wenn die Anzahl bekannt ist:

+
for ($i = 0; $i < 3; $i++) {
+    echo $i;   // 0, 1, 2
+}
+ +

foreach

+

Läuft über jedes Element einer Sammlung, ohne Zähler:

+
$colors = ["rot", "grün"];
+foreach ($colors as $color) {
+    echo $color;   // rot, grün
+}
+

Mit Schlüssel und Wert zugleich:

+
$prices = ["apfel" => 2, "birne" => 3];
+foreach ($prices as $name => $price) {
+    echo "$name: $price\n";   // apfel: 2 / birne: 3
+}
+

Mit & bekommst du eine Referenz und veränderst das Original:

+
foreach ($prices as &$p) { $p *= 2; }
+// alle Preise verdoppelt
+ +

break & continue

+

break verlässt die Schleife, continue springt zum nächsten Durchlauf:

+
foreach ([1, -2, 3, 99] as $n) {
+    if ($n < 0) continue;   // negative überspringen
+    if ($n > 10) break;     // ab 11 abbrechen
+    echo $n;                  // 1, 3
+}
+

In verschachtelten Schleifen verlässt break 2; zwei Ebenen auf einmal:

+
foreach ($lines as $line) {
+    foreach ($line as $cell) {
+        if ($cell === null) break 2;   // beide Schleifen verlassen
+    }
+}
+ +
+
+
foreach ist dein StandardIn der Praxis ist foreach die mit Abstand häufigste Schleife. for brauchst du nur, wenn du den Zähler selbst kontrollieren musst; while, wenn die Anzahl der Durchläufe vorher unbekannt ist.
+
Kapitel 6

Klassische Kontrollstrukturen

Neben if, match und den Schleifen kennt PHP weitere Steuerungskonstrukte. Im Alltag seltener, in fremdem Code aber häufig – du solltest sie lesen und einordnen können.

+ +

switch

+

Die klassische Mehrfachverzweigung. Jeder Zweig braucht ein break, sonst läuft die Ausführung in den nächsten Fall durch:

+
switch ($role) {
+    case "admin":
+        $rights = "alle";
+        break;
+    case "redakteur":
+        $rights = "schreiben";
+        break;
+    default:
+        $rights = "lesen";
+}
+

Der Vergleich ist locker (==), nicht typsicher – genau deshalb ist match heute meist besser.

+ +

Gewolltes Fall-Through

+

Mehrere Fälle teilen sich einen Block, indem das break weggelassen wird:

+
switch ($tag) {
+    case "Sa":
+    case "So":
+        echo "Wochenende";
+        break;
+    default:
+        echo "Werktag";
+}
+ +

Alternative Syntax für Templates

+

In HTML-Vorlagen sind geschweifte Klammern unübersichtlich. PHP bietet eine Doppelpunkt-Form mit sprechendem Abschluss – ideal zwischen HTML:

+
<?php if ($loggedIn): ?>
+  <p>Willkommen zurück</p>
+<?php else: ?>
+  <a href="/login">Anmelden</a>
+<?php endif; ?>
+

Dieselbe Form gibt es als endforeach, endfor, endwhile und endswitch.

+ +

goto

+

PHP hat ein goto, das zu einer Marke springt. Es macht den Programmfluss schwer nachvollziehbar und hat in modernem Code praktisch keinen Platz – du wirst es höchstens in sehr altem Code antreffen:

+
goto ende;
+echo "wird übersprungen";
+ende:
+echo "hier geht es weiter";
+ +
+
i
+
switch oder match?Nutze match, wenn du einen Wert zurückgibst und typsicher vergleichen willst. switch bleibt brauchbar, wenn ein Fall mehrere Anweisungen ausführt oder du bewusst Fall-Through nutzt.
+
Kapitel 7

Funktionen

Eine Funktion bündelt Code, den du benennen und wiederverwenden kannst. Sie nimmt Eingaben (Parameter) und liefert oft ein Ergebnis (Rückgabewert). PHP bietet dabei viel: Typen, Standardwerte, benannte und beliebig viele Argumente.

+ +

Definition & Aufruf

+
function greet($name) {
+    echo "Hallo, $name!\n";
+}
+greet("Anna");   // Hallo, Anna!
+ +

Rückgabewerte

+

return beendet die Funktion und liefert einen Wert zurück:

+
function add($a, $b) {
+    return $a + $b;
+}
+echo add(3, 5);   // 8
+ +

Typdeklarationen

+

Parameter- und Rückgabetypen machen Absichten klar und fangen Fehler früh ab:

+
function area(float $b, float $h): float {
+    return $b * $h;
+}
+echo area(2.5, 4);   // 10
+ +

Standardwerte

+

Parameter mit Vorgabe dürfen beim Aufruf weggelassen werden:

+
function connect(string $host, int $port = 3306): string {
+    return "$host:$port";
+}
+echo connect("localhost");        // localhost:3306
+echo connect("localhost", 5432);  // localhost:5432
+ +

Benannte Argumente

+

Seit PHP 8 kannst du Argumente beim Namen nennen – die Reihenfolge ist dann egal, und optionale lassen sich gezielt setzen:

+
echo connect(port: 5432, host: "db");   // db:5432
+ +

Beliebig viele Argumente

+

Drei Punkte sammeln alle weiteren Argumente in ein Array:

+
function sum(int ...$numbers): int {
+    return array_sum($numbers);
+}
+echo sum(1, 2, 3, 4);   // 10
+ +

Übergabe per Referenz

+

Mit & arbeitet die Funktion am Original statt an einer Kopie:

+
function increment(int &$n): void {
+    $n++;
+}
+$x = 5;
+increment($x);
+echo $x;   // 6 – $x selbst wurde verändert
+ +

Pfeilfunktionen

+

Kurzschreibweise für kleine Funktionen; sie übernehmen den Geltungsbereich automatisch:

+
$double = fn($n) => $n * 2;
+echo $double(21);   // 42
+ +
+
+
Eine Funktion, eine AufgabeEine gute Funktion tut genau eine Sache und hat einen Namen, der das verrät. Wird sie lang oder schwer benennbar, ist das ein Zeichen, sie aufzuteilen. Funktionen als Werte (Closures) und ihre Feinheiten vertieft ein späteres Kapitel.
+
Teil 2

Daten verarbeiten: Strings, Arrays & Muster

Die Werkzeuge, mit denen du täglich Daten formst: Texte, Listen und – für variable Muster – reguläre Ausdrücke von Grund auf.
8 · Strings im Detail9 · Arrays10 · Reguläre Ausdrücke: Grundlagen11 · Reguläre Ausdrücke in der Praxis
Kapitel 8

Strings im Detail

Text ist allgegenwärtig. PHP bietet dutzende String-Funktionen – hier die wichtigsten, jede einzeln erklärt und sofort gezeigt.

+ +

Einfache vs. doppelte Anführungszeichen

+

In doppelten Anführungszeichen werden Variablen eingesetzt (Interpolation), in einfachen nicht:

+
$name = "Anna";
+echo "Hallo $name";     // Hallo Anna
+echo 'Hallo $name';     // Hallo $name
+echo "Wert: {$arr['x']}";   // {} grenzt komplexe Ausdrücke ab
+ +

Heredoc & Nowdoc

+

Für lange Texte. Heredoc interpoliert wie doppelte Quotes, Nowdoc (mit '…') wie einfache:

+
$html = <<<HTML
+<h1>$name</h1>
+HTML;
+$raw = <<<'TXT'
+$name bleibt wörtlich.
+TXT;
+ +

Länge

+

strlen zählt Bytes, mb_strlen zählt Zeichen – bei Umlauten ein Unterschied:

+
$w = "Größe";
+echo strlen($w);      // 6 (ö belegt 2 Bytes)
+echo mb_strlen($w);   // 5 (Zeichen)
+ +

Ausschnitt

+

substr schneidet einen Teil nach Byte-Position heraus, mb_substr arbeitet zeichenweise und damit umlautsicher:

+
echo substr("Hallo Welt", 0, 5);   // Hallo
+echo substr("Hallo Welt", -4);     // Welt (von hinten)
+echo mb_substr("Größe", 0, 3);    // Grö (umlautsicher)
+ +

Suchen

+

Die drei modernen Funktionen liefern direkt bool; strpos liefert die Position oder false:

+
$s = "info@example.com";
+var_dump(str_contains($s, "@"));        // true
+var_dump(str_starts_with($s, "info"));  // true
+var_dump(str_ends_with($s, ".com"));   // true
+echo strpos($s, "@");                  // 4
+ +

Ersetzen

+

str_replace tauscht alle Vorkommen eines Teilstrings aus, substr_replace ersetzt einen Bereich nach Position und Länge:

+
echo str_replace("Welt", "PHP", "Hallo Welt");   // Hallo PHP
+echo str_replace(["a", "e"], ["4", "3"], "hase");   // h4s3 (mehrere)
+echo substr_replace("2026-01-01", "12", 5, 2);    // 2026-12-01
+ +

Groß-/Kleinschreibung

+

strtoupper und strtolower wandeln komplett um, ucfirst nur den ersten Buchstaben, ucwords jeden Wortanfang:

+
echo strtoupper("php");              // PHP
+echo strtolower("PHP");              // php
+echo ucfirst("hallo");              // Hallo
+echo ucwords("max mustermann");      // Max Mustermann
+echo mb_strtoupper("größe");        // GRÖSSE (umlautsicher)
+ +

Trimmen

+

Entfernt Zeichen an den Rändern – trim auf beiden Seiten, ltrim und rtrim nur links bzw. rechts:

+
echo "[" . trim("  text  ") . "]";   // [text]
+echo ltrim("  text");                   // "text" (nur links)
+echo rtrim("datei.tmp", ".tmp");        // "datei" (diese Zeichen rechts)
+ +

Aufteilen & Zusammenfügen

+

explode zerlegt einen String an einem Trennzeichen in ein Array, implode fügt ein Array zu einem String zusammen, str_split zerlegt in einzelne Zeichen:

+
$parts = explode(",", "php,sql,css");   // ["php", "sql", "css"]
+echo implode(" / ", $parts);          // php / sql / css
+print_r(str_split("abc"));            // ["a", "b", "c"]
+ +

Formatieren

+

printf/sprintf bauen Text nach einer Vorlage, number_format formatiert Zahlen, str_pad füllt auf eine Mindestlänge auf, str_repeat wiederholt:

+
printf("%s ist %d Jahre alt.\n", "Anna", 30);
+echo sprintf("%.2f €", 3.5);                  // 3.50 €
+echo number_format(1234567.89, 2, ",", ".");   // 1.234.567,89
+echo str_pad("7", 3, "0", STR_PAD_LEFT);          // 007
+echo str_repeat("=-", 3);                        // =-=-=-
+ +

Vergleichen

+

strcmp vergleicht zwei Strings (Rückgabe negativ, 0 oder positiv), strcasecmp tut dasselbe ohne Rücksicht auf Groß-/Kleinschreibung:

+
var_dump("abc" === "abc");          // true (Wert + Typ)
+echo strcmp("a", "b");              // -1 (a kommt vor b)
+echo strcasecmp("PHP", "php");       // 0 (gleich, Groß/Klein egal)
+ +
+
+
mb_-Funktionen bei UmlautenSobald Text Umlaute, Akzente oder Emojis enthalten kann, nutze die mb_*-Varianten. Die Byte-Funktionen zerschneiden sonst Mehrbyte-Zeichen. Setze früh mb_internal_encoding("UTF-8").
+
Kapitel 9

Arrays

Ein Array speichert mehrere Werte unter einem Namen – als Liste, Schlüssel-Wert-Sammlung oder verschachtelt. PHP hat eine der größten Funktionsbibliotheken dafür; hier jede gruppiert nach Zweck, einzeln gezeigt.

+ +

Anlegen & Zugriff

+
$colors = ["rot", "grün", "blau"];
+echo $colors[0];     // rot
+$person = ["name" => "Anna", "alter" => 30];
+echo $person["name"];   // Anna
+ +

Hinzufügen

+

Elemente anfügen – [] hängt am Ende an, array_push auch mehrere auf einmal, array_unshift am Anfang:

+
$l = ["a", "b"];
+$l[] = "c";                 // ["a","b","c"] – am Ende
+array_push($l, "d", "e");    // mehrere am Ende
+array_unshift($l, "x");     // am Anfang
+ +

Entfernen

+

Elemente herausnehmen – array_pop das letzte, array_shift das erste (beide liefern es zurück), unset löscht einen bestimmten Index:

+
$last = array_pop($l);     // entfernt & liefert das letzte
+$first  = array_shift($l);   // entfernt & liefert das erste
+unset($l[1]);                 // löscht Index 1
+ +

Suchen

+

Prüfen, ob und wo ein Wert vorkommt – in_array liefert einen Wahrheitswert, array_search den Schlüssel, array_key_exists prüft auf einen Schlüssel:

+
$fruit = ["apfel", "birne"];
+var_dump(in_array("birne", $fruit, true));   // true (strikt)
+echo array_search("birne", $fruit);          // 1 (Schlüssel)
+var_dump(array_key_exists(0, $fruit));         // true
+ +

Schlüssel & Werte

+

Schlüssel und Werte auslesen und umformen:

+
$p = ["apfel" => 2, "birne" => 3];
+print_r(array_keys($p));     // ["apfel", "birne"]
+print_r(array_values($p));   // [2, 3]
+print_r(array_flip($p));     // [2 => "apfel", 3 => "birne"]
+print_r(array_unique([1, 1, 2]));   // [1, 2]
+print_r(array_combine(["a", "b"], [1, 2]));   // ["a"=>1, "b"=>2]
+
+$team = [["name" => "Anna"], ["name" => "Ben"]];
+print_r(array_column($team, "name"));   // ["Anna", "Ben"]
+ +

Transformieren

+

Aus einem Array ein neues bilden – array_map wandelt jedes Element um, array_filter siebt aus, array_reduce fasst zu einem Wert zusammen, array_walk verändert direkt:

+
$z = [1, 2, 3, 4];
+print_r(array_map(fn($n) => $n ** 2, $z));   // [1, 4, 9, 16]
+print_r(array_filter($z, fn($n) => $n % 2 === 0));   // [1=>2, 3=>4]
+echo array_reduce($z, fn($t, $n) => $t + $n, 0);   // 10
+array_walk($z, function(&$n) { $n *= 10; });   // verändert direkt
+

Ohne Callback entfernt array_filter alle „falsy" Werte (0, "", null, false). array_filter behält die Schlüssel – mit array_values nummerierst du neu durch.

+ +

Sortieren

+

Alle Sortierfunktionen verändern das Array an Ort und Stelle. sort/rsort sortieren nach Wert auf- bzw. absteigend und vergeben neue Schlüssel:

+
$z = [3, 1, 2];
+sort($z);    // [1, 2, 3]
+rsort($z);   // [3, 2, 1]
+

asort/arsort sortieren nach Wert, aber erhalten die Schlüssel; ksort/krsort sortieren nach Schlüssel:

+
$m = ["b" => 2, "a" => 1];
+asort($m);    // a=>1, b=>2 (nach Wert, Schlüssel bleiben)
+ksort($m);    // a, b (nach Schlüssel)
+krsort($m);   // b, a (Schlüssel absteigend)
+

Mit einer eigenen Vergleichsfunktion sortiert usort (neue Schlüssel), uasort (Schlüssel bleiben) bzw. uksort (auf Schlüssel). natsort sortiert „natürlich":

+
$people = [["alter" => 40], ["alter" => 30]];
+usort($people, fn($a, $b) => $a["alter"] <=> $b["alter"]);   // 30 vor 40
+$d = ["datei10", "datei2"];
+natsort($d);   // datei2 vor datei10
+ +

Kombinieren & Aufteilen

+

Mehrere Arrays zusammenführen oder eines zerteilen:

+
print_r(array_merge([1, 2], [3, 4]));      // [1, 2, 3, 4]
+print_r(array_slice(["a", "b", "c"], 1, 2));   // ["b", "c"]
+$x = ["a", "b", "c"];
+array_splice($x, 1, 1, ["B1", "B2"]);     // ["a","B1","B2","c"]
+print_r(array_chunk([1, 2, 3], 2));     // [[1,2], [3]]
+print_r(array_diff([1, 2, 3], [2]));     // [0=>1, 2=>3]
+print_r(array_intersect([1, 2], [2, 9]));   // [1=>2]
+ +

Aggregieren

+

Die Werte eines Arrays zu einer Kennzahl zusammenfassen:

+
$w = [10, 3, 7];
+echo count($w);            // 3
+echo array_sum($w);        // 20
+echo array_product($w);    // 210
+echo max($w);              // 10
+echo min($w);              // 3
+ +
+
+
map/filter/reduce statt SchleifenFür Transformationen sind diese drei oft klarer als eine foreach-Schleife – sie sagen direkt, was passiert (umwandeln, aussieben, zusammenfassen). Sie lassen sich verketten und mit dem Pipe-Operator von PHP 8.5 von links nach rechts lesbar machen.
+
Kapitel 10

Reguläre Ausdrücke: Grundlagen

Reguläre Ausdrücke (Regex) beschreiben Muster in Text – etwa „eine Folge von Ziffern" oder „etwas, das wie eine E-Mail aussieht". Wo feste Teilstrings nicht reichen, sind sie das Werkzeug. PHP nutzt die PCRE-Syntax.

+ +

Aufbau eines Musters

+

Ein Muster steht zwischen Trennzeichen (meist /). preg_match liefert 1 bei Treffer:

+
var_dump(preg_match("/katze/", "meine katze"));   // 1 (Treffer)
+var_dump(preg_match("/hund/", "meine katze"));    // 0
+ +

Zeichenklassen

+

\d Ziffer, \w Wortzeichen, \s Leerraum, . beliebiges Zeichen. In eckigen Klammern eine eigene Auswahl:

+
var_dump(preg_match("/\d/", "abc7"));      // 1 (enthält Ziffer)
+var_dump(preg_match("/[aeiou]/", "xyz"));   // 0 (kein Vokal)
+var_dump(preg_match("/[^0-9]/", "123"));   // 0 (^ negiert: kein Nicht-Ziffer)
+ +

Quantoren

+

* = null oder mehr, + = eins oder mehr, ? = optional, {n,m} = Bereich:

+
var_dump(preg_match("/^\d+$/", "12345"));        // 1 (nur Ziffern)
+var_dump(preg_match("/^a\d{3}$/", "a042"));      // 1 (a + genau 3 Ziffern)
+var_dump(preg_match("/colou?r/", "color"));      // 1 (u optional)
+ +

Anker

+

^ bindet an den Anfang, $ ans Ende – wichtig, um den ganzen String zu prüfen statt nur eines Teils:

+
var_dump(preg_match("/^\d+$/", "12a"));   // 0 (a am Ende)
+var_dump(preg_match("/\d+/", "12a"));     // 1 (Teil reicht ohne Anker)
+ +

Modifikatoren

+

Nach dem schließenden Trennzeichen: i ignoriert Groß/Klein, m mehrzeilig, u für UTF-8:

+
var_dump(preg_match("/php/i", "Ich liebe PHP"));   // 1 (i: Groß/Klein egal)
+ +
+
!
+
Sonderzeichen maskierenZeichen wie . + * ? ( ) [ ] haben in Regex eine Bedeutung. Willst du sie wörtlich suchen, stelle einen Backslash voran (\.) oder nutze preg_quote(). Für reine Festtext-Suche bleibt str_contains einfacher und schneller.
+
Kapitel 11

Reguläre Ausdrücke in der Praxis

Mit den Grundlagen im Rücken geht es ans Anwenden: Treffer auslesen, ersetzen, aufteilen – und die typischen Fallstricke vermeiden.

+ +

preg_match mit Gruppen

+

Klammern bilden Gruppen, deren Inhalt im Treffer-Array landet – so liest du Teile aus:

+
preg_match("/(\d{4})-(\d{2})-(\d{2})/", "Datum: 2026-05-31", $m);
+echo $m[0];   // 2026-05-31 (ganzer Treffer)
+echo $m[1];   // 2026 (erste Gruppe)
+echo $m[2];   // 05
+ +

Benannte Gruppen

+

(?<name>…) macht den Code lesbarer als Zahlenindizes:

+
preg_match("/(?<jahr>\d{4})-(?<monat>\d{2})/", "2026-05", $m);
+echo $m["jahr"];    // 2026
+echo $m["monat"];   // 05
+ +

preg_match_all

+

Findet nicht nur den ersten, sondern alle Treffer und sammelt sie:

+
preg_match_all("/\d+/", "a1 b22 c333", $m);
+print_r($m[0]);   // ["1", "22", "333"]
+ +

preg_replace

+

Mit $1 greifst du im Ersatz auf Gruppen zurück:

+
echo preg_replace("/\s+/", " ", "zu   viel   Raum");   // "zu viel Raum"
+echo preg_replace("/(\d{4})-(\d{2})/", "$2/$1", "2026-05");   // "05/2026"
+ +

preg_replace_callback

+

Ersetzt Treffer durch das Ergebnis einer Funktion – so kann die Ersetzung von jedem Treffer abhängen:

+
echo preg_replace_callback("/\d+/",
+    fn($t) => $t[0] * 2, "a3 b10");   // "a6 b20"
+ +

preg_split

+

Zerlegt einen String dort, wo ein Muster passt:

+
print_r(preg_split("/[\s,]+/", "a, b,  c"));   // ["a", "b", "c"]
+ +

Lookarounds

+

Ein Lookahead (?=…) prüft, was folgt, ohne es zu verbrauchen – hier: nur Zahlen vor „€":

+
preg_match_all("/\d+(?= €)/", "5 € und 10 Stück", $m);
+print_r($m[0]);   // ["5"]  (10 wird nicht von € gefolgt)
+ +
+
!
+
Gier vermeidenQuantoren sind „gierig": .* greift so viel wie möglich. In "<a><b>" trifft /<.*>/ den ganzen String. Mit ? machst du sie genügsam: /<.*?>/ trifft nur "<a>". Und: E-Mail-Adressen oder HTML mit Regex vollständig zu validieren ist ein Fass ohne Boden – nutze dafür filter_var bzw. einen echten Parser.
+
Teil 3

Code strukturieren

Sobald Programme wachsen, müssen sie sich aufteilen lassen: Dateien einbinden, Namensräume ordnen, Pakete verwalten und Fehler kontrolliert behandeln.
12 · Code aufteilen: include & require13 · Namespaces14 · Composer & Autoloading15 · Fehler & Exceptions
Kapitel 12

Code aufteilen: include & require

Sobald Programme wachsen, teilst du sie auf mehrere Dateien auf. include und require binden eine andere PHP-Datei an Ort und Stelle ein.

+ +

include vs. require

+

Beide führen die eingebundene Datei aus. Der Unterschied liegt im Fehlerfall: include warnt nur und läuft weiter, require bricht hart ab:

+
include "optional.php";    // fehlt sie: Warnung, Programm läuft weiter
+require "datenbank.php";    // fehlt sie: fataler Fehler, Stopp
+

Faustregel: require für alles, ohne das es nicht weitergeht.

+ +

Nur einmal einbinden

+

Die _once-Varianten binden eine Datei höchstens einmal ein – das verhindert doppelte Funktions- oder Klassendefinitionen:

+
require_once "helfer.php";
+require_once "helfer.php";   // tut nichts mehr – keine Doppeldefinition
+ +

Werte zurückgeben

+

Eine eingebundene Datei kann mit return einen Wert liefern – ein verbreitetes Muster für Konfigurationsdateien:

+
// config.php
+return ["host" => "localhost", "port" => 3306];
+
+// woanders:
+$config = require "config.php";
+echo $config["host"];   // localhost
+ +
+
i
+
Im Alltag selten von HandIn modernen Projekten bindest du Klassen kaum noch selbst ein – das übernimmt der Autoloader von Composer (gleich im Composer-Kapitel). require brauchst du dann fast nur noch einmal für Composers vendor/autoload.php und für Konfigurationsdateien.
+
Kapitel 13

Namespaces

Namespaces verhindern Namenskollisionen: Zwei Bibliotheken dürfen beide eine Klasse Logger haben, solange sie in verschiedenen Namensräumen liegen. Sie sind außerdem die Grundlage des Autoloadings.

+ +

Definieren

+

namespace steht ganz oben in der Datei und ordnet alles darin ein:

+
namespace App\Service;
+
+class Logger {
+    public function write(string $text): void {
+        echo $text;
+    }
+}
+ +

Verwenden

+

Der voll qualifizierte Name enthält den Namespace. Mit use importierst du ihn und sparst dir die Wiederholung:

+
use App\Service\Logger;
+
+$log = new Logger();        // dank use kurz
+$log->write("Start");
+ +

Aliasnamen

+

Kollidieren zwei importierte Namen, benennst du einen per as um:

+
use App\Service\Logger as AppLogger;
+use Monolog\Logger as MonoLogger;
+ +

Globale Funktionen & der führende Backslash

+

Innerhalb eines Namespaces verweist ein führender \ auf den globalen Raum – nützlich, um eingebaute Funktionen oder Klassen eindeutig zu erreichen:

+
namespace App;
+$now = new \DateTime();   // die globale DateTime-Klasse
+echo \strlen("abc");        // die globale Funktion (oft auch ohne \)
+ +
+
+
Ein Namespace pro VerzeichnisDer PSR-4-Standard (im Ökosystem-Teil) bildet Namespaces auf Ordner ab: App\Service\Logger liegt in src/Service/Logger.php. Hältst du dich daran, findet Composers Autoloader jede Klasse automatisch – ohne ein einziges require.
+
Kapitel 14

Composer & Autoloading

Composer ist der Paketmanager von PHP. Er installiert Bibliotheken, verwaltet ihre Versionen und – fast noch wichtiger – lädt deine Klassen automatisch nach, sobald du sie benutzt.

+ +

Ein Projekt starten

+

composer.json beschreibt das Projekt und seine Abhängigkeiten. composer require fügt ein Paket hinzu:

+
# neues Paket installieren (landet in vendor/)
+composer require monolog/monolog
+
+# alle Abhängigkeiten aus composer.json installieren
+composer install
+ +

Der Autoloader

+

Eine einzige Zeile bindet Composers Autoloader ein. Danach werden alle Klassen aus deinem Code und aus den Paketen bei Bedarf automatisch geladen:

+
require "vendor/autoload.php";
+
+use Monolog\Logger;
+$log = new Logger("app");   // kein require für die Klasse nötig
+ +

Eigene Klassen autoladen (PSR-4)

+

Im autoload-Abschnitt der composer.json bildest du einen Namespace auf einen Ordner ab:

+
// composer.json
+{
+    "autoload": {
+        "psr-4": { "App\\": "src/" }
+    }
+}
+

Nach composer dump-autoload findet PHP App\Service\Logger automatisch in src/Service/Logger.php.

+ +

Versionsbereiche

+

Das ^ erlaubt verträgliche Updates (gleiche Hauptversion):

+
"require": {
+    "monolog/monolog": "^3.0"   // 3.x, aber nicht 4.0
+}
+ +
+
i
+
composer.lock eincheckenDie composer.lock hält die exakt installierten Versionen fest. Sie gehört ins Repository, damit alle – und der Server – identische Stände bekommen. composer install folgt der Lock-Datei, composer update aktualisiert sie.
+
Kapitel 15

Fehler & Exceptions

Wenn etwas schiefgeht, wirft PHP eine Exception – ein Objekt, das den Fehler beschreibt. Du fängst sie gezielt ab und reagierst, statt das Programm abstürzen zu lassen.

+ +

try / catch

+

Code, der scheitern kann, kommt in try; der catch-Block fängt die Exception:

+
try {
+    $result = 10 / $divisor;
+    if ($divisor === 0) {
+        throw new InvalidArgumentException("Teiler ist 0");
+    }
+} catch (InvalidArgumentException $e) {
+    echo "Fehler: " . $e->getMessage();   // Fehler: Teiler ist 0
+}
+ +

finally

+

Der finally-Block läuft immer – ob Fehler oder nicht. Ideal zum Aufräumen:

+
try {
+    $file = fopen("daten.txt", "r");
+    // ... verarbeiten ...
+} finally {
+    if (isset($file)) fclose($file);   // läuft in jedem Fall
+}
+ +

Eigene Exceptions

+

Eigene Klassen, die von Exception erben, machen Fehler im Code unterscheidbar:

+
class PaymentFailed extends Exception {}
+
+function charge(int $cent): void {
+    if ($cent <= 0) {
+        throw new PaymentFailed("Betrag ungültig");
+    }
+}
+try { charge(-5); }
+catch (PaymentFailed $e) { echo $e->getMessage(); }   // Betrag ungültig
+ +

Mehrere Typen fangen

+

Ein catch kann mehrere Exception-Typen behandeln; die Reihenfolge geht von speziell zu allgemein:

+
try {
+    // ...
+} catch (TypeError | ValueError $e) {
+    echo "Eingabeproblem";
+} catch (Throwable $e) {
+    echo "Irgendetwas anderes";   // Throwable fängt wirklich alles
+}
+ +
+
!
+
Nicht stillschweigend schluckenEin leerer catch-Block versteckt Fehler und macht die Suche zur Qual. Fange nur, was du wirklich behandeln kannst; alles andere lass nach oben durchreichen, wo eine zentrale Stelle es protokolliert. Error (z. B. TypeError) und Exception haben mit Throwable eine gemeinsame Basis.
+
Teil 4

Das Typsystem & moderne Sprache

Was modernes PHP ausmacht: ein ausdrucksstarkes Typsystem, Enums und die Syntax-Neuerungen von PHP 8.
16 · Typen & strict_types17 · Union-, Nullable- & spezielle Typen18 · Enums19 · Moderne Syntax
Kapitel 16

Typen & strict_types

PHP erlaubt Typdeklarationen für Parameter, Rückgaben und Eigenschaften. Sie machen Code selbsterklärend und fangen Fehler früh – besonders mit aktiviertem strikten Modus.

+ +

Skalare Typen deklarieren

+
function repeat(string $text, int $times): string {
+    return str_repeat($text, $times);
+}
+echo repeat("ab", 3);   // ababab
+ +

strict_types

+

Ohne strikten Modus wandelt PHP "3" stillschweigend in 3 um. Die Zeile declare(strict_types=1) ganz oben in der Datei erzwingt exakte Typen:

+
declare(strict_types=1);
+
+repeat("ab", "3");   // TypeError! "3" ist kein int
+repeat("ab", 3);     // ok
+ +

Eigenschafts-Typen

+
class Account {
+    public int $balance = 0;
+    public string $owner;
+}
+$k = new Account();
+$k->balance = 100;       // ok
+// $k->balance = "viel";  // TypeError
+ +
+
+
strict_types in jede DateiSetze declare(strict_types=1) als erste Zeile in jede PHP-Datei. Du fängst Tippfehler und falsche Übergaben sofort ab, statt dass sie sich als kuriose Werte durch das Programm ziehen.
+
Kapitel 17

Union-, Nullable- & spezielle Typen

Manchmal passt ein Wert in mehrere Typen. PHP drückt das mit Union-Typen aus – und kennt nützliche Sondertypen wie mixed, void oder self.

+ +

Nullable

+

Das Fragezeichen erlaubt zusätzlich null – typisch für „nicht gefunden":

+
function find(int $id): ?string {
+    return $id === 1 ? "Anna" : null;
+}
+var_dump(find(1));   // string "Anna"
+var_dump(find(9));   // NULL
+ +

Union-Typen

+

Mehrere erlaubte Typen, getrennt mit |:

+
function id(int|string $value): string {
+    return "ID-" . $value;
+}
+echo id(42);      // ID-42
+echo id("abc");   // ID-abc
+ +

void & never

+

void heißt „gibt nichts zurück", never „kehrt nie zurück" (wirft oder beendet):

+
function log(string $t): void { echo $t; }
+function abort(): never { throw new RuntimeException("Stopp"); }
+ +

mixed & self

+

mixed akzeptiert jeden Typ; self bzw. static stehen für die eigene Klasse – wichtig bei verketteten Methoden:

+
class Box {
+    private array $content = [];
+    public function add(mixed $x): static {
+        $this->content[] = $x;
+        return $this;            // erlaubt $box->add(1)->add(2)
+    }
+}
+echo count((new Box())->add(1)->add(2)->content ?? []);   // 2
+ +
+
i
+
So eng wie möglichJe genauer der Typ, desto mehr Sicherheit. Nutze mixed nur, wenn wirklich alles erlaubt ist; ein Union-Typ wie int|string sagt mehr aus und lässt die IDE und statische Analyse besser helfen.
+
Kapitel 18

Enums

Ein Enum (seit PHP 8.1) ist ein Typ mit einer festen, benannten Menge von Werten. Statt magischer Strings wie "offen" bekommst du echte, typsichere Fälle.

+ +

Reines Enum

+
enum Status {
+    case Open;
+    case Paid;
+    case Cancelled;
+}
+function handle(Status $s): string {
+    return match($s) {
+        Status::Open     => "wartet",
+        Status::Paid   => "erledigt",
+        Status::Cancelled => "abgebrochen",
+    };
+}
+echo handle(Status::Paid);   // erledigt
+

Der Gewinn: An eine Funktion mit Status-Parameter kann man nichts Falsches übergeben, und match ohne default zwingt dich, alle Fälle zu behandeln.

+ +

Backed Enum

+

Hängt jedem Fall einen Skalar an, z. B. für Datenbank oder API:

+
enum Role: string {
+    case Admin     = "admin";
+    case Editor = "editor";
+}
+echo Role::Admin->value;          // admin
+$r = Role::from("editor");          // aus dem Wert zurück: Rolle::Redakteur
+$r = Role::tryFrom("x");            // null statt Fehler bei Unbekanntem
+ +

Methoden im Enum

+

Enums dürfen Methoden haben – praktisch für Anzeigetexte:

+
enum TrafficLight {
+    case Red;
+    case Green;
+    public function drive(): bool {
+        return $this === TrafficLight::Green;
+    }
+}
+var_dump(TrafficLight::Red->drive());   // false
+

Alle Fälle bekommst du über cases():

+
foreach (Role::cases() as $role) {
+    echo $role->value;   // admin, editor
+}
+ +
+
+
Enums statt Konstanten-SammlungenWo früher eine Klasse mit const STATUS_OFFEN = "offen" stand, ist heute ein Enum die bessere Wahl: typsicher, mit Methoden und mit cases() aufzählbar. Nutze tryFrom() statt from(), wenn Eingaben unbekannt sein könnten.
+
Kapitel 19

Moderne Syntax

PHP 8 hat die Sprache spürbar moderner gemacht. Diese Schmankerl begegnen dir in jedem aktuellen Projekt – hier jedes mit Beispiel.

+ +

Nullsafe-Operator

+

?-> ruft eine Methode nur auf, wenn das Objekt nicht null ist – kein Fehler bei fehlenden Zwischenwerten:

+
$country = $user?->getAddress()?->country;   // null, wenn etwas in der Kette fehlt
+ +

Named Arguments

+

Argumente beim Namen nennen – die Reihenfolge wird egal, optionale gezielt setzbar:

+
array_slice(array: [1, 2, 3, 4], offset: 1, length: 2);   // [2, 3]
+ +

Konstruktor-Promotion

+

Parameter mit Sichtbarkeit werden automatisch zu Eigenschaften – kein Boilerplate mehr:

+
class Point {
+    public function __construct(
+        public float $x,
+        public float $y,
+    ) {}
+}
+echo (new Point(2, 3))->x;   // 2
+ +

Destrukturierung

+
[$a, $b] = [1, 2];
+echo $a;   // 1
+["name" => $name] = ["name" => "Anna", "alter" => 30];
+echo $name;   // Anna
+ +

First-Class Callable

+

(...) macht aus einer Funktion einen Wert, den man weiterreichen kann:

+
$fn = strtoupper(...);
+print_r(array_map($fn, ["a", "b"]));   // ["A", "B"]
+ +

Pipe-Operator (PHP 8.5)

+

Der neue |> reicht einen Wert von links nach rechts durch eine Kette – lesbarer als verschachtelte Aufrufe:

+
$result = "  Hallo Welt  "
+    |> trim(...)
+    |> strtolower(...);
+echo $result;   // hallo welt
+ +

clone with (PHP 8.5)

+

Klont ein Objekt und ändert dabei einzelne Eigenschaften in einem Ausdruck – ideal für unveränderliche Wertobjekte:

+
$p2 = clone $point with { x: 10 };   // Kopie mit neuem x, Rest gleich
+ +
+
i
+
Nutzen, wo es Klarheit bringtDiese Features sind additiv – alter Code läuft unverändert weiter. Konstruktor-Promotion und Enums lohnen sich sofort; den Pipe-Operator setzt man am besten dort ein, wo eine Transformationskette sonst tief verschachtelt wäre.
+
Teil 5

Objektorientierung: Grundlagen

Das Rückgrat größerer Programme: von der ersten Klasse über Vererbung und Interfaces bis zu Traits und magischen Methoden.
20 · Klassen & Objekte21 · Sichtbarkeit & Kapselung22 · Konstruktoren modern23 · Vererbung24 · Abstrakte Klassen & Interfaces25 · Traits26 · Statisches & Konstanten27 · Magische Methoden
Kapitel 20

Klassen & Objekte

Eine Klasse ist ein Bauplan; ein Objekt eine konkrete Ausführung davon. Klassen bündeln Daten (Eigenschaften) und Verhalten (Methoden) zu einer Einheit – das Rückgrat größerer Programme.

+ +

Eine Klasse definieren

+
class Dog {
+    public string $name;          // Eigenschaft
+    public function bark(): string {   // Methode
+        return $this->name . " sagt Wuff";
+    }
+}
+

$this verweist innerhalb der Klasse auf das aktuelle Objekt.

+ +

Objekte erzeugen & nutzen

+
$dog = new Dog();
+$dog->name = "Bello";        // Eigenschaft setzen
+echo $dog->bark();          // Bello sagt Wuff
+ +

Mehrere unabhängige Objekte

+

Jedes Objekt hat seinen eigenen Zustand:

+
$a = new Dog(); $a->name = "Rex";
+$b = new Dog(); $b->name = "Luna";
+echo $a->bark();   // Rex sagt Wuff
+echo $b->bark();   // Luna sagt Wuff
+ +

Objekte sind Referenzen

+

Eine Zuweisung kopiert nicht das Objekt, sondern den Verweis darauf – beide zeigen auf dasselbe:

+
$x = new Dog(); $x->name = "Rex";
+$y = $x;
+$y->name = "Bello";
+echo $x->name;   // Bello – $x und $y sind dasselbe Objekt
+ +
+
i
+
Klasse vs. ObjektDie Klasse schreibst du einmal; Objekte erzeugst du beliebig oft daraus. Eine echte Kopie eines Objekts bekommst du mit clone – mehr dazu bei den magischen Methoden.
+
Kapitel 21

Sichtbarkeit & Kapselung

Kapselung bedeutet: Eine Klasse verbirgt ihre Interna und gibt nur einen kontrollierten Zugang nach außen. Sichtbarkeits-Schlüsselwörter steuern, wer worauf zugreifen darf.

+ +

public, protected, private

+

public ist überall erreichbar, protected in der Klasse und ihren Erben, private nur in der Klasse selbst:

+
class Account {
+    private int $balance = 0;   // von außen unsichtbar
+    public function deposit(int $amount): void {
+        if ($amount > 0) $this->balance += $amount;
+    }
+    public function balance(): int { return $this->balance; }
+}
+$k = new Account();
+$k->deposit(100);
+echo $k->balance();      // 100
+// $k->balance = -999;   // Fehler: private, der Schutz greift
+

So kann niemand den Kontostand direkt manipulieren – jede Änderung muss durch deposit() und dessen Prüfung.

+ +

Getter und Setter

+

Lesen und kontrolliertes Schreiben über Methoden:

+
class Temperature {
+    private float $celsius = 0;
+    public function setCelsius(float $c): void {
+        if ($c < -273.15) throw new ValueError("zu kalt");
+        $this->celsius = $c;
+    }
+    public function fahrenheit(): float { return $this->celsius * 9 / 5 + 32; }
+}
+$t = new Temperature();
+$t->setCelsius(100);
+echo $t->fahrenheit();   // 212
+ +

Asymmetrische Sichtbarkeit (PHP 8.4)

+

Eine Eigenschaft kann öffentlich lesbar, aber nur intern schreibbar sein – ganz ohne Getter:

+
class Article {
+    public private(set) string $status = "entwurf";
+    public function publish(): void { $this->status = "live"; }
+}
+$a = new Article();
+echo $a->status;          // entwurf (lesen erlaubt)
+$a->publish();
+echo $a->status;          // live
+// $a->status = "x";   // Fehler: von außen nicht schreibbar
+ +
+
+
Standardmäßig privat denkenMache Eigenschaften so privat wie möglich und öffne nur, was wirklich von außen gebraucht wird. Das hält den inneren Zustand konsistent und erlaubt dir, die Umsetzung später zu ändern, ohne den restlichen Code zu brechen.
+
Kapitel 22

Konstruktoren modern

Der Konstruktor __construct richtet ein Objekt bei der Erzeugung ein. PHP 8 macht ihn besonders knapp – und mit readonly sogar unveränderlich.

+ +

Der klassische Konstruktor

+
class Point {
+    public float $x;
+    public float $y;
+    public function __construct(float $x, float $y) {
+        $this->x = $x;
+        $this->y = $y;
+    }
+}
+$p = new Point(2, 3);
+echo $p->x;   // 2
+ +

Konstruktor-Promotion

+

Die Schreibweise von oben ist so häufig, dass PHP sie abkürzt: Sichtbarkeit direkt am Parameter macht ihn automatisch zur Eigenschaft – das obige Beispiel schrumpft auf:

+
class Point {
+    public function __construct(
+        public float $x,
+        public float $y,
+    ) {}
+}
+echo (new Point(2, 3))->y;   // 3
+ +

readonly

+

Eine readonly-Eigenschaft lässt sich nur im Konstruktor belegen und danach nie wieder ändern – die Grundlage unveränderlicher Objekte:

+
class Money {
+    public function __construct(
+        public readonly int $cent,
+        public readonly string $currency = "EUR",
+    ) {}
+}
+$price = new Money(1999);
+echo $price->cent;      // 1999
+// $price->cent = 0;   // Fehler: readonly
+ +

Mit benannten Argumenten erzeugen

+
$price = new Money(currency: "USD", cent: 500);
+echo $price->currency;   // USD
+ +
+
+
Objekte gültig geborenLass den Konstruktor alle Pflichtwerte entgegennehmen und prüfen. Ist das Objekt erst einmal erzeugt, sollte es in einem gültigen Zustand sein – das erspart später endlose „ist das gesetzt?"-Prüfungen. Mit readonly bleibt es das garantiert.
+
Kapitel 23

Vererbung

Vererbung lässt eine Klasse Eigenschaften und Methoden einer anderen übernehmen und erweitern. Sie modelliert eine „ist ein"-Beziehung – ein Kreis ist eine Form.

+ +

extends

+
class Animal {
+    public function __construct(protected string $name) {}
+    public function sound(): string { return "..."; }
+}
+class Cat extends Animal {
+    public function sound(): string { return "Miau"; }   // überschreibt
+}
+$k = new Cat("Minka");
+echo $k->sound();   // Miau
+

Die abgeleitete Klasse erbt den Konstruktor und kann Methoden überschreiben.

+ +

parent::

+

Beim Überschreiben rufst du oft die ursprüngliche Methode mit auf:

+
class Dog extends Animal {
+    public function __construct(string $name, public string $breed) {
+        parent::__construct($name);   // Basis-Konstruktor
+    }
+    public function sound(): string { return "Wuff"; }
+}
+$h = new Dog("Rex", "Terrier");
+echo $h->breed;   // Terrier
+ +

protected

+

Im Beispiel oben ist $name protected und damit in Dog und Cat nutzbar, von außen aber verborgen.

+ +

final

+

final verhindert weiteres Überschreiben bzw. Erben:

+
final class UUID {}          // kann nicht erweitert werden
+class Base {
+    final public function id(): int { return 1; }   // nicht überschreibbar
+}
+ +
+
!
+
Komposition vor VererbungVererbung bindet Klassen eng aneinander. Oft ist es flexibler, Verhalten einzubauen (ein Objekt hält ein anderes) statt es zu erben. Nutze Vererbung für echte „ist ein"-Beziehungen – nicht nur, um Code zu teilen; dafür sind Traits oder Komposition meist besser.
+
Kapitel 24

Abstrakte Klassen & Interfaces

Beide definieren einen Vertrag, den andere Klassen erfüllen. Ein Interface schreibt nur vor, was es können muss; eine abstrakte Klasse kann zusätzlich gemeinsamen Code mitbringen.

+ +

Interface

+

Ein Interface listet Methoden ohne Rumpf. Wer es implements, muss sie ausfüllen:

+
interface Shape {
+    public function area(): float;
+}
+class Circle implements Shape {
+    public function __construct(private float $r) {}
+    public function area(): float { return 3.14159 * $this->r ** 2; }
+}
+class Square implements Shape {
+    public function __construct(private float $s) {}
+    public function area(): float { return $this->s ** 2; }
+}
+

Der Gewinn: Code kann mit jeder Shape arbeiten, ohne die konkrete Klasse zu kennen:

+
function describe(Shape $f): string {
+    return "Fläche: " . round($f->area(), 2);
+}
+echo describe(new Circle(2));     // Fläche: 12.57
+echo describe(new Square(3));   // Fläche: 9
+ +

Mehrere Interfaces

+

Eine Klasse kann beliebig viele Interfaces erfüllen:

+
interface Printable { public function output(): void; }
+class Receipt implements Shape, Printable { /* beide Verträge erfüllen */ }
+ +

Abstrakte Klasse

+

Sie kann nicht selbst instanziiert werden, gibt aber fertige Methoden mit und lässt einzelne offen:

+
abstract class Payment {
+    abstract public function book(int $cent): string;   // muss gefüllt werden
+    public function receipt(int $cent): string {              // fertig vererbt
+        return "Beleg über " . number_format($cent / 100, 2) . " €";
+    }
+}
+class PayPal extends Payment {
+    public function book(int $cent): string { return "via PayPal"; }
+}
+echo (new PayPal())->receipt(1999);   // Beleg über 19.99 €
+ +
+
i
+
Interface oder abstrakte Klasse?Nimm ein Interface, wenn du nur den Vertrag brauchst – eine Klasse kann mehrere erfüllen. Nimm eine abstrakte Klasse, wenn mehrere Unterklassen sich echten Code teilen sollen. Programmiere gegen Interfaces, nicht gegen konkrete Klassen – das hält den Code austauschbar und testbar.
+
Kapitel 25

Traits

Ein Trait ist ein wiederverwendbarer Methodenbaustein. Da eine Klasse nur von einer Klasse erben kann, lösen Traits das Problem, Code über mehrere unverwandte Klassen zu teilen.

+ +

Definieren & einbinden

+
trait Timestamps {
+    public ?int $created = null;
+    public function stamp(): void { $this->created = time(); }
+}
+class Post { use Timestamps; }
+class Comment { use Timestamps; }
+
+$b = new Post();
+$b->stamp();
+var_dump(is_int($b->created));   // true – beide Klassen haben die Methode
+

Beide Klassen teilen denselben Code, ohne miteinander verwandt zu sein.

+ +

Mehrere Traits

+
trait Counting {
+    public int $count = 0;
+    public function up(): void { $this->count++; }
+}
+class Gallery {
+    use Timestamps, Counting;   // mehrere Bausteine kombinieren
+}
+$g = new Gallery();
+$g->up(); $g->up();
+echo $g->count;   // 2
+ +

Konflikte auflösen

+

Bringen zwei Traits eine gleichnamige Methode mit, entscheidest du mit insteadof und as:

+
class Report {
+    use A, B {
+        A::hello insteadof B;   // A gewinnt
+        B::hello as helloB;     // Bs Version unter neuem Namen
+    }
+}
+ +
+
!
+
Sparsam einsetzenTraits sind praktisch, verstecken aber, woher Methoden kommen, und neigen zu verborgenen Abhängigkeiten auf $this. Für echtes Teilen von Verhalten ist Komposition (ein eingebautes Hilfsobjekt) oft klarer. Nutze Traits gezielt für kleine, eigenständige Fähigkeiten.
+
Kapitel 26

Statisches & Konstanten

Statische Member gehören zur Klasse statt zu einem Objekt. Konstanten halten unveränderliche Werte. Beide erreichst du ohne Instanz – über den Klassennamen.

+ +

Klassenkonstanten

+
class Circle {
+    const PI = 3.14159;
+    public function __construct(private float $r) {}
+    public function area(): float { return self::PI * $this->r ** 2; }
+}
+echo Circle::PI;                 // 3.14159 (ohne Objekt)
+echo (new Circle(2))->area();   // 12.56636
+

Seit PHP 8.3 dürfen Konstanten typisiert sein: const float PI = 3.14159;.

+ +

Statische Eigenschaften & Methoden

+

Sie existieren einmal pro Klasse, nicht pro Objekt – nützlich z. B. für einen Zähler:

+
class Connection {
+    public static int $open = 0;
+    public static function open(): void { self::$open++; }
+}
+Connection::open();
+Connection::open();
+echo Connection::$open;   // 2 – über alle „Objekte" geteilt
+ +

self vs. static

+

self bezieht sich auf die Klasse, in der der Code steht; static auf die tatsächlich aufgerufene (Late Static Binding) – wichtig bei Vererbung:

+
class Base {
+    public static function create(): static { return new static(); }
+}
+class Child extends Base {}
+var_dump(Child::create() instanceof Child);   // true (dank static)
+ +

::class

+

Liefert den voll qualifizierten Klassennamen als String – tippsicher:

+
echo Circle::class;   // Kreis (bzw. mit Namespace)
+ +
+
!
+
Statik sparsamStatische Eigenschaften sind globaler Zustand und erschweren Tests, weil sie zwischen Aufrufen bestehen bleiben. Konstanten und statische Hilfsfunktionen ohne Zustand sind unkritisch; veränderlichen Zustand verwaltest du besser in Objekten, die der DI-Container reicht (späterer Teil).
+
Kapitel 27

Magische Methoden

Magische Methoden (Namen mit doppeltem Unterstrich) werden von PHP automatisch in bestimmten Situationen aufgerufen – beim Erzeugen, beim Zugriff auf fehlende Eigenschaften, beim Ausgeben als String und mehr.

+ +

__construct & __destruct

+

Beim Erzeugen bzw. beim Aufräumen eines Objekts:

+
class File {
+    public function __construct(private string $path) { echo "öffne\n"; }
+    public function __destruct() { echo "schließe\n"; }
+}
+$d = new File("a.txt");   // öffne
+unset($d);                 // schließe (kein Verweis mehr)
+ +

__toString

+

Macht ein Objekt als String darstellbar:

+
class Money {
+    public function __construct(private int $cent) {}
+    public function __toString(): string { return number_format($this->cent / 100, 2) . " €"; }
+}
+echo new Money(1999);   // 19.99 €
+ +

__get, __set, __isset, __unset

+

Fangen Zugriffe auf nicht (öffentlich) vorhandene Eigenschaften ab:

+
class Bag {
+    private array $data = [];
+    public function __get(string $k): mixed { return $this->data[$k] ?? null; }
+    public function __set(string $k, mixed $v): void { $this->data[$k] = $v; }
+    public function __isset(string $k): bool { return isset($this->data[$k]); }
+}
+$t = new Bag();
+$t->color = "rot";        // __set
+echo $t->color;          // __get → rot
+var_dump(isset($t->color));   // __isset → true
+ +

__call & __callStatic

+

Fangen Aufrufe nicht vorhandener Methoden ab – Basis vieler „magischer" Framework-APIs:

+
class Fluent {
+    public function __call(string $name, array $args): string {
+        return "$name(" . implode(",", $args) . ")";
+    }
+}
+echo (new Fluent())->whatever(1, 2);   // wasAuchImmer(1,2)
+ +

__invoke

+

Lässt ein Objekt wie eine Funktion aufrufen:

+
class Doubler {
+    public function __invoke(int $n): int { return $n * 2; }
+}
+$f = new Doubler();
+echo $f(21);   // 42
+ +

__clone

+

Greift beim Kopieren mit clone ein – etwa um enthaltene Objekte mitzukopieren:

+
class Folder {
+    public array $pages = [];
+    public function __clone() { echo "kopiert\n"; }
+}
+$copy = clone new Folder();   // kopiert
+

Weitere magische Methoden steuern Sonderfälle: __serialize/__unserialize (Serialisierung), __debugInfo (Ausgabe bei var_dump) und __set_state (für var_export).

+ +
+
!
+
Magie mit MaßMagische Methoden sind mächtig, machen Code aber schwer durchschaubar und für IDEs/statische Analyse undurchsichtig. Bevorzuge echte, deklarierte Eigenschaften und Methoden; greife zu __get/__call nur, wo dynamisches Verhalten der eigentliche Zweck ist.
+
Teil 6

Objektorientierung: fortgeschritten

Das tiefere Handwerk hinter Frameworks: typisierte Sammlungen, Generatoren, Closures, Attribute und Reflection.
28 · Collections & Generics-Denken29 · Iteratoren & Generatoren30 · Closures & Bindung31 · Attribute32 · Reflection
Kapitel 28

Collections & Generics-Denken

Ein nacktes Array kann alles enthalten – das ist flexibel, aber fehleranfällig. Eine Collection ist eine Klasse, die ein Array kapselt und nur Werte eines bestimmten Typs zulässt. So wird aus „irgendein Array" ein klar benannter, typsicherer Behälter.

+ +

Eine typsichere Collection

+
class ArticleList {
+    private array $article = [];
+    public function add(Article $a): void {   // nur Artikel erlaubt
+        $this->article[] = $a;
+    }
+    public function total(): int {
+        return array_sum(array_map(fn(Article $a) => $a->cent, $this->article));
+    }
+}
+

Der Typ am Parameter add(Artikel $a) garantiert, dass nie etwas Falsches hineinrutscht – anders als bei $list[] = $irgendwas.

+ +

Wie ein Array benutzbar machen

+

Mit den SPL-Interfaces Countable und IteratorAggregate lässt sich die Collection mit count() zählen und mit foreach durchlaufen:

+
class Names implements \Countable, \IteratorAggregate {
+    public function __construct(private array $items = []) {}
+    public function count(): int { return count($this->items); }
+    public function getIterator(): \Iterator { return new \ArrayIterator($this->items); }
+}
+$n = new Names(["Anna", "Ben"]);
+echo count($n);                  // 2
+foreach ($n as $name) echo $name;   // AnnaBen
+ +

„Generics" in PHP

+

Echte Generics (wie List<Artikel>) kennt PHP zur Laufzeit nicht. Den gewünschten Effekt erreichst du über getypte Collection-Klassen wie oben – und über DocBlock-Annotationen, die statische Analyse-Werkzeuge auswerten:

+
/** @var array<int, Artikel> $article */
+private array $article = [];
+ +
+
+
Eigene Collection statt nacktem ArraySobald ein Array immer denselben Objekttyp hält und feste Operationen darauf laufen (summieren, filtern, suchen), lohnt sich eine eigene Klasse. Sie dokumentiert die Absicht, erzwingt den Typ und bietet sprechende Methoden statt verstreuter Array-Funktionen.
+
Kapitel 29

Iteratoren & Generatoren

Iteratoren machen beliebige Objekte mit foreach durchlaufbar. Generatoren erzeugen Werte bei Bedarf – ideal für große oder unendliche Folgen, ohne alles im Speicher zu halten.

+ +

Das Iterator-Interface

+

Wer die fünf Methoden erfüllt, ist durchlaufbar. In der Praxis nimmt man dafür meist Generatoren (gleich), aber so sieht das Grundprinzip aus:

+
class Numbers implements \Iterator {
+    private int $i = 0;
+    public function __construct(private int $limit) {}
+    public function current(): mixed { return $this->i; }
+    public function key(): mixed { return $this->i; }
+    public function next(): void { $this->i++; }
+    public function rewind(): void { $this->i = 0; }
+    public function valid(): bool { return $this->i < $this->limit; }
+}
+foreach (new Numbers(3) as $n) echo $n;   // 012
+ +

Generatoren mit yield

+

Ein yield macht aus einer Funktion einen Generator: Sie liefert Werte einzeln, pausiert dazwischen und merkt sich ihren Stand. Dasselbe wie oben, aber drastisch kürzer:

+
function numbers(int $limit): \Generator {
+    for ($i = 0; $i < $limit; $i++) {
+        yield $i;
+    }
+}
+foreach (numbers(3) as $n) echo $n;   // 012
+ +

Schlüssel mitliefern

+
function pairs(): \Generator {
+    yield "a" => 1;
+    yield "b" => 2;
+}
+foreach (pairs() as $k => $v) echo "$k=$v ";   // a=1 b=2
+ +

Faulheit spart Speicher

+

Der entscheidende Vorteil: Ein Generator hält immer nur den aktuellen Wert – so liest man riesige Dateien Zeile für Zeile, ohne sie komplett zu laden:

+
function lines(string $file): \Generator {
+    $f = fopen($file, "r");
+    while (($z = fgets($f)) !== false) {
+        yield rtrim($z);   // nur eine Zeile zur Zeit im Speicher
+    }
+    fclose($f);
+}
+ +

yield from

+

Delegiert an einen anderen Generator oder ein iterierbares:

+
function all(): \Generator {
+    yield 0;
+    yield from [1, 2];   // fügt deren Werte ein
+}
+echo implode(",", iterator_to_array(all()));   // 0,1,2
+ +
+
+
Generatoren für große DatenmengenVerarbeitest du Dateien, Datenbank-Ergebnisse oder Streams, die nicht komplett in den Speicher passen, sind Generatoren das Mittel der Wahl. Du iterierst wie über ein Array, hältst aber nur ein Element zur Zeit.
+
Kapitel 30

Closures & Bindung

Eine Closure ist eine Funktion als Wert: Du kannst sie einer Variablen zuweisen, herumreichen und später aufrufen. Sie kann sich Werte aus ihrer Umgebung „merken".

+ +

Closure als Wert

+
$greet = function(string $name): string {
+    return "Hallo, $name";
+};
+echo $greet("Anna");   // Hallo, Anna
+ +

use

+

Mit use holt eine Closure Variablen von außen herein – standardmäßig als Kopie:

+
$factor = 3;
+$times = function(int $n) use ($factor) {
+    return $n * $factor;
+};
+echo $times(5);   // 15
+

Mit & bindest du per Referenz – Änderungen wirken zurück:

+
$sum = 0;
+$add = function(int $n) use (&$sum) { $sum += $n; };
+$add(5); $add(3);
+echo $sum;   // 8
+ +

Pfeilfunktionen fangen automatisch ein

+

Eine Arrow-Funktion (fn) übernimmt die Umgebung von selbst – kein use nötig:

+
$factor = 3;
+$times = fn(int $n) => $n * $factor;   // $factor automatisch
+echo $times(5);   // 15
+ +

Als Argument übergeben

+

Closures sind das, was viele Array-Funktionen erwarten:

+
print_r(array_map(fn($n) => $n ** 2, [1, 2, 3]));   // [1, 4, 9]
+ +

$this binden

+

Eine Closure kann an ein Objekt gebunden werden und dann auf dessen Interna zugreifen:

+
class Counter { private int $n = 10; }
+$read = Closure::bind(fn() => $this->n, new Counter(), Counter::class);
+echo $read();   // 10
+ +
+
i
+
fn oder function?Nimm die kurze fn-Form für knappe Einzeiler, die nur Umgebungswerte lesen. Brauchst du mehrere Anweisungen oder Bindung per Referenz, ist die ausführliche function() use(...)-Form richtig.
+
Kapitel 31

Attribute

Attribute (seit PHP 8) hängen strukturierte Metadaten direkt an Klassen, Methoden, Eigenschaften oder Parameter – maschinenlesbar, statt in Kommentaren versteckt. Frameworks nutzen sie für Routing, Validierung, ORM-Mappings und mehr.

+ +

Eingebaute Attribute

+

#[\Override] sagt aus, dass eine Methode bewusst überschreibt – PHP meldet einen Fehler, wenn es in der Basis nichts zu überschreiben gibt:

+
class Base { public function run(): void {} }
+class Child extends Base {
+    #[\Override]
+    public function run(): void {}   // abgesichert: tippt man "luaf", gibt es einen Fehler
+}
+

Weitere eingebaute: #[\Deprecated] markiert Veraltetes (mit Warnung bei Nutzung) und – neu in PHP 8.5 – #[\NoDiscard] warnt, wenn ein wichtiger Rückgabewert ignoriert wird.

+ +

Ein eigenes Attribut

+

Ein Attribut ist eine Klasse mit dem Attribut #[Attribute]. Sein Konstruktor nimmt die Werte entgegen:

+
#[\Attribute]
+class Route {
+    public function __construct(public string $path) {}
+}
+
+class StartController {
+    #[Route("/start")]
+    public function index(): string { return "Startseite"; }
+}
+

Bis hierher ist nichts „passiert" – das Attribut steht nur am Code. Lebendig wird es erst, wenn jemand es ausliest.

+ +

Attribute auslesen

+

Per Reflection (nächstes Kapitel) holst du die Metadaten zur Laufzeit – so liest ein Router seine Pfade:

+
$m = new \ReflectionMethod(StartController::class, "index");
+foreach ($m->getAttributes(Route::class) as $attr) {
+    $route = $attr->newInstance();   // erzeugt das Route-Objekt
+    echo $route->path;            // /start
+}
+ +
+
i
+
Metadaten statt KonfigurationAttribute halten Konfiguration dort, wo sie wirkt – direkt am Code. Statt eine Route in einer separaten Datei zu pflegen, steht sie an der Methode. Genau so arbeiten moderne Frameworks; du selbst brauchst eigene Attribute selten, begegnest ihnen aber überall.
+
Kapitel 32

Reflection

Reflection erlaubt es, zur Laufzeit in Klassen hineinzuschauen: Welche Methoden, Eigenschaften, Parameter und Attribute haben sie? Darauf bauen DI-Container, ORMs, Test- und Serialisierungs-Bibliotheken auf.

+ +

Eine Klasse untersuchen

+
class User {
+    public function __construct(public string $name, private int $age) {}
+    public function greet(): string { return "Hi $this->name"; }
+}
+$r = new \ReflectionClass(User::class);
+foreach ($r->getMethods() as $m) {
+    echo $m->getName() . " ";   // __construct begruessen
+}
+ +

Parameter und Typen lesen

+

Das ist der Kern eines DI-Containers: Welche Typen braucht der Konstruktor?

+
$ctor = (new \ReflectionClass(User::class))->getConstructor();
+foreach ($ctor->getParameters() as $p) {
+    echo $p->getName() . ":" . $p->getType() . " ";   // name:string alter:int
+}
+ +

Objekte ohne new erzeugen

+

Reflection kann Instanzen mit dynamischen Argumenten bauen – die Grundlage automatischer Verdrahtung:

+
$obj = (new \ReflectionClass(User::class))
+    ->newInstanceArgs(["Anna", 30]);
+echo $obj->greet();   // Hi Anna
+ +

Auf Privates zugreifen

+

Für Tests und Serialisierung lässt sich (mit Bedacht) auch Verborgenes lesen:

+
$prop = new \ReflectionProperty(User::class, "alter");
+echo $prop->getValue($obj);   // 30
+ +

Ein Mini-Container

+

Reflection und Attribute zusammen ergeben automatische Objekt-Erzeugung – stark vereinfacht:

+
function create(string $className): object {
+    $ctor = (new \ReflectionClass($className))->getConstructor();
+    $args = array_map(
+        fn($p) => create((string) $p->getType()),   // rekursiv Abhängigkeiten bauen
+        $ctor ? $ctor->getParameters() : []
+    );
+    return new $className(...$args);
+}
+ +
+
!
+
Mächtig, aber langsam und indirektReflection ist langsamer als direkter Code und umgeht die Kapselung. Sie ist das richtige Werkzeug für Frameworks und Werkzeuge, die generisch über fremde Klassen arbeiten – im normalen Anwendungscode brauchst du sie selten. Container cachen die Ergebnisse, um den Aufwand zu vermeiden.
+
Teil 7

Die Standardbibliothek

PHP bringt enorm viel ab Werk mit: Zahlen, Datum und Zeit, das Dateisystem, Streams und die SPL-Datenstrukturen.
33 · Mathematik & Zahlen34 · Datum & Zeit35 · Dateien & Dateisystem36 · Streams & Filter37 · SPL-Datenstrukturen
Kapitel 33

Mathematik & Zahlen

PHP bringt für jede Rechenaufgabe eine Funktion mit – und für Geldbeträge einen wichtigen Sonderweg. Hier die Grundfunktionen, jede mit Beispiel.

+ +

Runden & Ganzzahlen

+
echo abs(-7);            // 7   Betrag
+echo round(3.14159, 2);  // 3.14 kaufmännisch runden
+echo floor(4.7);         // 4   abrunden
+echo ceil(4.2);          // 5   aufrunden
+echo intdiv(17, 5);      // 3   Ganzzahldivision
+echo 17 % 5;            // 2   Rest dazu
+ +

Potenz & Wurzel

+
echo pow(2, 10);   // 1024
+echo 2 ** 10;      // 1024 (gleichbedeutend)
+echo sqrt(144);    // 12
+ +

Größtes, Kleinstes, Zufall

+
echo max(3, 9, 5);     // 9
+echo min([4, 1, 8]);    // 1 (auch mit Array)
+echo random_int(1, 6);   // kryptografisch sichere Zufallszahl 1–6
+

Für Zufall, auf den es ankommt (Tokens, Passwörter), immer random_int bzw. random_bytes statt des alten rand.

+ +

Zahlen formatieren

+
echo number_format(1234567.5, 2, ",", ".");   // 1.234.567,50
+ +

Die Tücke der Fließkommazahlen

+

Floats speichern Dezimalzahlen nicht exakt – ein Problem in jeder Sprache:

+
var_dump(0.1 + 0.2 === 0.3);   // false!
+// 0.1 + 0.2 ergibt 0.30000000000000004
+

Vergleiche Floats deshalb nie direkt, sondern über eine kleine Toleranz:

+
$equal = abs((0.1 + 0.2) - 0.3) < 0.00001;
+var_dump($equal);   // true
+ +
+
!
+
Geld nie als FloatRechne Geldbeträge in der kleinsten Einheit als int (Cent), nicht als Float – sonst summieren sich Rundungsfehler. 1999 Cent statt 19.99 Euro. Zur Anzeige teilst du am Ende durch 100 und formatierst mit number_format.
+
Kapitel 34

Datum & Zeit

Zeit ist tückisch: Zeitzonen, Sommerzeit, Schaltjahre. PHP nimmt dir das mit den DateTime-Klassen ab – arbeite mit Objekten statt mit rohen Zeitstempeln.

+ +

Zeitstempel & date()

+
echo time();                 // Sekunden seit 1970 (Unix-Zeitstempel)
+echo date("d.m.Y H:i");     // z. B. 31.05.2026 14:30
+

Die Formatzeichen sind zahlreich (Y Jahr, m Monat, d Tag, H:i:s Zeit …); die vollständige Liste steht in der PHP-Doku.

+ +

DateTimeImmutable

+

Unveränderliche Datumsobjekte: Jede Änderung liefert ein neues Objekt, das Original bleibt unangetastet – das verhindert eine ganze Klasse von Fehlern:

+
$now = new \DateTimeImmutable("2026-05-31 10:00");
+$later = $now->modify("+2 days");
+echo $now->format("d.m.");     // 31.05.
+echo $later->format("d.m.");   // 02.06. (Original unverändert)
+ +

Aus einem Format einlesen

+
$d = \DateTimeImmutable::createFromFormat("d.m.Y", "24.12.2026");
+echo $d->format("Y-m-d");   // 2026-12-24
+ +

Differenzen mit diff()

+

Liefert ein DateInterval mit Tagen, Stunden usw.:

+
$a = new \DateTimeImmutable("2026-01-01");
+$b = new \DateTimeImmutable("2026-12-31");
+$diff = $a->diff($b);
+echo $diff->days;       // 364
+echo $diff->format("%m Monate");   // 11 Monate
+ +

Vergleichen & Rechnen

+
var_dump($a < $b);   // true – Datumsobjekte sind direkt vergleichbar
+$deadline = $a->add(new \DateInterval("P30D"));   // + 30 Tage
+ +

Zeitzonen

+
$tz = new \DateTimeZone("Europe/Berlin");
+$d = new \DateTimeImmutable("now", $tz);
+echo $d->format("H:i e");   // Uhrzeit samt Zone
+ +
+
+
Immutable + UTCNimm DateTimeImmutable statt DateTime – so kann dir kein Aufruf unbemerkt das Datum „unter den Füßen" ändern. Speichere Zeiten intern und in der Datenbank in UTC und rechne erst zur Anzeige in die lokale Zone um. Für komfortablere APIs ist die Bibliothek Carbon verbreitet.
+
Kapitel 35

Dateien & Dateisystem

PHP liest und schreibt Dateien, legt Verzeichnisse an und durchsucht sie. Für kleine Dateien gibt es bequeme Einzeiler, für große den Strom-orientierten Weg.

+ +

Ganze Datei lesen & schreiben

+
file_put_contents("notiz.txt", "Hallo\n");          // schreibt (überschreibt)
+file_put_contents("notiz.txt", "mehr\n", FILE_APPEND);   // hängt an
+echo file_get_contents("notiz.txt");             // gesamten Inhalt lesen
+print_r(file("notiz.txt"));                     // als Array von Zeilen
+ +

Zeilenweise für große Dateien

+

Statt alles in den Speicher zu laden, öffnest du die Datei und liest Zeile für Zeile:

+
$f = fopen("gross.log", "r");
+while (($line = fgets($f)) !== false) {
+    // $line verarbeiten ...
+}
+fclose($f);
+ +

Existenz & Eigenschaften prüfen

+
var_dump(file_exists("notiz.txt"));   // true
+var_dump(is_dir("/tmp"));            // true
+echo filesize("notiz.txt");        // Größe in Bytes
+ +

Pfade zerlegen

+
$p = "/var/www/config.php";
+echo dirname($p);    // /var/www
+echo basename($p);   // config.php
+echo pathinfo($p)["extension"];   // php
+echo realpath("./../config.php");    // löst .. zu absolutem Pfad auf
+ +

Verzeichnisse

+
mkdir("daten/cache", 0755, true);   // rekursiv anlegen
+print_r(glob("daten/*.txt"));        // alle .txt per Muster
+unlink("notiz.txt");                  // Datei löschen
+ +
+
!
+
Fehler behandeln & Pfade prüfenfopen & Co. liefern bei Problemen false oder werfen Warnungen – prüfe die Rückgabe. Baue Pfade nie ungeprüft aus Benutzereingaben (Gefahr von „Path Traversal" mit ../); validiere mit realpath gegen ein erlaubtes Basisverzeichnis. Schließe geöffnete Handles immer wieder.
+
Kapitel 36

Streams & Filter

Hinter Dateien, Netzwerk und sogar Speicher steckt in PHP ein einheitliches Konzept: der Stream. Dieselben Funktionen (fopen, fread …) arbeiten auf allen, nur die „Adresse" (der Wrapper) unterscheidet sich.

+ +

Wrapper

+

Ein Präfix wie file://, php://, http:// oder data:// bestimmt die Quelle. data:// trägt den Inhalt direkt im Pfad:

+
echo file_get_contents("data://text/plain,Hallo Welt");   // Hallo Welt
+ +

php://memory

+

Ein Stream, der seine Daten nur im Arbeitsspeicher hält – praktisch als Puffer oder in Tests:

+
$f = fopen("php://memory", "r+");
+fwrite($f, "zwischenspeicher");
+rewind($f);
+echo stream_get_contents($f);   // zwischenspeicher
+ +

Standard-Ein-/Ausgabe im CLI

+
fwrite(STDOUT, "normale Ausgabe\n");
+fwrite(STDERR, "Fehler-Ausgabe\n");
+$input = fgets(STDIN);     // liest eine Zeile von Tastatur/Pipe
+ +

Netzwerk transparent

+

Mit dem http://-Wrapper liest sich eine URL wie eine Datei (sofern allow_url_fopen aktiv ist):

+
$content = file_get_contents("https://example.com");   // HTML der Seite
+ +

Stream-Kontext

+

Ein Kontext gibt dem Wrapper Optionen mit – etwa HTTP-Header oder eine POST-Anfrage:

+
$ctx = stream_context_create([
+    "http" => ["method" => "POST", "header" => "Content-Type: application/json",
+                "content" => '{"x":1}'],
+]);
+$response = file_get_contents("https://api.example.com", false, $ctx);
+ +

Filter

+

Stream-Filter verändern Daten beim Lesen/Schreiben – z. B. Groß­schreibung:

+
$f = fopen("php://memory", "r+");
+stream_filter_append($f, "string.toupper");
+fwrite($f, "hallo"); rewind($f);
+echo stream_get_contents($f);   // HALLO
+ +
+
i
+
Ein Konzept, viele QuellenWeil Datei, Speicher und Netzwerk dieselbe Stream-Schnittstelle teilen, kannst du Code gegen „irgendeinen Strom" schreiben und ihn in Tests mit php://memory füttern statt mit echten Dateien. Für ernsthafte HTTP-Aufrufe nimmt man aber besser einen echten Client wie Guzzle (Web-Teil).
+
Kapitel 37

SPL-Datenstrukturen

Die Standard PHP Library (SPL) liefert fertige Datenstrukturen, die als Klassen daherkommen – Stapel, Schlangen, Prioritätswarteschlangen und mehr. Sie sind oft klarer und manchmal effizienter als ein nacktes Array.

+ +

SplStack

+

Zuletzt Hineingelegtes kommt zuerst heraus:

+
$s = new \SplStack();
+$s->push("a");
+$s->push("b");
+echo $s->pop();   // b (zuletzt rein, zuerst raus)
+ +

SplQueue

+

Eine Warteschlange: Zuerst Hineingelegtes kommt zuerst wieder heraus (FIFO):

+
$q = new \SplQueue();
+$q->enqueue("erst");
+$q->enqueue("dann");
+echo $q->dequeue();   // erst (zuerst rein, zuerst raus)
+ +

SplPriorityQueue

+

Gibt Elemente nicht nach Reihenfolge, sondern nach zugewiesener Priorität aus:

+
$p = new \SplPriorityQueue();
+$p->insert("normal", 1);
+$p->insert("dringend", 5);
+echo $p->extract();   // dringend (höchste Priorität zuerst)
+ +

SplObjectStorage

+

Speichert Objekte eindeutig und kann ihnen Daten zuordnen – ideal als Beobachter-Liste:

+
$store = new \SplObjectStorage();
+$a = new \stdClass();
+$store->attach($a, "info zu a");
+var_dump($store->contains($a));   // true
+echo $store[$a];                   // info zu a
+ +

SplFixedArray

+

Ein Array mit fester Länge und nur Ganzzahl-Indizes – sparsamer im Speicher bei großen, gleichförmigen Datenmengen:

+
$fix = new \SplFixedArray(3);
+$fix[0] = "x";
+echo $fix[0];           // x
+echo count($fix);        // 3
+ +
+
i
+
Wann SPL, wann Array?Für die meisten Fälle reicht das gewöhnliche Array. Greife zur SPL, wenn die Absicht davon profitiert: Ein SplStack sagt „hier wird gestapelt", SplObjectStorage verwaltet Objektmengen sauber, und SplPriorityQueue erspart eigenes Sortieren.
+
Teil 8

Datenformate & Serialisierung

Daten verlassen dein Programm und kommen wieder herein – als JSON, XML, CSV oder PHP-eigene Serialisierung.
38 · JSON39 · XML40 · CSV & Tabellendaten41 · serialize & JsonSerializable42 · Weitere Formate: INI, YAML, Base64
Kapitel 38

JSON

JSON ist das Standardformat für APIs und Konfiguration. PHP wandelt mit zwei Funktionen zwischen PHP-Werten und JSON hin und her.

+ +

Kodieren mit json_encode

+
$data = ["name" => "Anna", "aktiv" => true, "tags" => ["a", "b"]];
+echo json_encode($data);
+// {"name":"Anna","aktiv":true,"tags":["a","b"]}
+ +

Dekodieren mit json_decode

+

Das zweite Argument true liefert ein assoziatives Array statt eines Objekts:

+
$arr = json_decode('{"name":"Anna","alter":30}', true);
+echo $arr["name"];   // Anna
+$obj = json_decode('{"name":"Anna"}');
+echo $obj->name;     // Anna (als Objekt)
+ +

Optionen (Flags)

+

JSON_PRETTY_PRINT formatiert lesbar, JSON_UNESCAPED_UNICODE lässt Umlaute stehen, JSON_UNESCAPED_SLASHES maskiert Schrägstriche nicht:

+
echo json_encode(["ort" => "Köln"], JSON_UNESCAPED_UNICODE);
+// {"ort":"Köln"}  (ohne Flag: \u00f6)
+echo json_encode(["url" => "https://php.net"], JSON_UNESCAPED_SLASHES);
+// {"url":"https://php.net"}
+ +

Fehler abfangen

+

Mit JSON_THROW_ON_ERROR wirft ungültiges JSON eine Exception statt still null zu liefern:

+
try {
+    $x = json_decode("{kaputt}", true, 512, JSON_THROW_ON_ERROR);
+} catch (\JsonException $e) {
+    echo "ungültiges JSON";
+}
+ +
+
+
Immer mit THROW_ON_ERROROhne dieses Flag liefert json_decode bei Fehlern null – nicht von echtem null zu unterscheiden. Setze es grundsätzlich, dann scheitern fehlerhafte Daten laut und früh statt leise und spät.
+
Kapitel 39

XML

XML ist sperriger als JSON, aber in Altsystemen, Konfigurationen und Standards (SOAP, RSS) verbreitet. PHP bietet drei Wege – je nach Größe und Aufgabe.

+ +

SimpleXML

+

Für überschaubares XML der bequemste Weg: Knoten werden zu Objekteigenschaften:

+
$xml = simplexml_load_string("<buch><titel>PHP</titel><jahr>2026</jahr></buch>");
+echo $xml->title;   // PHP
+echo $xml->year;    // 2026
+

Über mehrere gleichnamige Knoten iterierst du mit foreach:

+
$xml = simplexml_load_string("<liste><e>a</e><e>b</e></liste>");
+foreach ($xml->e as $e) echo $e;   // ab
+ +

DOMDocument

+

Mächtiger: XML aufbauen oder mit XPath gezielt abfragen:

+
$dom = new \DOMDocument();
+$root = $dom->createElement("gruss", "Hallo");
+$dom->appendChild($root);
+echo $dom->saveXML();   // <?xml ...><gruss>Hallo</gruss>
+

Mit XPath suchst du in vorhandenem XML:

+
$dom->loadXML("<b><t>X</t><t>Y</t></b>");
+$xpath = new \DOMXPath($dom);
+foreach ($xpath->query("//t") as $n) echo $n->nodeValue;   // XY
+ +

XMLReader

+

Liest XML strömend, ohne alles in den Speicher zu laden – das Pendant zu Generatoren für XML:

+
$r = new \XMLReader();
+$r->open("gross.xml");
+while ($r->read()) {
+    if ($r->nodeType === \XMLReader::ELEMENT && $r->name === "eintrag") {
+        // einen Eintrag verarbeiten
+    }
+}
+ +
+
!
+
XXE: externe EntitätenBeim Einlesen fremden XML drohen „XML External Entity"-Angriffe (Zugriff auf lokale Dateien). Moderne PHP-Versionen laden externe Entitäten standardmäßig nicht mehr; lade trotzdem nie unkontrolliert fremdes XML mit aktivierter Entity-Auflösung.
+
Kapitel 40

CSV & Tabellendaten

CSV ist das kleinste gemeinsame Format für Tabellen – jede Software liest es. PHP hat eingebaute Funktionen, die Feinheiten wie Anführungszeichen und Trennzeichen korrekt behandeln.

+ +

Schreiben mit fputcsv

+

Über einen Stream, Zeile für Zeile. Sonderzeichen und Trennzeichen in Werten werden automatisch korrekt maskiert:

+
$f = fopen("export.csv", "w");
+fputcsv($f, ["Name", "Stadt"]);          // Kopfzeile
+fputcsv($f, ["Anna", "Köln; Zentrum"]);    // Wert mit ; wird gequotet
+fclose($f);
+ +

Lesen mit fgetcsv

+
$f = fopen("export.csv", "r");
+while (($line = fgetcsv($f)) !== false) {
+    print_r($line);   // ["Anna", "Köln; Zentrum"]
+}
+fclose($f);
+ +

Kopfzeile als Schlüssel nutzen

+

Ein verbreitetes Muster: erste Zeile als Spaltennamen, danach jede Zeile als assoziatives Array:

+
$f = fopen("export.csv", "r");
+$header = fgetcsv($f);
+while (($z = fgetcsv($f)) !== false) {
+    $record = array_combine($header, $z);
+    echo $record["Name"];   // Anna
+}
+ +

Einen einzelnen String parsen

+
print_r(str_getcsv('a,"b,c",d'));   // ["a", "b,c", "d"]
+ +
+
+
Nie von Hand splittenZerlege CSV niemals mit explode(",", …) – das zerbricht an Werten, die selbst Kommas oder Zeilenumbrüche enthalten. Die fgetcsv/str_getcsv-Funktionen kennen die Quoting-Regeln. Für Excel-Dateien (.xlsx) nutzt man die Bibliothek PhpSpreadsheet.
+
Kapitel 41

serialize & JsonSerializable

Manchmal willst du einen PHP-Wert komplett speichern und später unverändert zurückholen – mit Typen und Objektstruktur. Dafür gibt es PHPs eigene Serialisierung und Schnittstellen, um die JSON-Darstellung zu steuern.

+ +

serialize & unserialize

+

Wandeln beliebige PHP-Werte in einen String und zurück – inklusive Typen und Objekten:

+
$data = ["n" => 42, "aktiv" => true];
+$str = serialize($data);
+echo $str;   // a:2:{s:1:"n";i:42;s:5:"aktiv";b:1;}
+$restored = unserialize($str);
+var_dump($restored["n"]);   // int(42) – Typ erhalten
+ +

JsonSerializable

+

Implementiert eine Klasse dieses Interface, bestimmt sie selbst, wie json_encode sie darstellt – etwa um Privates zu verbergen:

+
class User implements \JsonSerializable {
+    public function __construct(
+        public string $name,
+        private string $password,
+    ) {}
+    public function jsonSerialize(): array {
+        return ["name" => $this->name];   // Passwort bewusst weglassen
+    }
+}
+echo json_encode(new User("Anna", "geheim"));   // {"name":"Anna"}
+ +

__serialize & __unserialize

+

Steuern, was bei PHPs serialize gespeichert und wie es wiederhergestellt wird – z. B. um eine offene Verbindung beim Speichern auszulassen:

+
class Cache {
+    public array $data = [];
+    public function __serialize(): array { return ["daten" => $this->data]; }
+    public function __unserialize(array $d): void { $this->data = $d["daten"]; }
+}
+ +
+
!
+
unserialize nie auf fremde DatenPHPs unserialize kann beim Wiederherstellen Objekte erzeugen und Code auslösen („Object Injection"). Wende es niemals auf Daten an, die von außen kommen (Cookies, Requests). Für den Datenaustausch nimm JSON; serialize nur intern, wo du der Quelle vollständig vertraust.
+
Kapitel 42

Weitere Formate: INI, YAML, Base64

Neben JSON, XML und CSV begegnen dir im Alltag weitere kleine Formate – für Konfiguration, lesbare Datenstrukturen und binärsichere Kodierung.

+ +

INI

+

Das klassische Schlüssel-Wert-Format mit Abschnitten, eingebaut lesbar:

+
// app.ini:  [db]  host = localhost  port = 3306
+$cfg = parse_ini_file("app.ini", true);   // true: Abschnitte behalten
+echo $cfg["db"]["host"];   // localhost
+

Auch direkt aus einem String:

+
$c = parse_ini_string("name = Anna\nalter = 30");
+echo $c["name"];   // Anna
+ +

Base64

+

Macht beliebige Bytes (z. B. ein Bild) als reinen Text transportierbar – etwa für Data-URLs oder JSON:

+
$encoded = base64_encode("Hällo");
+echo $encoded;                    // SMOkbGxv
+echo base64_decode($encoded);    // Hällo
+

Wichtig: Base64 ist keine Verschlüsselung – nur eine Umkodierung. Jeder kann es zurückrechnen.

+ +

URL-Kodierung & Query-Strings

+
echo urlencode("a b&c");                  // a+b%26c
+echo http_build_query(["q" => "php 8", "seite" => 2]);   // q=php+8&seite=2
+ +

YAML

+

Sehr lesbar, in der PHP-Welt vor allem aus Symfony bekannt. Ein YAML-Parser ist nicht im Kern – entweder über die PECL-Erweiterung ext-yaml oder die Bibliothek symfony/yaml:

+
use Symfony\Component\Yaml\Yaml;
+$data = Yaml::parse("name: Anna\ntags: [a, b]");
+echo $data["name"];   // Anna
+ +
+
i
+
Das richtige Format wählenJSON für APIs und Datenaustausch, INI oder YAML für menschenlesbare Konfiguration, CSV für Tabellen, Base64 nur zum Transport von Binärdaten. Für Konfiguration in PHP-Projekten ist oft auch eine .php-Datei mit return [...] die einfachste Wahl – sie wird vom OPcache mitgecacht.
+
Teil 9

Datenbanken mit PDO

Fast jede Anwendung speichert Daten. PDO ist PHPs einheitliche, sichere Schnittstelle – von der Verbindung bis zu Transaktionen und ORM-Konzepten.
43 · Verbindung & Grundlagen44 · Abfragen & Prepared Statements45 · Ergebnisse verarbeiten46 · Transaktionen47 · Datenbankdesign-Grundlagen48 · ORM-Konzepte & Doctrine
Kapitel 43

Verbindung & Grundlagen

PDO (PHP Data Objects) ist die einheitliche, sichere Schnittstelle zu Datenbanken. Derselbe Code spricht – bis auf den Verbindungsstring – MySQL, PostgreSQL, SQLite und mehr.

+ +

Verbinden

+

Der DSN beschreibt Treiber, Host und Datenbank. Drei Optionen solltest du immer setzen:

+
$pdo = new \PDO(
+    "mysql:host=localhost;dbname=shop;charset=utf8mb4",
+    "benutzer", "passwort",
+    [
+        \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,   // Fehler werfen
+        \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, // Standard: assoz. Array
+        \PDO::ATTR_EMULATE_PREPARES => false,          // echte Prepared Statements
+    ]
+);
+ +

SQLite

+

Ideal zum Lernen und für Tests – die ganze Datenbank ist eine Datei (oder im Speicher):

+
$pdo = new \PDO("sqlite::memory:");   // flüchtig, nur im RAM
+$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+$pdo->exec("CREATE TABLE nutzer (id INTEGER PRIMARY KEY, name TEXT)");
+ +

Direkte Befehle mit exec

+

exec führt SQL ohne Ergebnismenge aus und liefert die Zahl betroffener Zeilen:

+
$count = $pdo->exec("DELETE FROM nutzer WHERE id = 5");
+echo $count;   // z. B. 1
+ +
+
!
+
ERRMODE_EXCEPTION immer setzenOhne diesen Modus schluckt PDO Fehler still und du suchst stundenlang. Mit ihm wird jeder Datenbankfehler zu einer PDOException, die du fangen kannst. Setze außerdem charset=utf8mb4, sonst gehen Emojis und manche Zeichen verloren.
+
Kapitel 44

Abfragen & Prepared Statements

Das Wichtigste an PDO: Werte gehören nie direkt in den SQL-String, sondern als Parameter in ein Prepared Statement. Das ist der einzige verlässliche Schutz vor SQL-Injection.

+ +

Das Problem (so NICHT)

+
// GEFÄHRLICH – niemals so:
+$pdo->query("SELECT * FROM nutzer WHERE name = '" . $_GET["name"] . "'");
+// Eingabe  ' OR '1'='1  hebelt die Bedingung aus
+ +

Prepared Statement mit benannten Platzhaltern

+

Der Wert kommt getrennt vom SQL – die Datenbank behandelt ihn garantiert als Daten, nie als Befehl:

+
$stmt = $pdo->prepare("SELECT * FROM nutzer WHERE name = :name");
+$stmt->execute(["name" => $_GET["name"]]);
+$lines = $stmt->fetchAll();
+ +

Positionsplatzhalter

+

Statt Namen gehen auch Fragezeichen – die Werte in derselben Reihenfolge:

+
$stmt = $pdo->prepare("INSERT INTO nutzer (name, stadt) VALUES (?, ?)");
+$stmt->execute(["Anna", "Köln"]);
+echo $pdo->lastInsertId();   // ID des neuen Datensatzes
+ +

Dasselbe Statement mehrfach nutzen

+

Einmal vorbereiten, mehrfach ausführen – effizient bei vielen Einfügungen:

+
$stmt = $pdo->prepare("INSERT INTO tags (wort) VALUES (?)");
+foreach (["php", "sql", "web"] as $tag) {
+    $stmt->execute([$tag]);
+}
+ +
+
!
+
Spalten- und Tabellennamen sind keine ParameterPlatzhalter funktionieren nur für Werte, nicht für Tabellen- oder Spaltennamen oder ORDER BY-Richtungen. Muss so etwas dynamisch sein, prüfe es gegen eine feste Erlaubnisliste (Allowlist) – niemals direkt aus Benutzereingaben übernehmen.
+
Kapitel 45

Ergebnisse verarbeiten

Nach einer Abfrage holst du die Zeilen ab. Der Fetch-Modus bestimmt die Form – Array, Objekt, einzelne Spalte oder direkt in eine Klasse.

+ +

Eine Zeile oder alle

+
$stmt = $pdo->query("SELECT id, name FROM nutzer");
+$one = $stmt->fetch();        // nächste Zeile oder false
+$all = $pdo->query("SELECT id, name FROM nutzer")->fetchAll();   // alle Zeilen
+ +

Die Fetch-Modi

+

Als assoziatives Array, als Objekt, nur die erste Spalte als Liste oder als Schlüssel-Wert-Paare:

+
$pdo->query("SELECT * FROM nutzer")->fetch(\PDO::FETCH_ASSOC);   // ["id"=>1, "name"=>"Anna"]
+$o = $pdo->query("SELECT * FROM nutzer")->fetch(\PDO::FETCH_OBJ);    // $o->name
+$ids = $pdo->query("SELECT id FROM nutzer")->fetchAll(\PDO::FETCH_COLUMN);   // [1, 2, 3]
+$map = $pdo->query("SELECT id, name FROM nutzer")->fetchAll(\PDO::FETCH_KEY_PAIR);   // [1=>"Anna"]
+ +

Direkt in eine Klasse

+

FETCH_CLASS füllt die Spalten in Objekte deiner Klasse – Datensätze werden zu typisierten Objekten:

+
class User { public int $id; public string $name; }
+$user = $pdo->query("SELECT id, name FROM nutzer")
+    ->fetchAll(\PDO::FETCH_CLASS, User::class);
+echo $user[0]->name;   // Anna
+ +

Über Zeilen iterieren

+

Ein Statement ist direkt durchlaufbar – speicherschonend, da Zeile für Zeile:

+
foreach ($pdo->query("SELECT name FROM nutzer") as $line) {
+    echo $line["name"];
+}
+ +
+
i
+
fetchAll vs. iterierenfetchAll ist bequem, lädt aber alle Zeilen in den Speicher. Bei großen Ergebnismengen iteriere über das Statement (oben) – so hältst du immer nur eine Zeile. rowCount() liefert bei INSERT/UPDATE/DELETE die betroffenen Zeilen.
+
Kapitel 46

Transaktionen

Eine Transaktion bündelt mehrere Änderungen zu einer Einheit: Entweder alle gelingen, oder keine. Das hält die Daten konsistent, wenn mittendrin etwas schiefgeht.

+ +

Alles-oder-nichts

+

Das klassische Beispiel: Geld von einem Konto auf ein anderes – beide Buchungen müssen zusammen gelingen:

+
try {
+    $pdo->beginTransaction();
+
+    $pdo->prepare("UPDATE konto SET stand = stand - ? WHERE id = ?")
+        ->execute([100, 1]);
+    $pdo->prepare("UPDATE konto SET stand = stand + ? WHERE id = ?")
+        ->execute([100, 2]);
+
+    $pdo->commit();        // beide Buchungen endgültig
+} catch (\Throwable $e) {
+    $pdo->rollBack();      // bei Fehler: alles zurücknehmen
+    throw $e;
+}
+

Ohne Transaktion könnte das erste Konto belastet werden, die Gutschrift aber scheitern – Geld wäre verschwunden.

+ +

Wann lohnt sich das?

+

Immer, wenn mehrere Schreibvorgänge logisch zusammengehören: eine Bestellung samt ihren Positionen, ein Nutzer samt Profil. Auch zur Geschwindigkeit – viele Einfügungen in einer Transaktion sind deutlich schneller als einzeln.

+ +
+
+
try/commit/rollBack als MusterVerpacke zusammengehörige Schreibvorgänge immer in dieses Dreierschema: beginTransaction im try, commit am Ende, rollBack im catch. Wirf den Fehler danach weiter, damit der aufrufende Code von der gescheiterten Operation erfährt.
+
Kapitel 47

Datenbankdesign-Grundlagen

Wie du Daten in Tabellen organisierst, entscheidet über Wartbarkeit und Geschwindigkeit. Ein paar Grundregeln ersparen später viel Schmerz.

+ +

Tabellen, Schlüssel, Typen

+

Jede Tabelle braucht einen Primärschlüssel, der jede Zeile eindeutig identifiziert. Wähle passende Spaltentypen:

+
CREATE TABLE nutzer (
+    id        INT AUTO_INCREMENT PRIMARY KEY,
+    email     VARCHAR(255) NOT NULL UNIQUE,
+    erstellt  DATETIME NOT NULL
+);
+ +

Beziehungen über Fremdschlüssel

+

Statt Daten zu wiederholen, verweist eine Tabelle per Fremdschlüssel auf eine andere. Eine Bestellung gehört zu einem Nutzer:

+
CREATE TABLE bestellung (
+    id       INT AUTO_INCREMENT PRIMARY KEY,
+    nutzer_id INT NOT NULL,
+    betrag   INT NOT NULL,             -- in Cent!
+    FOREIGN KEY (nutzer_id) REFERENCES nutzer(id)
+);
+ +

Normalisierung

+

Speichere jede Tatsache genau einmal. Statt den Nutzernamen in jede Bestellung zu schreiben, steht er nur in nutzer und wird per Verknüpfung geholt:

+
SELECT b.id, n.email
+FROM bestellung b
+JOIN nutzer n ON n.id = b.nutzer_id;
+

Das verhindert Widersprüche: Ändert sich die E-Mail, gibt es nur eine Stelle zu pflegen.

+ +

Indizes für Tempo

+

Ein Index beschleunigt das Suchen in einer Spalte enorm – setze ihn auf Spalten, nach denen du häufig filterst oder verknüpfst:

+
CREATE INDEX idx_bestellung_nutzer ON bestellung(nutzer_id);
+ +
+
i
+
Indizes mit MaßIndizes beschleunigen das Lesen, verlangsamen aber das Schreiben und brauchen Platz. Indiziere gezielt die Spalten in WHERE- und JOIN-Bedingungen, nicht blind alles. Primär- und Fremdschlüssel sind meist automatisch oder sinnvollerweise indiziert.
+
Kapitel 48

ORM-Konzepte & Doctrine

Ein ORM (Object-Relational Mapper) bildet Datenbankzeilen auf Objekte ab. Statt SQL zu schreiben, arbeitest du mit Entitäten – das große PHP-ORM ist Doctrine.

+ +

Die Idee

+

Eine Tabelle wird zu einer Klasse (Entität), eine Zeile zu einem Objekt, Spalten zu Eigenschaften. Das Mapping beschreibst du heute mit Attributen:

+
use Doctrine\ORM\Mapping as ORM;
+
+#[ORM\Entity]
+class Product {
+    #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column]
+    public int $id;
+    #[ORM\Column]
+    public string $name;
+    #[ORM\Column]
+    public int $cent;
+}
+ +

Speichern & Laden

+

Der EntityManager übersetzt zwischen Objekten und SQL. Du fügst Objekte hinzu und rufst flush() – Doctrine erzeugt die passenden INSERT/UPDATE:

+
$p = new Product();
+$p->name = "Buch"; $p->cent = 1999;
+$em->persist($p);   // vormerken
+$em->flush();        // in die DB schreiben
+
+$found = $em->find(Product::class, 1);   // per ID laden
+echo $found->name;
+ +

Repositories für Abfragen

+

Wiederkehrende Abfragen kapselt ein Repository – sprechende Methoden statt SQL überall:

+
$repo = $em->getRepository(Product::class);
+$expensive = $repo->findBy(["name" => "Buch"]);
+$single = $repo->findOneBy(["id" => 1]);
+ +
+
i
+
ORM oder pures PDO?Ein ORM nimmt dir viel Schreibarbeit ab und hält die Domäne objektorientiert – ideal für große Anwendungen mit vielen Entitäten. Für einfache Skripte, Reports oder hochoptimierte Abfragen ist pures PDO direkter und schneller. Viele Projekte mischen beides bewusst.
+
Teil 10

Web-Entwicklung mit purem PHP

Das Fundament unter jedem Framework: Requests, Formulare, Sessions, Cookies, Uploads und HTTP.
49 · Wie PHP & Webserver zusammenspielen50 · Request & Superglobals51 · Formulare verarbeiten52 · Eingabevalidierung & Filter53 · Sessions54 · Cookies55 · Datei-Uploads56 · HTTP-Clients & APIs
Kapitel 49

Wie PHP & Webserver zusammenspielen

PHP wurde für das Web geboren. Pro HTTP-Anfrage startet ein Skript, baut eine Antwort und endet wieder – dieses „bei jeder Anfrage von vorn"-Modell prägt alles Weitere.

+ +

Der Ablauf einer Anfrage

+

Der Browser schickt eine Anfrage, der Webserver reicht sie an PHP, PHP führt das Skript aus, dessen Ausgabe wird zur HTTP-Antwort. Danach ist der gesamte Zustand wieder weg.

+
// index.php – läuft komplett bei jeder Anfrage neu
+echo "<h1>Hallo Welt</h1>";   // wird zum Antwort-Body
+ +

Zustandslosigkeit

+

Weil jede Anfrage frisch startet, gibt es keine Variablen, die zwischen zwei Aufrufen überleben. Wer Daten behalten will, braucht Sessions, Cookies oder eine Datenbank (kommt gleich).

+ +

Front-Controller

+

Moderne Anwendungen leiten alle Anfragen über eine einzige Datei – den Front-Controller. Der Webserver schreibt jede URL auf index.php um, die dann entscheidet, was zu tun ist:

+
$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
+echo match($path) {
+    "/"      => "Startseite",
+    "/hilfe" => "Hilfeseite",
+    default  => http_response_code(404) . "Nicht gefunden",
+};
+ +
+
i
+
Das Mentalmodell„Ein Request, ein frischer Prozess, eine Antwort, Ende." Anders als bei lang laufenden Servern (Node) musst du in klassischem PHP nichts manuell aufräumen – aber auch nichts im Speicher zwischenspeichern. Genau hier setzen die asynchronen Laufzeiten aus Teil 15 an.
+
Kapitel 50

Request & Superglobals

Alle Daten einer HTTP-Anfrage stellt PHP in vordefinierten „Superglobals" bereit – Arrays, die überall verfügbar sind.

+ +

$_GET und $_POST

+

$_GET enthält die Query-Parameter aus der URL, $_POST die Felder eines abgeschickten Formulars:

+
// Aufruf: /suche?q=php&seite=2
+echo $_GET["q"] ?? "";       // php
+echo $_POST["name"] ?? "";   // aus einem <form method="post">
+

Immer mit ?? absichern – fehlt der Schlüssel, gäbe es sonst eine Warnung.

+ +

$_SERVER

+

Enthält Informationen über die Anfrage und die Serverumgebung:

+
echo $_SERVER["REQUEST_METHOD"];   // GET, POST, ...
+echo $_SERVER["REQUEST_URI"];      // /suche?q=php
+echo $_SERVER["HTTP_USER_AGENT"];   // Browser-Kennung
+ +

Rohdaten & JSON-Body

+

Schickt ein Client JSON (statt Formularfeldern), liest du den rohen Body über einen Stream:

+
$raw = file_get_contents("php://input");
+$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
+ +

Weitere Superglobals

+

$_COOKIE (gesendete Cookies), $_FILES (Uploads), $_SESSION (Sitzungsdaten) und $_REQUEST (GET+POST gemischt – besser meiden) – jedes bekommt sein eigenes Kapitel bzw. wird gleich behandelt.

+ +
+
!
+
Superglobals sind roh und ungeprüftAlles in $_GET, $_POST & Co. kommt direkt vom Client und kann beliebig manipuliert sein. Nie ungeprüft in SQL, HTML oder Dateipfade einsetzen – immer erst validieren (nächstes Kapitel) und beim Ausgeben passend maskieren.
+
Kapitel 51

Formulare verarbeiten

Formulare sind der häufigste Weg, Daten vom Nutzer zu bekommen. Der Ablauf: anzeigen, abschicken, auf dem Server prüfen, reagieren.

+ +

GET oder POST?

+

GET für harmlose Abfragen (Suche, Filter) – die Daten stehen in der URL. POST für alles, was etwas ändert oder vertraulich ist (Anmeldung, Speichern) – die Daten stehen im Body.

+ +

Ein Formular im selben Skript

+

Ein verbreitetes Muster: dieselbe Seite zeigt das Formular und verarbeitet die Eingabe:

+
if ($_SERVER["REQUEST_METHOD"] === "POST") {
+    $name = trim($_POST["name"] ?? "");
+    if ($name === "") {
+        $error = "Name fehlt";
+    } else {
+        // speichern, weiterleiten ...
+        echo "Danke, " . htmlspecialchars($name);
+    }
+}
+ +

Werte im HTML korrekt einsetzen

+

Jeder Wert, der ins HTML zurückgeschrieben wird (z. B. um Eingaben zu erhalten), muss durch htmlspecialchars – sonst droht XSS:

+
<input name="name" value="<?= htmlspecialchars($name ?? "") ?>">
+ +

Post/Redirect/Get

+

Nach erfolgreichem POST leitest du per Redirect auf eine GET-Seite um. Das verhindert doppeltes Absenden beim Neuladen:

+
// nach dem Speichern:
+header("Location: /danke", true, 303);
+exit;
+ +
+
+
Serverseitig prüfen ist PflichtHTML-Attribute wie required oder type="email" sind reiner Komfort – sie lassen sich umgehen. Die echte Prüfung passiert immer auf dem Server. Und gegen Fremd-Absenden gehört ein CSRF-Token ins Formular (Sicherheits-Teil).
+
Kapitel 52

Eingabevalidierung & Filter

Jede Eingabe ist erst einmal verdächtig. Validierung prüft, ob ein Wert die erwartete Form hat; Filterung bringt ihn in eine saubere Form. PHP bringt dafür filter_var mit.

+ +

Validieren mit filter_var

+

Liefert den Wert zurück, wenn er gültig ist, sonst false:

+
var_dump(filter_var("a@b.de", FILTER_VALIDATE_EMAIL));   // "a@b.de"
+var_dump(filter_var("abc", FILTER_VALIDATE_INT));      // false
+var_dump(filter_var("3.14", FILTER_VALIDATE_FLOAT));   // 3.14
+var_dump(filter_var("https://php.net", FILTER_VALIDATE_URL));   // die URL
+var_dump(filter_var("yes", FILTER_VALIDATE_BOOLEAN));   // true
+ +

Mit Optionen

+

Ein Wertebereich für Zahlen – außerhalb gibt es false:

+
$age = filter_var($_POST["alter"] ?? "", FILTER_VALIDATE_INT, [
+    "options" => ["min_range" => 0, "max_range" => 120],
+]);
+ +

Eigene Regeln

+

Für alles, was kein eingebauter Filter abdeckt, prüfst du selbst – klar und früh:

+
$name = trim($_POST["name"] ?? "");
+$error = [];
+if (mb_strlen($name) < 2) $error[] = "Name zu kurz";
+if (!preg_match("/^[\p{L} -]+$/u", $name)) $error[] = "ungültige Zeichen";
+ +
+
+
Validieren ≠ MaskierenTrenne zwei Dinge: Validierung beim Hereinkommen (hat die E-Mail die richtige Form?) und kontextgerechtes Maskieren beim Hinausgeben (htmlspecialchars für HTML, Prepared Statements für SQL). Beides ist nötig – das eine ersetzt das andere nicht.
+
Kapitel 53

Sessions

Eine Session hält Daten über mehrere Anfragen desselben Nutzers hinweg – die Antwort auf PHPs Zustandslosigkeit. Typischer Einsatz: „wer ist angemeldet?".

+ +

Starten & nutzen

+

session_start() muss vor jeder Ausgabe stehen. Danach ist $_SESSION ein Array, das gespeichert und beim nächsten Request wieder geladen wird:

+
session_start();
+$_SESSION["nutzer_id"] = 42;   // merken
+// bei der nächsten Anfrage:
+echo $_SESSION["nutzer_id"] ?? "Gast";
+

Technisch bekommt der Browser ein Cookie mit der Session-ID; die eigentlichen Daten liegen auf dem Server.

+ +

Anmeldung merken

+
session_start();
+if (passwordMatches($_POST["pw"])) {
+    session_regenerate_id(true);     // neue ID gegen Session-Fixation
+    $_SESSION["nutzer_id"] = $id;
+}
+ +

Abmelden

+
session_start();
+$_SESSION = [];          // Daten leeren
+session_destroy();       // Session serverseitig beenden
+ +
+
!
+
ID bei Anmeldung erneuernRufe nach erfolgreicher Anmeldung session_regenerate_id(true) auf. Sonst könnte ein Angreifer dem Opfer vorab eine bekannte Session-ID unterschieben und nach dessen Login mitschwimmen (Session-Fixation). Wie man die Session-Cookies härtet, zeigt der Sicherheits-Teil.
+
Kapitel 54

Cookies

Cookies sind kleine Werte, die der Browser speichert und bei jeder Anfrage mitschickt. Sessions nutzen sie für die ID; daneben dienen sie für Einstellungen oder „Angemeldet bleiben".

+ +

Setzen mit setcookie

+

Muss vor jeder Ausgabe stehen. Das Optionen-Array steuert Gültigkeit und Sicherheit:

+
setcookie("theme", "dunkel", [
+    "expires"  => time() + 86400 * 30,   // 30 Tage
+    "path"     => "/",
+    "secure"   => true,                  // nur über HTTPS
+    "httponly" => true,                  // für JavaScript unsichtbar
+    "samesite" => "Lax",                 // gegen CSRF
+]);
+ +

Lesen

+

Gesendete Cookies stehen in $_COOKIE – verfügbar ab der nächsten Anfrage, nicht in derselben:

+
echo $_COOKIE["theme"] ?? "hell";   // dunkel
+ +

Löschen

+

Ein Cookie löscht man, indem man es mit einem Ablaufdatum in der Vergangenheit überschreibt:

+
setcookie("theme", "", ["expires" => time() - 3600, "path" => "/"]);
+ +
+
!
+
Cookies sind manipulierbarDer Inhalt liegt beim Nutzer und kann verändert werden – speichere dort nie etwas Vertrauliches oder Sicherheitsrelevantes im Klartext (etwa „ist_admin=1"). Setze httponly, secure und samesite immer; speichere Sensibles in der serverseitigen Session.
+
Kapitel 55

Datei-Uploads

Hochgeladene Dateien landen in $_FILES. Hier ist besondere Vorsicht nötig: Nichts an einer hochgeladenen Datei darf man dem Client glauben.

+ +

Das Formular

+
<form method="post" enctype="multipart/form-data">
+  <input type="file" name="bild">
+</form>
+

Ohne enctype="multipart/form-data" kommt keine Datei an.

+ +

Sicher entgegennehmen

+

Prüfe den Fehlercode, bestimme den Typ selbst (nicht aus dem Client-Header) und verschiebe mit move_uploaded_file:

+
$f = $_FILES["bild"];
+if ($f["error"] !== UPLOAD_ERR_OK) {
+    throw new \RuntimeException("Upload fehlgeschlagen");
+}
+$type = mime_content_type($f["tmp_name"]);   // echten Typ aus dem Inhalt
+if (!in_array($type, ["image/png", "image/jpeg"], true)) {
+    throw new \RuntimeException("nur PNG/JPEG");
+}
+$target = "uploads/" . bin2hex(random_bytes(8)) . ".bin";   // eigener Name
+move_uploaded_file($f["tmp_name"], $target);
+ +
+
!
+
Drei Regeln für Uploads(1) Vertraue nie dem mitgelieferten Dateinamen oder MIME-Typ – bestimme den Typ aus dem Inhalt und vergib einen eigenen Namen. (2) Lege Uploads außerhalb des öffentlichen Verzeichnisses ab oder verbiete dort die Ausführung – sonst lädt jemand ein PHP-Skript hoch. (3) Begrenze Größe und Typ. Eine als „bild.jpg" benannte Datei kann beliebigen Inhalt haben.
+
Kapitel 56

HTTP-Clients & APIs

Oft ist PHP nicht nur Server, sondern auch Client: Es ruft fremde APIs auf. Für einfache Fälle reicht cURL bzw. Streams, für echte Projekte ein Client wie Guzzle.

+ +

GET mit cURL

+
$ch = curl_init("https://api.example.com/status");
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);   // Antwort zurückgeben statt ausgeben
+$response = curl_exec($ch);
+$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+curl_close($ch);
+echo $code;   // 200
+ +

POST mit JSON

+
$ch = curl_init("https://api.example.com/nutzer");
+curl_setopt_array($ch, [
+    CURLOPT_RETURNTRANSFER => true,
+    CURLOPT_POST           => true,
+    CURLOPT_HTTPHEADER     => ["Content-Type: application/json"],
+    CURLOPT_POSTFIELDS     => json_encode(["name" => "Anna"]),
+]);
+$data = json_decode(curl_exec($ch), true);
+ +

Mit Guzzle

+

Komfortabler und besser testbar – der De-facto-Standard-Client:

+
use GuzzleHttp\Client;
+$client = new Client();
+$res = $client->post("https://api.example.com/nutzer", [
+    "json" => ["name" => "Anna"],
+]);
+echo $res->getStatusCode();          // 201
+$data = json_decode((string) $res->getBody(), true);
+ +
+
i
+
Timeouts & Fehler einplanenExterne APIs sind langsam oder fallen aus. Setze immer ein Timeout, prüfe den Status-Code und fange Netzwerkfehler ab – sonst hängt deine Seite an einem fremden Dienst. Standardisierte Clients (PSR-18) und Guzzle nehmen dir Wiederholungen, Header und Fehlerbehandlung weitgehend ab.
+
Teil 11

Sicherheit

Sicherheit ist eine Haltung, kein nachträgliches Feature. Die großen Angriffsklassen einzeln – und wie man sie schließt.
57 · Das Grundprinzip: niemals vertrauen58 · SQL-Injection im Detail59 · XSS & CSRF im Detail60 · Authentifizierung & Passwörter61 · Sessions & Cookies absichern62 · Kryptografie & Sodium
Kapitel 57

Das Grundprinzip: niemals vertrauen

Sicherheit ist keine Funktion, die man am Ende anschraubt, sondern eine Haltung beim Schreiben jeder Zeile. Das Kernprinzip: Traue keiner Eingabe – egal woher.

+ +

Die zwei Bewegungsrichtungen

+

Daten kommen herein (Request, Datei, API) und gehen hinaus (HTML, SQL, Shell). An jeder Grenze gilt eine eigene Regel:

+
// HEREIN: validieren – hat der Wert die erwartete Form?
+$age = filter_var($_POST["alter"] ?? "", FILTER_VALIDATE_INT);
+
+// HINAUS: kontextgerecht maskieren – passend zum Ziel
+echo htmlspecialchars($name);              // fürs HTML
+$stmt->execute([$name]);                    // für SQL (Prepared Statement)
+ +

Defense in Depth

+

Verlasse dich nie auf eine einzige Schutzschicht. Eingaben werden validiert und Ausgaben maskiert und Rechte geprüft. Fällt eine Schicht, hält die nächste.

+ +

Least Privilege

+

Gib jedem Teil nur die Rechte, die er braucht: Der Datenbank-Benutzer der Web-App darf keine Tabellen löschen, Upload-Ordner sind nicht ausführbar, Fehlermeldungen verraten nichts über interne Pfade.

+ +
+
i
+
Die DenkweiseFrage bei jeder Eingabe: „Was, wenn hier das Bösartigste steht, das technisch möglich ist?" Und bei jeder Ausgabe: „In welchem Kontext landet dieser Wert – HTML, SQL, Shell, URL?" Die folgenden Kapitel sind nur konkrete Anwendungen dieser zwei Fragen.
+
Kapitel 58

SQL-Injection im Detail

SQL-Injection entsteht, wenn Benutzereingaben als Teil eines SQL-Befehls interpretiert werden. Sie gehört zu den ältesten und gefährlichsten Lücken – und ist vollständig vermeidbar.

+ +

Wie der Angriff funktioniert

+
// verwundbar:
+$sql = "SELECT * FROM nutzer WHERE name = '" . $_GET["name"] . "'";
+// Eingabe:  '; DROP TABLE nutzer; --
+// ergibt:   SELECT ... WHERE name = ''; DROP TABLE nutzer; --'
+

Die Eingabe bricht aus dem String aus und schmuggelt einen eigenen Befehl ein.

+ +

Die Lösung: Prepared Statements

+

Werte gehören als Parameter übergeben, getrennt vom SQL. Die Datenbank behandelt sie dann garantiert als Daten:

+
$stmt = $pdo->prepare("SELECT * FROM nutzer WHERE name = :name");
+$stmt->execute(["name" => $_GET["name"]]);   // sicher, egal was drinsteht
+ +

Was Platzhalter NICHT können

+

Tabellen-/Spaltennamen und Sortierrichtungen sind keine Werte. Müssen sie dynamisch sein, gegen eine feste Liste prüfen:

+
$allowed = ["name", "datum"];
+$column = in_array($_GET["sort"] ?? "", $allowed, true)
+    ? $_GET["sort"] : "name";
+$sql = "SELECT * FROM eintrag ORDER BY $column";   // nur erlaubte Spalten
+ +
+
!
+
Niemals Eingaben in SQL klebenEs gibt keinen sicheren Weg, Benutzereingaben per String-Verkettung in SQL einzubauen – auch nicht „durch Escaping selbst gemacht". Nutze immer Prepared Statements. Für Strukturteile (Spalten, Tabellen) gilt: Allowlist statt Eingabe.
+
Kapitel 59

XSS & CSRF im Detail

Zwei der häufigsten Web-Angriffe: XSS schmuggelt fremdes JavaScript in deine Seite; CSRF lässt das Opfer ungewollt Aktionen auslösen. Beide haben klare Gegenmittel.

+ +

XSS

+

Gibt man Benutzereingaben ungefiltert ins HTML, kann der Angreifer Skripte einschleusen, die im Browser anderer Nutzer laufen:

+
// verwundbar:
+echo "<p>" . $_GET["text"] . "</p>";
+// Eingabe:  <script>stehleCookies()</script>
+

Schutz: jeden ausgegebenen Wert mit htmlspecialchars maskieren – aus < wird &lt;, der Code wird Text statt Befehl:

+
echo "<p>" . htmlspecialchars($_GET["text"], ENT_QUOTES) . "</p>";
+

Template-Engines wie Twig maskieren automatisch – einer ihrer großen Sicherheitsvorteile.

+ +

CSRF

+

Ein eingeloggter Nutzer besucht eine fremde Seite, die heimlich ein Formular an deine App schickt. Da der Browser die Session-Cookies mitsendet, sieht die Aktion echt aus.

+

Schutz: ein unvorhersehbares Token, das nur dein Formular kennt:

+
// beim Anzeigen des Formulars:
+$_SESSION["csrf"] = bin2hex(random_bytes(32));
+// im Formular:  <input type="hidden" name="csrf" value="...">
+
+// beim Verarbeiten:
+if (!hash_equals($_SESSION["csrf"] ?? "", $_POST["csrf"] ?? "")) {
+    http_response_code(403);
+    exit("ungültiges Token");
+}
+ +
+
!
+
Kontext entscheidet die Maskierunghtmlspecialchars schützt im HTML-Textkontext. In einem JavaScript-Block, einem URL-Attribut oder CSS gelten andere Regeln. Im Zweifel gibt man Daten nicht in solche Kontexte – und ein SameSite-Cookie plus CSRF-Token deckt die Formular-Seite ab.
+
Kapitel 60

Authentifizierung & Passwörter

Passwörter sind das wertvollste, was eine App verwahrt. PHP macht es richtig einfach, sie sicher zu behandeln – man muss nur die zwei eingebauten Funktionen nutzen.

+ +

Hashen, nie verschlüsseln

+

password_hash erzeugt einen sicheren, gesalzenen Hash (standardmäßig bcrypt). Den speicherst du – nie das Klartext-Passwort:

+
$hash = password_hash($_POST["pw"], PASSWORD_DEFAULT);
+// in die DB: ein langer String wie $2y$12$...
+ +

Prüfen mit password_verify

+

Beim Login vergleichst du die Eingabe gegen den gespeicherten Hash – sicher und in konstanter Zeit:

+
if (password_verify($_POST["pw"], $hashFromDb)) {
+    // Passwort korrekt – anmelden
+} else {
+    // falsch
+}
+ +

Hashes aktuell halten

+

Wird das Standardverfahren stärker, prüfst du beim erfolgreichen Login, ob ein Neu-Hashen sinnvoll ist:

+
if (password_needs_rehash($hashFromDb, PASSWORD_DEFAULT)) {
+    $fresh = password_hash($_POST["pw"], PASSWORD_DEFAULT);
+    // $fresh in der DB speichern
+}
+ +
+
!
+
Tabu-ListeNiemals Passwörter im Klartext speichern, niemals mit md5 oder sha1 „hashen" (zu schnell, knackbar), niemals selbst ein Salt basteln – password_hash erledigt das. Vergleiche Hashes nur mit password_verify bzw. hash_equals, nie mit == (zeitbasierte Angriffe).
+
Kapitel 61

Sessions & Cookies absichern

Eine Session ist nur so sicher wie ihr Cookie und ihr Lebenszyklus. Ein paar Einstellungen und Gewohnheiten verhindern die häufigsten Übernahmen.

+ +

Sichere Cookie-Einstellungen

+

Setze die Session-Cookie-Optionen, bevor du die Session startest:

+
session_set_cookie_params([
+    "secure"   => true,    // nur über HTTPS übertragen
+    "httponly" => true,    // kein Zugriff per JavaScript (gegen XSS-Diebstahl)
+    "samesite" => "Lax",   // gegen CSRF
+]);
+session_start();
+ +

ID bei Rechtewechsel erneuern

+

Nach Anmeldung oder Rechteänderung eine frische ID vergeben – gegen Session-Fixation:

+
session_regenerate_id(true);   // alte ID verfällt
+ +

Sauber abmelden & ablaufen lassen

+
$_SESSION = [];
+session_destroy();
+

Begrenze außerdem die Lebensdauer und prüfe Inaktivität – eine ewig gültige Session ist ein Risiko:

+
if (($_SESSION["letzte_aktion"] ?? 0) < time() - 1800) {
+    session_destroy();   // nach 30 Min. Inaktivität raus
+}
+$_SESSION["letzte_aktion"] = time();
+ +
+
!
+
HTTPS ist die VoraussetzungOhne HTTPS lässt sich das Session-Cookie im Netzwerk mitlesen, und alle obigen Maßnahmen verpuffen. secure erzwingt die Übertragung nur über verschlüsselte Verbindungen – im Web heute Pflicht, nicht Kür.
+
Kapitel 62

Kryptografie & Sodium

Wenn du Daten verschlüsseln oder signieren musst, baue nichts selbst. PHP hat mit der Sodium-Bibliothek moderne, sichere Bausteine direkt eingebaut.

+ +

Zufall

+

Sichere Tokens, Salts und Schlüssel kommen nur aus einem kryptografischen Zufallsgenerator:

+
$token = bin2hex(random_bytes(32));   // 64 Hex-Zeichen, unvorhersehbar
+echo random_int(100000, 999999);      // sicherer 6-stelliger Code
+ +

Symmetrisch verschlüsseln mit Sodium

+

Ein Schlüssel ver- und entschlüsselt. Sodium erledigt Nonce und Authentifizierung korrekt:

+
$key = sodium_crypto_secretbox_keygen();
+$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
+$secret = sodium_crypto_secretbox("vertraulich", $nonce, $key);
+
+$plain = sodium_crypto_secretbox_open($secret, $nonce, $key);
+echo $plain;   // vertraulich
+

Der $nonce muss pro Nachricht einmalig sein, darf aber öffentlich neben dem Geheimtext stehen.

+ +

Werte vergleichen ohne Timing-Leck

+

Geheimnisse (Tokens, Hashes) vergleichst du in konstanter Zeit, damit kein Angreifer aus der Antwortzeit Rückschlüsse zieht:

+
if (hash_equals($expected, $input)) {
+    // gleich – sicher verglichen
+}
+ +
+
!
+
Niemals eigene KryptoSelbstgebaute Verschlüsselung ist fast immer angreifbar. Nutze Sodium für Verschlüsselung/Signaturen, password_hash für Passwörter, random_bytes/random_int für Zufall. Verwechsle Kodierung (Base64) nicht mit Verschlüsselung – Base64 schützt nichts.
+
Teil 12

Sauberes Design

Wie man Code so baut, dass er wachsen, sich ändern und testen lässt: SOLID, Dependency Injection, Wertobjekte und eine durchdachte Fehlerstrategie.
63 · SOLID-Prinzipien64 · Dependency Injection65 · Wertobjekte & DTOs66 · Fehlerbehandlung als Architektur67 · Häufige Entwurfsmuster
Kapitel 63

SOLID-Prinzipien

SOLID ist eine Eselsbrücke für fünf Prinzipien, die Code wandelbar und testbar halten. Jedes mit einer knappen Idee und einem Beispiel.

+ +

S – Single Responsibility

+

Eine Klasse hat genau einen Grund, sich zu ändern. Statt eine Klasse, die rechnet und speichert und mailt, trennst du das:

+
class InvoiceCalculator { public function sum(array $items): int { return array_sum($items); } }
+class InvoiceStorage { public function save(int $sum): void {} }
+// jede Klasse ein Zuständigkeitsbereich
+ +

O – Open/Closed

+

Offen für Erweiterung, geschlossen für Änderung: Neues Verhalten durch neue Klassen, nicht durch Umbau bestehender. Eine neue Versandart ist eine neue Klasse, kein weiteres if:

+
interface Shipping { public function cost(): int; }
+class Standard implements Shipping { public function cost(): int { return 499; } }
+class Express implements Shipping { public function cost(): int { return 999; } }
+ +

L – Liskov Substitution

+

Ein Untertyp muss überall dort funktionieren, wo der Basistyp erwartet wird – ohne Überraschungen. Wer Shipping erwartet, darf Express bekommen und sich auf cost() verlassen.

+ +

I – Interface Segregation

+

Viele kleine, gezielte Interfaces statt eines großen. Niemand soll Methoden implementieren müssen, die er nicht braucht:

+
interface Readable { public function read(): string; }
+interface Writable { public function write(string $s): void; }
+// eine Klasse erfüllt nur, was sie wirklich kann
+ +

D – Dependency Inversion

+

Hänge von Abstraktionen ab, nicht von konkreten Klassen. Der Bestellprozess kennt das Interface Shipping, nicht Express – die konkrete Wahl wird hineingereicht (nächstes Kapitel).

+ +
+
i
+
Prinzipien, keine GesetzeSOLID sind Heuristiken gegen starren, schwer testbaren Code – kein Selbstzweck. Wende sie an, wo Code sich tatsächlich ändert; übertreibe es nicht bei kleinen, stabilen Skripten. Der rote Faden: gegen Interfaces programmieren und Verantwortlichkeiten trennen.
+
Kapitel 64

Dependency Injection

Dependency Injection (DI) heißt schlicht: Eine Klasse bekommt ihre Abhängigkeiten von außen gereicht, statt sie selbst zu erzeugen. Das macht sie austauschbar und testbar.

+ +

Ohne DI

+

Erzeugt eine Klasse ihre Abhängigkeit selbst, ist sie fest damit verdrahtet und im Test nicht austauschbar:

+
class Order {
+    private MySqlStorage $db;
+    public function __construct() {
+        $this->db = new MySqlStorage();   // fest – im Test nicht ersetzbar
+    }
+}
+ +

Mit DI

+

Die Abhängigkeit kommt als Konstruktor-Parameter, typisiert auf ein Interface:

+
interface Storage { public function save(array $d): void; }
+
+class Order {
+    public function __construct(private Storage $storage) {}
+    public function complete(array $d): void {
+        $this->storage->save($d);
+    }
+}
+$b = new Order(new MySqlStorage());   // echte DB
+$test = new Order(new StorageStub());   // im Test ein Dummy
+

Dieselbe Klasse funktioniert mit echter Datenbank, In-Memory-Attrappe oder einem anderen Backend – ohne sie zu ändern.

+ +

Der DI-Container

+

In größeren Projekten erzeugt ein Container die Objekte samt ihrer Abhängigkeiten automatisch (per Reflection, vgl. Teil 6). Du fragst nach einem Typ und bekommst ein fertig verdrahtetes Objekt:

+
$order = $container->get(Order::class);
+// Container baut selbst den passenden Speicher und reicht ihn hinein
+ +
+
+
Konstruktor-Injection als StandardReiche Pflicht-Abhängigkeiten über den Konstruktor herein und typisiere sie auf Interfaces. So ist auf einen Blick sichtbar, was eine Klasse braucht, sie lässt sich isoliert testen, und der Container kann sie automatisch zusammenbauen. Vermeide es, den Container überall durchzureichen (das wäre ein „Service Locator" – ein Anti-Muster).
+
Kapitel 65

Wertobjekte & DTOs

Nicht jedes Objekt hat eine Identität. Ein Wertobjekt ist allein durch seinen Wert definiert und unveränderlich; ein DTO transportiert nur Daten. Beide machen Code klarer und sicherer.

+ +

Wertobjekt

+

Ein Geldbetrag, eine E-Mail, ein Datum: gleich, wenn der Wert gleich ist, und immer gültig, weil der Konstruktor prüft:

+
final class Email {
+    public function __construct(public readonly string $value) {
+        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
+            throw new \InvalidArgumentException("keine E-Mail");
+        }
+    }
+}
+$e = new Email("a@b.de");   // ab hier garantiert gültig
+echo $e->value;            // a@b.de
+// new Email("kaputt");   // wirft sofort
+

Der Vorteil: Wer eine Email in der Hand hält, muss nie wieder prüfen, ob sie gültig ist.

+ +

Ändern heißt: neues Objekt

+

Da Wertobjekte unveränderlich sind, liefert eine „Änderung" eine neue Instanz – mit PHP 8.5 elegant per clone with:

+
final class Money {
+    public function __construct(public readonly int $cent) {}
+    public function plus(Money $x): static { return new static($this->cent + $x->cent); }
+}
+echo (new Money(1000))->plus(new Money(500))->cent;   // 1500
+ +

DTO

+

Ein Data Transfer Object bündelt zusammengehörige Felder, etwa eine validierte Formulareingabe, ohne Verhalten:

+
final class RegistrationData {
+    public function __construct(
+        public readonly string $name,
+        public readonly Email $email,
+    ) {}
+}
+

Statt einem unklaren Array mit Stringschlüsseln reicht man ein typisiertes Objekt herum – die IDE kennt jedes Feld.

+ +
+
+
Primitive Obsession vermeidenWo du sonst überall string $email oder int $cent durchreichst, lohnt ein Wertobjekt: Es bündelt die Gültigkeitsprüfung an einer Stelle und macht falsche Verwendung unmöglich. readonly und final sind dabei die natürlichen Begleiter.
+
Kapitel 66

Fehlerbehandlung als Architektur

Wie eine Anwendung mit Fehlern umgeht, ist eine Designentscheidung – kein Detail. Eine durchdachte Strategie macht den Unterschied zwischen einer robusten App und einem Kartenhaus.

+ +

Eigene, sprechende Exception-Typen

+

Statt überall generische Exceptions zu werfen, baue eine kleine Hierarchie, die deine Domäne abbildet:

+
class DomainException extends \Exception {}
+class AccountOverdrawn extends DomainException {}
+class AccountLocked extends DomainException {}
+
+function withdraw(int $balance, int $amount): int {
+    if ($amount > $balance) throw new AccountOverdrawn();
+    return $balance - $amount;
+}
+ +

Fangen, wo man handeln kann

+

Eine Exception steigt auf, bis eine Stelle sie sinnvoll behandeln kann. Tief im Code wirfst du nur; oben – z. B. im Controller – entscheidest du über die Reaktion:

+
try {
+    $fresh = withdraw($balance, $amount);
+} catch (AccountOverdrawn) {
+    http_response_code(422);
+    echo "Betrag übersteigt das Guthaben";
+}
+ +

Eine zentrale Fehlerstelle

+

Ganz außen fängt ein globaler Handler alles Unerwartete, protokolliert es und zeigt dem Nutzer eine neutrale Meldung – nie einen Stacktrace:

+
set_exception_handler(function(\Throwable $e) {
+    error_log((string) $e);          // für Entwickler ins Log
+    http_response_code(500);
+    echo "Es ist ein Fehler aufgetreten.";   // für den Nutzer neutral
+});
+ +
+
!
+
Erwartet vs. unerwartet trennenFachliche, erwartbare Fälle (Konto überzogen) behandelst du gezielt nah am Geschehen. Unerwartete Fehler (DB weg, Bug) lässt du bis zum zentralen Handler durchlaufen, der protokolliert und neutral antwortet. Zeige Endnutzern niemals interne Details – das hilft nur Angreifern.
+
Kapitel 67

Häufige Entwurfsmuster

Bevor der nächste Teil den vollständigen Muster-Katalog systematisch durchgeht, hier die drei, die dir im PHP-Alltag am häufigsten begegnen – jeweils mit lauffähigem Mini-Beispiel.

+ +

Strategy

+

Ein Algorithmus wird hinter einem Interface gekapselt und zur Laufzeit gewählt:

+
interface Discount { public function apply(int $cent): int; }
+class NoDiscount implements Discount { public function apply(int $c): int { return $c; } }
+class PercentageDiscount implements Discount {
+    public function __construct(private int $percent) {}
+    public function apply(int $c): int { return (int) ($c * (100 - $this->percent) / 100); }
+}
+function finalPrice(int $c, Discount $r): int { return $r->apply($c); }
+echo finalPrice(1000, new PercentageDiscount(20));   // 800
+ +

Factory

+

Eine Funktion oder Methode entscheidet, welches Objekt entsteht – der Aufrufer muss die konkreten Klassen nicht kennen:

+
function discountFor(string $customer): Discount {
+    return match($customer) {
+        "stamm" => new PercentageDiscount(10),
+        default => new NoDiscount(),
+    };
+}
+echo finalPrice(1000, discountFor("stamm"));   // 900
+ +

Observer

+

Ein Subjekt benachrichtigt mehrere Beobachter, ohne sie konkret zu kennen:

+
interface Observer { public function notify(string $event): void; }
+class Newsletter implements Observer {
+    public function notify(string $e): void { echo "Mail: $e\n"; }
+}
+class Shop {
+    private array $observers = [];
+    public function subscribe(Observer $b): void { $this->observers[] = $b; }
+    public function sell(): void {
+        foreach ($this->observers as $b) $b->notify("Verkauf");
+    }
+}
+$shop = new Shop(); $shop->subscribe(new Newsletter());
+$shop->sell();   // Mail: Verkauf
+ +
+
i
+
Muster sind VokabelnEntwurfsmuster sind bewährte Lösungen mit Namen – sie helfen vor allem, im Team über Struktur zu reden. Setze sie ein, wenn das Problem wirklich da ist, nicht auf Vorrat. Der nächste Teil ordnet den vollständigen Katalog systematisch.
+
Teil 13

Entwurfsmuster & Architektur

Der systematische Muster-Katalog und die großen Architekturansätze – von MVC über DDD bis zur hexagonalen Architektur.
68 · Erzeugungsmuster69 · Strukturmuster70 · Verhaltensmuster71 · MVC & Schichtenarchitektur72 · Domain-Driven Design73 · Event-getriebene Architektur74 · Hexagonale Architektur
Kapitel 68

Erzeugungsmuster

Erzeugungsmuster (Creational Patterns) regeln, wie Objekte entstehen – getrennt von ihrer Verwendung. So bleibt der Code flexibel, wenn sich die Erzeugung ändert.

+ +

Factory Method

+

Eine Methode liefert das passende Objekt, der Aufrufer kennt nur das Interface:

+
interface Exporter { public function export(array $d): string; }
+class JsonExporter implements Exporter {
+    public function export(array $d): string { return json_encode($d); }
+}
+function exporter(string $format): Exporter {
+    return match($format) { "json" => new JsonExporter() };
+}
+echo exporter("json")->export(["a" => 1]);   // {"a":1}
+ +

Builder

+

Baut ein komplexes Objekt Schritt für Schritt – lesbar durch verkettete Aufrufe:

+
class QueryBuilder {
+    private array $parts = [];
+    public function select(string $s): static { $this->parts["select"] = $s; return $this; }
+    public function from(string $t): static { $this->parts["from"] = $t; return $this; }
+    public function sql(): string { return "SELECT {$this->parts['select']} FROM {$this->parts['from']}"; }
+}
+echo (new QueryBuilder())->select("*")->from("nutzer")->sql();   // SELECT * FROM nutzer
+ +

Singleton

+

Stellt sicher, dass es von einer Klasse nur eine Instanz gibt:

+
class Config {
+    private static ?Konfig $instance = null;
+    private function __construct() {}
+    public static function get(): static {
+        return self::$instance ??= new static();
+    }
+}
+var_dump(Config::get() === Config::get());   // true – stets dasselbe Objekt
+

Singletons sind globaler Zustand und erschweren Tests – in modernem Code übernimmt meist der DI-Container die „eine Instanz".

+ +
+
i
+
Erzeugung vom Rest trennenDer gemeinsame Nutzen: Wo und wie ein Objekt entsteht, ist von seiner Verwendung entkoppelt. Ändert sich die Konstruktion (neue Pflichtabhängigkeit, anderes Backend), bleibt der nutzende Code unberührt.
+
Kapitel 69

Strukturmuster

Strukturmuster (Structural Patterns) setzen Klassen und Objekte zu größeren Strukturen zusammen – etwa um Schnittstellen anzupassen oder Verhalten zu ergänzen.

+ +

Adapter

+

Macht eine fremde Klasse mit unpassender Schnittstelle nutzbar, ohne sie zu ändern:

+
interface Logger { public function log(string $m): void; }
+class ThirdPartyLog { public function write(string $t): void { echo $t; } }
+
+class ThirdPartyLogAdapter implements Logger {
+    public function __construct(private ThirdPartyLog $f) {}
+    public function log(string $m): void { $this->f->write($m); }   // übersetzt log() auf write()
+}
+(new ThirdPartyLogAdapter(new ThirdPartyLog()))->log("Hallo");   // Hallo
+ +

Decorator

+

Legt sich um ein Objekt und erweitert es, ohne dessen Klasse zu ändern – beliebig stapelbar:

+
class TimestampLog implements Logger {
+    public function __construct(private Logger $inner) {}
+    public function log(string $m): void {
+        $this->inner->log("[2026] " . $m);   // ergänzt, dann weiterreichen
+    }
+}
+(new TimestampLog(new ThirdPartyLogAdapter(new ThirdPartyLog())))->log("X");   // [2026] X
+ +

Facade

+

Bündelt ein kompliziertes Zusammenspiel hinter einer schlichten Schnittstelle:

+
class Checkout {
+    public function __construct(private Inventory $l, private Payment $z, private Shipping $v) {}
+    public function complete(int $id): void {
+        $this->l->reserve($id); $this->z->book($id); $this->v->start($id);
+    }   // ein Aufruf statt drei Subsysteme von Hand zu orchestrieren
+}
+ +
+
i
+
Komposition als WerkzeugStrukturmuster bauen alle auf demselben Gedanken: ein Objekt hält ein anderes und leitet weiter. Adapter übersetzt, Decorator ergänzt, Facade vereinfacht. Genau das ist „Komposition vor Vererbung" in der Praxis.
+
Kapitel 70

Verhaltensmuster

Verhaltensmuster (Behavioral Patterns) regeln, wie Objekte zusammenarbeiten und Verantwortung verteilen – wer wen wann aufruft.

+ +

Strategy

+

Austauschbare Algorithmen hinter einem Interface (im vorigen Teil eingeführt). Kurz als Erinnerung mit Ausgabe:

+
interface Sorting { public function sort(array $a): array; }
+class Ascending implements Sorting {
+    public function sort(array $a): array { sort($a); return $a; }
+}
+print_r((new Ascending())->sort([3, 1, 2]));   // [1, 2, 3]
+ +

Observer

+

Mehrere Zuhörer reagieren auf ein Ereignis – die Basis von Event-Systemen (eigenes Architektur-Kapitel folgt).

+ +

Command

+

Eine Aktion wird zum Objekt mit einer ausfuehren()-Methode – so lässt sie sich speichern, in eine Queue legen oder rückgängig machen:

+
interface Command { public function ausfuehren(): string; }
+class SendMail implements Command {
+    public function __construct(private string $to) {}
+    public function ausfuehren(): string { return "Mail an {$this->to}"; }
+}
+$queue = [new SendMail("a@b.de"), new SendMail("c@d.de")];
+foreach ($queue as $k) echo $k->ausfuehren() . "\n";   // abgearbeitet
+ +

Template Method

+

Eine Basisklasse legt den Ablauf fest und lässt einzelne Schritte offen (vgl. abstrakte Klassen):

+
abstract class Import {
+    public function run(): string { return $this->read() . " -> gespeichert"; }   // fester Ablauf
+    abstract protected function read(): string;                          // variabler Schritt
+}
+class CsvImport extends Import {
+    protected function read(): string { return "CSV gelesen"; }
+}
+echo (new CsvImport())->run();   // CSV gelesen -> gespeichert
+ +
+
i
+
Verantwortung verteilenVerhaltensmuster beantworten „wer macht was?". Strategy lagert wie aus, Command verpackt was, Observer verteilt Reaktionen, Template Method fixiert die Reihenfolge. Viele moderne Frameworks bestehen im Kern aus diesen Mustern.
+
Kapitel 71

MVC & Schichtenarchitektur

MVC und Schichten ordnen eine ganze Anwendung. Sie trennen, was zusammengehört: Darstellung, Ablaufsteuerung und Fachlogik bekommen je ihren Platz.

+ +

Die drei Rollen von MVC

+

Model hält Daten und Fachlogik, View stellt dar, Controller nimmt die Anfrage entgegen und koordiniert:

+
// Model: Fachlogik, kennt weder HTTP noch HTML
+class Cart {
+    private array $items = [];
+    public function add(int $cent): void { $this->items[] = $cent; }
+    public function sum(): int { return array_sum($this->items); }
+}
+
+// Controller: verbindet Request, Model und View
+class CartController {
+    public function show(Cart $k): string {
+        return "Summe: " . number_format($k->sum() / 100, 2) . " €";   // View-Aufbereitung
+    }
+}
+$k = new Cart(); $k->add(1999); $k->add(500);
+echo (new CartController())->show($k);   // Summe: 24.99 €
+ +

Schichten

+

Etwas allgemeiner ordnet man Code in Schichten, die nur nach unten zeigen: Präsentation (Controller, Views) → Anwendung/Domäne (Fachlogik) → Infrastruktur (Datenbank, externe Dienste). Die Domäne kennt keine Datenbank, nur ein Interface – sie bleibt rein.

+ +
+
+
Logik raus aus dem ControllerDer häufigste Anfängerfehler ist der „dicke Controller", der alles selbst macht. Halte Controller dünn: Sie nehmen die Eingabe entgegen, rufen die Fachlogik (Model/Service) und wählen die Ausgabe. Die eigentliche Logik gehört ins Model bzw. in Services – dort ist sie testbar und wiederverwendbar.
+
Kapitel 72

Domain-Driven Design

Domain-Driven Design (DDD) stellt die Fachlichkeit in den Mittelpunkt: Der Code spricht die Sprache der Domäne, und seine Struktur folgt dem Geschäft, nicht der Technik.

+ +

Allgegenwärtige Sprache

+

Klassen und Methoden heißen wie die Fachbegriffe. Ein Fachexperte würde den Code „lesen" können:

+
class Order {
+    public function place(): void {}
+    public function cancel(): void {}   // fachliche Verben, keine setStatus()
+}
+ +

Entitäten & Wertobjekte

+

Eine Entität hat eine Identität über die Zeit (eine Bestellung mit ID); ein Wertobjekt ist durch seinen Wert definiert (ein Geldbetrag, eine Adresse) und unveränderlich – siehe Teil 12.

+ +

Aggregate

+

Ein Aggregat ist eine Einheit aus Objekten mit einer Wurzel, die als einziger Zugang dient und die Regeln wahrt. Posten ändert man nur über die Bestellung, nie direkt:

+
class Order {              // Aggregat-Wurzel
+    private array $items = [];
+    public function addItem(Item $p): void {
+        if ($this->isCompleted()) throw new \DomainException("abgeschlossen");
+        $this->items[] = $p;   // Regel an einer Stelle erzwungen
+    }
+    private function isCompleted(): bool { return false; }
+}
+ +

Repository

+

Lädt und speichert Aggregate so, als wären sie eine Sammlung im Speicher – die Datenbank bleibt dahinter verborgen:

+
interface OrderRepository {
+    public function find(int $id): ?Bestellung;
+    public function save(Order $b): void;
+}
+ +
+
i
+
Für komplexe DomänenDDD entfaltet seinen Wert bei fachlich anspruchsvollen Systemen mit vielen Regeln. Für eine simple CRUD-Anwendung ist der volle Apparat überzogen. Der Kerngedanke lohnt aber fast immer: Lass den Code die Fachsprache sprechen und schiebe Technik an den Rand.
+
Kapitel 73

Event-getriebene Architektur

Statt dass Komponenten sich direkt aufrufen, melden sie Ereignisse, auf die andere reagieren. Das entkoppelt: Der Auslöser muss nicht wissen, wer zuhört.

+ +

Ereignis & Zuhörer

+

Ein Ereignis ist ein einfaches Objekt mit den Fakten; Zuhörer reagieren darauf:

+
final class OrderPlaced {
+    public function __construct(public readonly int $orderId) {}
+}
+interface Listener { public function __invoke(object $event): void; }
+ +

Ein einfacher Dispatcher

+

Eine zentrale Stelle nimmt Ereignisse an und ruft alle passenden Zuhörer:

+
class Dispatcher {
+    private array $listeners = [];
+    public function on(string $event, callable $z): void { $this->listeners[$event][] = $z; }
+    public function dispatch(object $e): void {
+        foreach ($this->listeners[$e::class] ?? [] as $z) $z($e);
+    }
+}
+$d = new Dispatcher();
+$d->on(OrderPlaced::class, fn($e) => print("Mail für #{$e->orderId}\n"));
+$d->on(OrderPlaced::class, fn($e) => print("Lager prüfen #{$e->orderId}\n"));
+$d->dispatch(new OrderPlaced(7));   // beide Zuhörer laufen
+ +

Synchron oder asynchron

+

Zuhörer können sofort laufen oder – über eine Queue (Teil 15) – später in einem Hintergrundprozess. So bleibt die Web-Antwort schnell, während Mails und Berichte nebenher entstehen.

+ +
+
+
Entkopplung mit AugenmaßEvents glänzen, wenn ein Vorgang mehrere unabhängige Folgen hat (Bestellung → Mail, Lager, Statistik). Sie erschweren aber das Nachvollziehen des Ablaufs – „wer reagiert eigentlich worauf?". Setze sie dort ein, wo die Entkopplung den Preis wert ist, und dokumentiere die Ereignisse gut.
+
Kapitel 74

Hexagonale Architektur

Die hexagonale Architektur (Ports & Adapters) stellt die Fachlogik in die Mitte und schiebt alle Technik (Web, Datenbank, APIs) an den Rand. Der Kern weiß nichts von der Außenwelt.

+ +

Ports

+

Der Kern definiert, was er braucht, als Interface (Port). Wie es erfüllt wird, ist ihm egal:

+
// im Kern – ein Port (was gebraucht wird)
+interface UserRepository {
+    public function find(int $id): ?array;
+}
+
+// Fachlogik nutzt nur den Port
+class UserService {
+    public function __construct(private UserRepository $repo) {}
+    public function name(int $id): string {
+        return $this->repo->find($id)["name"] ?? "unbekannt";
+    }
+}
+ +

Adapter

+

Ein Adapter erfüllt den Port mit konkreter Technik – hier eine DB-Variante und eine Test-Variante:

+
class PdoUserRepository implements UserRepository {
+    public function find(int $id): ?array { /* echte DB */ return ["name" => "Anna"]; }
+}
+class ArrayUserRepository implements UserRepository {
+    public function find(int $id): ?array { return ["name" => "Test"]; }   // für Tests
+}
+echo (new UserService(new PdoUserRepository()))->name(1);   // Anna
+echo (new UserService(new ArrayUserRepository()))->name(1);   // Test
+

Derselbe Kern läuft mit echter Datenbank im Betrieb und mit einer Attrappe im Test – ohne eine Zeile Fachlogik zu ändern.

+ +
+
i
+
Der Kern bleibt reinDie Leitidee aller dieser Architekturen ist dieselbe: Abhängigkeiten zeigen nach innen, zur Fachlogik – nie umgekehrt. Der Kern kennt nur Interfaces; Datenbank, Framework und HTTP sind austauschbare Details am Rand. Das macht die Logik langlebig und gründlich testbar.
+
Teil 14

Qualität & Performance

Was professionellen Code ausmacht: automatisierte Tests, statische Analyse, einheitlicher Stil, Debugging – und das Verständnis für Tempo und Speicher.
75 · Testen mit PHPUnit76 · Statische Analyse77 · Code-Style & Tooling78 · Debugging & Xdebug79 · Performance & OPcache80 · Speicher & Referenzen
Kapitel 75

Testen mit PHPUnit

Automatisierte Tests prüfen bei jeder Änderung, ob der Code noch tut, was er soll. PHPUnit ist der Standard – ein Test ist einfach Code, der deinen Code aufruft und das Ergebnis behauptet.

+ +

Ein einfacher Test

+
use PHPUnit\Framework\TestCase;
+
+class CalculatorTest extends TestCase {
+    public function testAdds(): void {
+        $r = new Calculator();
+        $this->assertSame(5, $r->add(2, 3));   // erwartet 5
+    }
+}
+

Ausgeführt mit vendor/bin/phpunit. Grün heißt: Behauptung erfüllt.

+ +

Wichtige Assertions

+
$this->assertSame(5, $value);          // gleich in Wert UND Typ (===)
+$this->assertEquals(5, $value);        // gleich im Wert (==)
+$this->assertTrue($condition);
+$this->assertCount(3, $list);
+$this->assertInstanceOf(Email::class, $obj);
+ +

Erwartete Ausnahmen

+
public function testThrowsOnInvalid(): void {
+    $this->expectException(\InvalidArgumentException::class);
+    new Email("kaputt");   // soll werfen
+}
+ +

Daten-Provider

+

Liefert mehrere Eingabe-/Erwartungs-Kombinationen, mit denen derselbe Test automatisch mehrfach läuft:

+
public static function cases(): array {
+    return [[2, 3, 5], [0, 0, 0], [-1, 1, 0]];
+}
+#[\PHPUnit\Framework\Attributes\DataProvider("cases")]
+public function testSum(int $a, int $b, int $expected): void {
+    $this->assertSame($expected, (new Calculator())->add($a, $b));
+}
+ +
+
+
Testbarer Code ist guter CodeLässt sich etwas schwer testen, liegt das meist am Design (versteckte Abhängigkeiten, zu viel in einer Methode). Genau hier zahlt sich Dependency Injection aus: Du reichst Attrappen statt echter Datenbanken hinein. Beginne mit Tests für die Fachlogik – dort steckt der Wert.
+
Kapitel 76

Statische Analyse

Statische Analyse prüft den Code, ohne ihn auszuführen – sie findet Tippfehler, falsche Typen und unmögliche Zustände, bevor sie zu Bugs werden. Die Werkzeuge: PHPStan und Psalm.

+ +

Was sie findet

+

Ein Aufruf mit falschem Typ, ein Zugriff auf eine vielleicht-null-Variable, eine Methode, die es nicht gibt – alles vor dem ersten Ausführen:

+
function length(?string $s): int {
+    return strlen($s);   // PHPStan: $s könnte null sein!
+}
+

Korrekt mit vorheriger Prüfung:

+
function length(?string $s): int {
+    return $s === null ? 0 : strlen($s);   // jetzt sauber
+}
+ +

Ausführen & Strenge-Stufen

+
# Analyse über das ganze Projekt
+vendor/bin/phpstan analyse src --level 9
+

Die Stufen reichen von 0 (locker) bis 9/max (sehr streng). Beginne niedrig, hebe schrittweise an.

+ +

DocBlocks für mehr Präzision

+

Wo PHPs Typsystem an Grenzen stößt (Array-Inhalte!), helfen Annotationen, die die Analyse versteht:

+
/** @param list<int> $numbers @return list<int> */
+function double(array $numbers): array {
+    return array_map(fn(int $n) => $n * 2, $numbers);
+}
+ +
+
+
In die CI einbauenStatische Analyse fängt eine ganze Fehlerklasse ab, die Tests übersehen, und kostet keine Laufzeit. Lass sie bei jedem Commit automatisch laufen (Continuous Integration). Zusammen mit declare(strict_types=1) und vollständigen Typdeklarationen ist sie der wirksamste Hebel für robusten Code.
+
Kapitel 77

Code-Style & Tooling

Einheitlicher Stil macht Code lesbar und Diffs klein. Statt im Team darüber zu streiten, lässt man Werkzeuge automatisch formatieren – nach dem Standard PSR-12.

+ +

Automatisch formatieren

+

PHP-CS-Fixer oder PHP_CodeSniffer bringen den ganzen Code auf einen einheitlichen Stil:

+
# prüfen, was nicht dem Standard entspricht
+vendor/bin/php-cs-fixer fix --dry-run --diff
+
+# automatisch korrigieren
+vendor/bin/php-cs-fixer fix
+ +

Was der Standard regelt

+

PSR-12 legt Dinge fest wie: vier Leerzeichen Einrückung, geschweifte Klammer der Methode in neuer Zeile, ein Leerzeichen nach Schlüsselwörtern. Vorher/nachher:

+
// vorher – uneinheitlich
+function x($a,$b){return $a+$b;}
+
+// nachher – PSR-12
+function x($a, $b)
+{
+    return $a + $b;
+}
+ +

Vor jedem Commit prüfen

+

Ein Git-Hook (z. B. über das Paket captainhook oder ein einfaches Skript) lässt Formatierung, Analyse und Tests automatisch laufen, bevor etwas eingecheckt wird – so kommt nichts Ungeprüftes ins Repository.

+ +
+
i
+
Nicht über Stil streitenWelcher Stil „besser" ist, ist meist Geschmack – wichtig ist nur, dass alle denselben nutzen. Lege die Konfiguration einmal ins Projekt und lass das Werkzeug entscheiden. Das spart Diskussionen und hält Code-Reviews bei der Sache statt bei Leerzeichen.
+
Kapitel 78

Debugging & Xdebug

Wenn etwas nicht stimmt, willst du sehen, was passiert. Xdebug ist die mächtigste Erweiterung dafür: Statt Werte zu erraten, hältst du das Programm an und schaust hinein.

+ +

Schnelle Diagnose ohne Tooling

+

Für einen schnellen Blick reichen die eingebauten Funktionen:

+
var_dump($value);          // Typ + Inhalt, ausführlich
+print_r($array, true);   // lesbare Struktur (true: als String)
+error_log(print_r($data, true));   // ins Log statt in die Ausgabe
+ +

Schrittweise mit dem Debugger

+

Mit Xdebug und der IDE setzt du einen Haltepunkt (Breakpoint) und läufst Zeile für Zeile durch – alle Variablen sind jederzeit einsehbar. Das ersetzt das mühsame Streuen von var_dump:

+
# Xdebug installieren und im php.ini aktivieren:
+# zend_extension=xdebug
+# xdebug.mode=debug
+# xdebug.start_with_request=yes
+

Danach in der IDE „Listen for debug connections" einschalten, Breakpoint setzen, Seite aufrufen – das Programm hält an.

+ +

Bessere Stacktraces

+

Schon im Modus develop macht Xdebug Fehlermeldungen und Stacktraces deutlich lesbarer und zeigt die Variablenwerte im Aufrufpfad – oft sieht man die Ursache sofort.

+ +
+
!
+
Xdebug nur in der EntwicklungAktives Xdebug bremst PHP erheblich – es gehört niemals auf einen Produktionsserver. Dort nutzt man stattdessen Logging und, für Performance-Analysen, leichtgewichtige Profiler. Vergiss außerdem keine var_dump-Reste im ausgelieferten Code (die statische Analyse warnt davor).
+
Kapitel 79

Performance & OPcache

PHP ist schneller, als viele denken – wenn ein paar Grundlagen stimmen. Die größten Hebel sind selten der PHP-Code selbst, sondern Caching und die Datenbank.

+ +

OPcache

+

Ohne OPcache übersetzt PHP jede Datei bei jeder Anfrage neu. Mit OPcache wird der kompilierte Bytecode im Speicher gehalten – ein gewaltiger Unterschied, ganz ohne Code-Änderung:

+
# php.ini (Produktion)
+# opcache.enable=1
+# opcache.validate_timestamps=0   ; im Betrieb: Dateien nicht ständig prüfen
+# opcache.memory_consumption=256
+

OPcache ist auf Produktionsservern Pflicht – es kann die Last vervielfachen.

+ +

Das N+1-Problem

+

Der häufigste Performance-Killer: in einer Schleife immer wieder die Datenbank fragen. Statt 1 + N Abfragen eine einzige mit JOIN oder IN:

+
// schlecht: pro Bestellung eine Extra-Abfrage
+foreach ($orders as $b) {
+    $user = $pdo->query("SELECT * FROM nutzer WHERE id = {$b['nutzer_id']}");   // N Abfragen!
+}
+// gut: alle auf einmal holen und im PHP zuordnen
+ +

Messen statt raten

+

Optimiere nie auf Verdacht. Miss zuerst, wo die Zeit hingeht – grob mit hrtime, gründlich mit einem Profiler:

+
$start = hrtime(true);
+// ... Code ...
+echo (hrtime(true) - $start) / 1_000_000 . " ms";
+ +
+
+
Die Reihenfolge der HebelOPcache aktivieren (riesig, gratis), N+1-Abfragen beseitigen, sinnvolle Datenbank-Indizes setzen (Teil 9), teure Ergebnisse cachen. Erst danach lohnt Mikro-Optimierung im PHP-Code – und auch die nur dort, wo der Profiler einen Engpass zeigt.
+
Kapitel 80

Speicher & Referenzen

Meist musst du dich um Speicher nicht kümmern – PHP räumt selbst auf. Bei großen Datenmengen und langlebigen Prozessen lohnt es aber zu verstehen, wie Kopien, Referenzen und die Speicherbereinigung arbeiten.

+ +

Copy-on-Write

+

PHP kopiert Werte nicht sofort, sondern erst bei der ersten Änderung. Eine Zuweisung ist also billig, solange beide gleich bleiben:

+
$a = range(1, 100000);
+$b = $a;          // noch keine echte Kopie – beide teilen die Daten
+$b[] = 1;          // jetzt wird kopiert (Copy-on-Write)
+ +

Referenzen sparsam einsetzen

+

Mit & teilen sich zwei Variablen denselben Speicher; eine Änderung wirkt bei beiden:

+
$x = 1;
+$y = &$x;
+$y = 9;
+echo $x;   // 9
+

Referenzen sind selten nötig und oft eine Fehlerquelle – nutze sie bewusst, etwa um ein großes Array in einer Funktion direkt zu verändern statt es zu kopieren.

+ +

Generatoren gegen Speicherspitzen

+

Der wirksamste Speichertrick bei großen Mengen: nicht alles laden, sondern strömen (vgl. Teil 6). So bleibt der Verbrauch konstant statt linear:

+
function lines(string $file): \Generator {
+    $f = fopen($file, "r");
+    while (($z = fgets($f)) !== false) yield $z;   // nur eine Zeile im Speicher
+    fclose($f);
+}
+ +

Verbrauch messen

+
echo memory_get_peak_usage(true) / 1024 / 1024 . " MB";   // Spitzenverbrauch
+ +
+
i
+
Zirkelbezüge & lange ProzesseBei normalen Web-Requests endet der Prozess schnell und gibt allen Speicher frei. In langlaufenden Workern (Teil 15) zählt jedes Leck: Achte auf Zirkelbezüge zwischen Objekten und gib große Strukturen mit unset frei. Ein Worker, der Speicher anhäuft, wird üblicherweise nach N Aufträgen kontrolliert neu gestartet.
+
Teil 15

CLI, Prozesse & asynchrones PHP

Jenseits des Webrequests: Kommandozeilen-Programme, externe Prozesse und die Welt der Fibers, Event-Loops und spezialisierten Laufzeiten.
81 · CLI-Programme bauen82 · Prozesse & FFI83 · Das klassische Modell & Fibers84 · Event-Loops & ReactPHP85 · Swoole, Amp & FrankenPHP86 · Echte Parallelität
Kapitel 81

CLI-Programme bauen

PHP läuft nicht nur im Web. Auf der Kommandozeile baust du Wartungsskripte, Cronjobs, Importer und Hintergrund-Worker – ganz ohne Webserver.

+ +

Argumente lesen

+

$argv enthält die übergebenen Argumente, $argc ihre Anzahl. Der erste Eintrag ist der Skriptname:

+
// aufruf: php import.php datei.csv --schnell
+echo $argv[1] ?? "keine Datei";   // datei.csv
+var_dump(in_array("--schnell", $argv, true));   // true
+

Für komfortable Optionen (--name=wert) gibt es getopt; größere CLI-Apps nutzen symfony/console.

+ +

Ein- und Ausgabe

+
fwrite(STDOUT, "Fortschritt...\n");
+fwrite(STDERR, "Warnung\n");      // Fehlerkanal, getrennt umleitbar
+$line = trim(fgets(STDIN));   // eine Eingabezeile lesen
+ +

Exit-Codes

+

Der Rückgabewert sagt der Shell, ob alles gut ging – 0 für Erfolg, alles andere für Fehler. Wichtig für Cron und CI:

+
if (!file_exists($argv[1] ?? "")) {
+    fwrite(STDERR, "Datei fehlt\n");
+    exit(1);   // Fehler signalisieren
+}
+exit(0);     // Erfolg
+ +
+
+
CLI hat kein ZeitlimitAnders als Web-Requests laufen CLI-Skripte ohne max_execution_time-Begrenzung – ideal für lange Importe. Trenne Ausgaben sauber in STDOUT (Ergebnis) und STDERR (Meldungen) und setze sinnvolle Exit-Codes, damit sich das Skript in Cron, Pipes und CI sauber einfügt.
+
Kapitel 82

Prozesse & FFI

Manchmal muss PHP andere Programme starten oder direkt mit C-Bibliotheken sprechen. Dafür gibt es Prozess-Funktionen und – für Fortgeschrittene – FFI.

+ +

Ein Programm aufrufen

+

shell_exec liefert die Ausgabe, exec zusätzlich Zeilen und Exit-Code:

+
echo shell_exec("date +%Y");   // z. B. 2026
+exec("ls -1", $lines, $code);
+echo $code;   // 0 bei Erfolg
+ +

Eingaben absichern

+

Baust du Argumente aus Variablen, drohen Command-Injection-Angriffe. Maskiere immer:

+
$file = escapeshellarg($userInput);   // sicher gequotet
+exec("wc -l " . $file, $out);
+ +

Volle Kontrolle mit proc_open

+

Für echte Interaktion (eigene STDIN/STDOUT/STDERR) öffnest du den Prozess mit Pipes:

+
$pipes = [];
+$p = proc_open("cat", [["pipe", "r"], ["pipe", "w"], ["pipe", "w"]], $pipes);
+fwrite($pipes[0], "hallo"); fclose($pipes[0]);
+echo stream_get_contents($pipes[1]);   // hallo
+proc_close($p);
+ +

FFI

+

Die Foreign Function Interface ruft Funktionen aus geteilten C-Bibliotheken auf, ohne eine PHP-Erweiterung zu schreiben:

+
$libc = \FFI::cdef("int strlen(const char *s);", "libc.so.6");
+echo $libc->strlen("hallo");   // 5 (aus der C-Bibliothek)
+ +
+
!
+
Mit VorsichtExterne Programme und FFI verlassen die sichere PHP-Welt. Bei Prozessaufrufen niemals ungeprüfte Eingaben in den Befehl einbauen (escapeshellarg/escapeshellcmd nutzen). FFI ist mächtig, aber fehleranfällig und umgeht PHPs Speichersicherheit – nur einsetzen, wenn es keine PHP-Lösung gibt.
+
Kapitel 83

Das klassische Modell & Fibers

Klassisches PHP ist synchron: Jede Anweisung wartet, bis die vorige fertig ist. Das ist einfach, aber bei vielem Warten (Netzwerk!) ineffizient. Fibers (seit PHP 8.1) bringen einen Ausweg.

+ +

Das blockierende Modell

+

Drei API-Aufrufe nacheinander dauern so lange wie ihre Summe – auch wenn jeder nur auf Antwort wartet:

+
$a = file_get_contents("https://api.example.com/1");   // warten
+$b = file_get_contents("https://api.example.com/2");   // warten
+// nacheinander – die Wartezeiten summieren sich
+ +

Was Fibers können

+

Eine Fiber ist eine Funktion, die sich selbst anhalten (suspend) und später fortsetzen kann. Damit lässt sich Warten unterbrechen und etwas anderes tun:

+
$fiber = new \Fiber(function(): void {
+    echo "Start\n";
+    $value = \Fiber::suspend("pause");   // hier anhalten, "pause" zurückgeben
+    echo "Weiter mit: $value\n";
+});
+echo $fiber->start();    // Start / liefert "pause"
+$fiber->resume("42");    // Weiter mit: 42
+ +

Wofür das gut ist

+

Fibers sind das Fundament, nicht das Werkzeug für den Alltag: Bibliotheken wie ReactPHP und Amp nutzen sie, um vielen wartenden Aufgaben einen Thread teilen zu lassen. Du selbst arbeitest meist mit deren komfortabler API (nächste Kapitel), nicht direkt mit Fiber.

+ +
+
i
+
Nebenläufig ≠ parallelFibers schalten kooperativ zwischen Aufgaben um, während eine wartet – auf einem einzigen Kern. Das beschleunigt I/O-lastige Arbeit (viele Netzwerkaufrufe), nicht rechenintensive (das wäre echte Parallelität, letztes Kapitel dieses Teils).
+
Kapitel 84

Event-Loops & ReactPHP

Asynchrones PHP dreht sich um die Event-Loop: eine Schleife, die viele Aufgaben verwaltet und immer dort weitermacht, wo gerade etwas fertig ist – statt untätig zu warten.

+ +

Die Idee der Event-Loop

+

Statt „mach A, warte, mach B, warte" registrierst du Aufgaben und Rückrufe. Die Loop arbeitet sie ab, sobald Daten bereit sind. Selbst ein Timer ist nur eine geplante Aufgabe:

+
use React\EventLoop\Loop;
+
+Loop::addTimer(1.0, fn() => print("nach 1s\n"));
+Loop::addTimer(0.5, fn() => print("nach 0.5s\n"));
+// Ausgabe: erst "nach 0.5s", dann "nach 1s" – nicht in Code-Reihenfolge
+ +

Versprechen (Promises)

+

Ein Promise steht für ein Ergebnis, das später kommt. Du hängst eine Reaktion an, statt zu warten:

+
use React\Http\Browser;
+
+$browser = new Browser();
+$browser->get("https://example.com")->then(
+    fn($response) => print("fertig: " . $response->getStatusCode() . "\n")
+);
+// das Programm läuft weiter, der Rückruf feuert, wenn die Antwort da ist
+ +

Vorteil bei vielem Warten

+

Mehrere Anfragen starten quasi gleichzeitig; die Gesamtdauer entspricht der langsamsten, nicht der Summe – ideal für APIs, die aufeinander warten.

+ +
+
+
Wann sich der Wechsel lohntAsynchrone Frameworks glänzen bei I/O-lastigen Diensten: viele gleichzeitige Verbindungen, WebSockets, das Bündeln vieler API-Aufrufe. Für klassische Web-Anwendungen mit etwas Logik und einer DB bleibt das synchrone PHP einfacher und völlig ausreichend.
+
Kapitel 85

Swoole, Amp & FrankenPHP

Drei Wege, PHP über das klassische „pro Request neu starten" hinaus laufen zu lassen – mit unterschiedlichen Stärken.

+ +

Swoole

+

Eine C-Erweiterung, die PHP zu einem im Speicher residenten, hochnebenläufigen Server macht. Der Code lädt einmal und bedient dann viele Anfragen ohne Neustart:

+
$server = new \Swoole\Http\Server("0.0.0.0", 9501);
+$server->on("request", function($req, $res) {
+    $res->end("Hallo von Swoole");
+});
+$server->start();   // läuft dauerhaft, sehr schnell
+ +

Amp

+

Amp bietet (wie ReactPHP) eine Event-Loop, nutzt aber Fibers, sodass async-Code aussieht wie synchroner – ohne Rückruf-Verschachtelung:

+
use function Amp\async;
+$results = Amp\Future\await([
+    async($fetchPage, "https://a.example"),
+    async($fetchPage, "https://b.example"),
+]);   // beide gleichzeitig, dann gemeinsam abwarten
+ +

FrankenPHP

+

Ein in Go geschriebener Server, der PHP einbettet. Er hält den Code im Speicher (Worker-Modus) und bringt HTTP/2, HTTP/3 und einfache Bereitstellung mit – oft ohne Code-Änderung deutlich schneller als das klassische Modell.

+ +
+
!
+
Speicher im Blick behaltenSobald PHP zwischen Anfragen im Speicher bleibt (Swoole, FrankenPHP-Worker), verschwindet der Zustand nicht mehr automatisch. Globale und statische Variablen leben weiter – ein Leck wächst über die Zeit. Solche Anwendungen müssen sauber mit Zustand umgehen und Worker periodisch neu starten.
+
Kapitel 86

Echte Parallelität

Nebenläufigkeit (Fibers, Event-Loops) hilft beim Warten. Für rechenintensive Arbeit, die mehrere CPU-Kerne nutzen soll, braucht es echte Parallelität – mehrere Ausführungspfade gleichzeitig.

+ +

Mehrere Prozesse

+

Der klassische Weg: das Programm in unabhängige Prozesse aufteilen. pcntl_fork (nur CLI, Unix) spaltet den laufenden Prozess:

+
$pid = pcntl_fork();
+if ($pid === 0) {
+    echo "Kindprozess rechnet\n";   // läuft parallel
+    exit(0);
+} else {
+    pcntl_wait($status);   // Elternprozess wartet auf das Kind
+}
+ +

Worker-Pools über eine Queue

+

In der Praxis verteilt man Arbeit selten von Hand auf Prozesse, sondern über eine Job-Queue: Web-Requests legen Aufgaben ab, mehrere Worker-Prozesse arbeiten sie parallel ab:

+
// vereinfachtes Bild:
+// Web:    queue.push(new BildVerkleinern($id));   // schnell zurück
+// Worker: while (true) { $job = queue.pop(); $job->ausfuehren(); }   // im Hintergrund
+

So bleibt die Web-Antwort schnell, und schwere Arbeit (Bilder, Mails, Berichte) läuft auf mehreren Kernen nebenher. Werkzeuge dafür: Symfony Messenger, Laravel Queues, Gearman.

+ +

Threads

+

Echte Threads in einem Prozess sind in PHP unüblich; das frühere pthreads ist überholt. Der Nachfolger parallel erlaubt Threads, ist aber eine Nische – für die meisten Aufgaben sind Prozesse und Queues der pragmatische Weg.

+ +
+
i
+
Das richtige WerkzeugViel Warten auf I/O? Nebenläufigkeit (Fibers, async). Viel Rechnen auf mehreren Kernen? Mehrere Prozesse, meist über eine Queue. Für die allermeisten Web-Anwendungen reicht jedoch: schnelle Antwort liefern, schwere Arbeit in einen Hintergrund-Worker auslagern – ganz ohne exotische Erweiterungen.
+
Teil 16

Das Ökosystem

PHP lebt von Composer, gemeinsamen Standards und großen Frameworks. Der Überblick über die Welt rund um deinen Code.
87 · Composer in der Tiefe88 · Die PSR-Standards89 · Symfony im Überblick90 · Laravel im Überblick91 · Wichtige Bibliotheken
Kapitel 87

Composer in der Tiefe

Composer hast du in Teil 3 kennengelernt. Hier geht es um die Feinheiten, die im Projektalltag zählen: Versionsregeln, Skripte und der Unterschied zwischen Entwicklung und Produktion.

+ +

Versionsregeln verstehen

+

Die Operatoren steuern, welche Updates erlaubt sind. ^ erlaubt alles bis zur nächsten Hauptversion, ~ nur die letzte genannte Stelle:

+
"monolog/monolog": "^3.5"    // >=3.5.0, <4.0.0
+"guzzlehttp/guzzle": "~7.8.0"  // >=7.8.0, <7.9.0
+"vlucas/phpdotenv": "5.*"    // jede 5er-Version
+ +

require vs. require-dev

+

Werkzeuge, die nur beim Entwickeln gebraucht werden (Tests, Analyse), gehören in require-dev – auf dem Produktionsserver werden sie weggelassen:

+
# nur für die Entwicklung
+composer require --dev phpunit/phpunit phpstan/phpstan
+
+# auf dem Server: dev-Pakete und optimierter Autoloader
+composer install --no-dev --optimize-autoloader
+ +

Skripte

+

In composer.json definierst du Kurzbefehle für wiederkehrende Aufgaben:

+
"scripts": {
+    "test":  "phpunit",
+    "check": ["phpstan analyse src", "php-cs-fixer fix --dry-run"]
+}
+# Aufruf:  composer test   /   composer check
+ +

Sicherheit & Aktualität prüfen

+
# bekannte Sicherheitslücken in Abhängigkeiten finden
+composer audit
+# welche Pakete sind veraltet?
+composer outdated --direct
+ +
+
+
Lock-Datei ist GesetzDie composer.lock gehört ins Repository und legt die exakten Versionen fest. composer install folgt ihr (identische Stände überall), composer update ändert sie bewusst. So baut der Server garantiert dasselbe wie deine Maschine.
+
Kapitel 88

Die PSR-Standards

PSR steht für „PHP Standard Recommendation" – Vereinbarungen der PHP-FIG, damit Code und Bibliotheken verschiedener Hersteller zusammenpassen. Sie sind der Grund, warum das Ökosystem so gut ineinandergreift.

+ +

PSR-4

+

Bildet Namespaces auf Verzeichnisse ab – die Basis dafür, dass Composer jede Klasse findet (Teil 3):

+
// App\Service\Mailer  =>  src/Service/Mailer.php
+"autoload": { "psr-4": { "App\\": "src/" } }
+ +

PSR-12

+

Der gemeinsame Formatierungsstandard (Teil 14). Werkzeuge wie PHP-CS-Fixer setzen ihn automatisch durch, sodass Code aus aller Welt gleich aussieht.

+ +

PSR-3

+

Ein einheitliches Logger-Interface. Dein Code hängt nur davon ab – welche Implementierung (Monolog o. a.) dahintersteckt, ist austauschbar:

+
use Psr\Log\LoggerInterface;
+class Import {
+    public function __construct(private LoggerInterface $log) {}
+    public function run(): void { $this->log->info("Import gestartet"); }
+}
+ +

PSR-7, -15, -17

+

Standardisierte Objekte für HTTP-Requests/Responses (PSR-7), Middleware (PSR-15) und deren Erzeugung (PSR-17). Dadurch lassen sich Middleware und Frameworks frei kombinieren:

+
interface MiddlewareInterface {
+    public function process(ServerRequestInterface $req, RequestHandlerInterface $next): ResponseInterface;
+}
+ +

PSR-11

+

Ein einheitliches Interface für DI-Container mit nur zwei Methoden – get() und has() – sodass Frameworks und Bibliotheken denselben Container nutzen können.

+ +
+
i
+
Warum das wichtig istDank PSR kannst du einen Logger, HTTP-Client oder Container austauschen, ohne deinen Code umzuschreiben – du hängst von Interfaces ab, nicht von Produkten. Das ist Dependency Inversion (Teil 12) auf Ökosystem-Ebene und ein Hauptgrund für die Reife der PHP-Welt.
+
Kapitel 89

Symfony im Überblick

Symfony ist zweierlei: eine Sammlung wiederverwendbarer Komponenten und ein darauf aufbauendes Full-Stack-Framework. Viele andere Projekte – auch Laravel – nutzen Symfony-Komponenten im Unterbau.

+ +

Komponenten einzeln nutzen

+

Du musst nicht das ganze Framework einsetzen – einzelne Komponenten gibt es als eigenständige Pakete:

+
# nur die HTTP-Foundation, ohne das ganze Framework
+composer require symfony/http-foundation
+
+use Symfony\Component\HttpFoundation\Request;
+$request = Request::createFromGlobals();
+echo $request->getMethod();   // GET
+ +

Typische Bausteine

+

Routing per Attribut, Controller als Klassen, Dependency Injection ab Werk, das Template-System Twig:

+
use Symfony\Component\Routing\Attribute\Route;
+
+class StartController {
+    #[Route("/", name: "start")]
+    public function index(): Response {
+        return new Response("Startseite");
+    }
+}
+ +

Versionen & Ausrichtung

+

Symfony erscheint zeitbasiert: alle sechs Monate eine Minor-Version, alle zwei Jahre eine Major. Aktuell ist Symfony 8.1 (Mai 2026, benötigt PHP 8.4+); für Projekte mit langem Planungshorizont gibt es die LTS-Reihe (derzeit 7.4 mit drei Jahren Support). Symfony gilt als besonders stark bei großen, langlebigen und Enterprise-Anwendungen.

+ +
+
i
+
KomponentendenkenDer größte Wert von Symfony liegt in seinen entkoppelten Komponenten – Console, HttpFoundation, Validator, Mailer, Yaml und viele mehr. Selbst wenn du ein anderes Framework nutzt, begegnest du ihnen ständig. Das Full-Stack-Framework setzt sie zu einem stimmigen Ganzen zusammen.
+
Kapitel 90

Laravel im Überblick

Laravel ist das meistgenutzte PHP-Framework und legt den Fokus auf Entwicklerfreude und schnelle Ergebnisse. Es bringt für fast jede Aufgabe eine elegante, fertige Lösung mit.

+ +

Eloquent

+

Laravels ORM macht Datenbankzugriff besonders knapp. Eine Klasse pro Tabelle, ausdrucksstarke Abfragen:

+
class Product extends Model {}
+
+$expensive = Product::where("cent", ">", 5000)->orderBy("name")->get();
+$p = Product::find(1);
+$p->name = "Buch";
+$p->save();
+ +

Artisan

+

Das Kommandozeilen-Werkzeug von Laravel – es erzeugt Gerüste, migriert die Datenbank und führt eigene Befehle aus:

+
# Gerüste erzeugen, Datenbank migrieren, eigene Befehle
+php artisan make:model Produkt -mc
+php artisan migrate
+php artisan tinker        # interaktive Konsole
+ +

Routing & Controller

+
use Illuminate\Support\Facades\Route;
+Route::get("/produkte/{id}", [ProductController::class, "zeigen"]);
+ +

Das Ökosystem drumherum

+

Laravel bietet ein dichtes Umfeld fertiger Pakete: Queues, Broadcasting/WebSockets (Reverb), Volltextsuche (Scout), Authentifizierung (Sanctum), Admin- und Monitoring-Werkzeuge. Aktuell ist Laravel 13 (2026, benötigt mindestens PHP 8.3). Es erscheint jährlich eine Major-Version mit dem erklärten Ziel möglichst weniger Brüche.

+ +
+
i
+
Laravel oder Symfony?Beide sind exzellent und teilen sich Unterbau. Faustregel: Laravel für schnelle Entwicklung, viel „Magie" und ein rundes Gesamtpaket; Symfony für maximale Kontrolle, Komponentendenken und große, langlebige Systeme. Die Wahl hängt mehr vom Team und Projekt ab als von technischer Überlegenheit.
+
Kapitel 91

Wichtige Bibliotheken

Bestimmte Pakete tauchen in fast jedem ernsthaften PHP-Projekt auf. Sie zu kennen, erspart dir, das Rad neu zu erfinden – hier die wichtigsten mit ihrem Zweck und einem kurzen Blick.

+ +

Monolog

+

Die verbreitete Logging-Bibliothek; sie schreibt Meldungen über austauschbare Handler an unterschiedliche Ziele:

+
use Monolog\Logger;
+use Monolog\Handler\StreamHandler;
+$log = new Logger("app");
+$log->pushHandler(new StreamHandler("app.log"));
+$log->warning("Speicher knapp");   // in app.log
+ +

Guzzle

+

Der Standard für ausgehende HTTP-Anfragen (Teil 10) – komfortabel und gut testbar.

+ +

Carbon

+

Erweitert DateTime um eine ausdrucksstarke, lesbare API:

+
use Carbon\Carbon;
+echo Carbon::now()->addDays(3)->diffForHumans();   // "in 3 Tagen"
+ +

Doctrine

+

Das große, datenbankunabhängige ORM (Teil 9) – Herzstück vieler Symfony-Projekte.

+ +

PHPUnit, PHPStan, Rector

+

Die Qualitäts-Werkzeuge aus Teil 14: Tests, statische Analyse – und Rector, das Code automatisch umbaut (etwa bei Upgrades, siehe nächster Teil).

+ +

Pakete finden & bewerten

+

Bibliotheken liegen auf packagist.org. Achte vor dem Einbinden auf: aktive Wartung (letzter Commit), Download-Zahlen, PHP-Versionsanforderung, offene Sicherheitsmeldungen und eine klare Lizenz.

+ +
+
+
Nicht selbst bauen, was es gibtLogging, HTTP, Datum, Validierung, UUID, Geld-Arithmetik – für all das gibt es ausgereifte, getestete Pakete. Eigenbau ist fast immer schlechter und teurer in der Pflege. Prüfe aber jede Abhängigkeit: Jedes Paket ist Code, für den du mitverantwortlich wirst (composer audit).
+
Teil 17

In Produktion

Code, der nur lokal läuft, hilft niemandem: Projektstruktur, Deployment, Beobachtbarkeit, Internationalisierung, Migration – und wie es weitergeht.
92 · Projektstruktur & Organisation93 · Deployment & Betrieb94 · Logging & Monitoring95 · Internationalisierung96 · Migration: von PHP 5 zu 8.597 · Weiterlernen & Community
Kapitel 92

Projektstruktur & Organisation

Eine klare Ordnerstruktur macht ein Projekt für jeden sofort verständlich. In der PHP-Welt hat sich ein Aufbau etabliert, an den sich Frameworks und Werkzeuge halten.

+ +

Der übliche Aufbau

+
projekt/
+├─ public/         # einziges web-erreichbares Verzeichnis (index.php)
+├─ src/            # dein Code (PSR-4, Namespace App\)
+├─ tests/          # die Tests
+├─ config/         # Konfiguration
+├─ vendor/         # Composer-Abhängigkeiten (nicht eingecheckt)
+├─ .env            # lokale Umgebungswerte (nicht eingecheckt)
+├─ composer.json
+└─ composer.lock
+ +

Nur public/ ist öffentlich

+

Der Webserver zeigt ausschließlich auf public/. So liegen Code, Konfiguration und .env außerhalb der Reichweite des Browsers – niemand kann sie abrufen:

+
# public/index.php – der einzige Einstiegspunkt
+require __DIR__ . "/../vendor/autoload.php";
+// ... Anwendung starten ...
+ +

Konfiguration über Umgebung

+

Zugangsdaten und umgebungsabhängige Werte gehören nicht in den Code, sondern in Umgebungsvariablen (12-Factor-Prinzip). Lokal über eine .env-Datei, die nie ins Repository kommt:

+
# .env (nur lokal, in .gitignore)
+# DB_DSN=mysql:host=localhost;dbname=shop
+$dsn = getenv("DB_DSN");   // liest die Umgebungsvariable
+ +
+
!
+
Geheimnisse niemals eincheckenPasswörter, API-Schlüssel und .env-Dateien gehören in die .gitignore, nie ins Repository. Versehentlich committete Schlüssel gelten als kompromittiert und müssen ausgetauscht werden. Eine .env.example ohne echte Werte dokumentiert, welche Variablen nötig sind.
+
Kapitel 93

Deployment & Betrieb

Deployment bringt deinen Code auf den Server – idealerweise vorhersehbar, wiederholbar und ohne Ausfall. Ein paar Bausteine machen aus „Dateien hochladen" einen verlässlichen Prozess.

+ +

Die Schritte eines Deployments

+
# auf dem Server, in dieser Reihenfolge:
+git pull
+composer install --no-dev --optimize-autoloader   # nur Produktionspakete
+php bin/migrate                                    # DB-Schema aktualisieren
+# Caches aufwärmen, OPcache zurücksetzen
+ +

Atomares Deployment

+

Statt im laufenden Verzeichnis zu arbeiten, baust du eine neue Version daneben auf und schaltest am Ende per Symlink um – so ist der Wechsel augenblicklich und rückkehrbar:

+
releases/2026-05-31-1430/   # neue Version, fertig gebaut
+current -> releases/2026-05-31-1430   # Symlink zeigt auf aktuell
+# Rollback = Symlink auf die vorige Release zurücksetzen
+

Werkzeuge wie Deployer oder die Pipelines deiner CI automatisieren genau das.

+ +

Produktionseinstellungen

+

Auf dem Server gelten andere Regeln als lokal – Fehler werden geloggt, nicht angezeigt, und OPcache läuft:

+
# php.ini (Produktion)
+# display_errors = Off        ; keine internen Details an Nutzer
+# log_errors = On
+# opcache.enable = 1
+# opcache.validate_timestamps = 0   ; max. Tempo
+ +
+
+
Reproduzierbar statt manuellManuelles Hochladen per FTP ist fehleranfällig. Ziel ist ein Deployment, das per Knopfdruck immer dieselben Schritte ausführt, sich bei Fehlern sauber zurückrollen lässt und keine Downtime erzeugt. Halte App und Konfiguration getrennt (Umgebungsvariablen), dann läuft derselbe Code in Test und Produktion.
+
Kapitel 94

Logging & Monitoring

In Produktion siehst du nicht zu, wie der Code läuft – du musst es ablesen können. Logging schreibt mit, was passiert; Monitoring schlägt Alarm, wenn etwas aus dem Ruder läuft.

+ +

Sinnvoll loggen mit Stufen

+

Nicht alles ist gleich wichtig. Die PSR-3-Stufen (debug, info, warning, error …) trennen Rauschen von echten Problemen:

+
$log->info("Bestellung aufgegeben", ["id" => 42]);
+$log->warning("Zahlung verzögert", ["id" => 42]);
+$log->error("Zahlung fehlgeschlagen", ["id" => 42, "grund" => $e->getMessage()]);
+ +

Strukturiert loggen

+

Logs als JSON statt Fließtext lassen sich maschinell durchsuchen und auswerten – wichtig, sobald mehrere Server schreiben:

+
// {"level":"error","msg":"Zahlung fehlgeschlagen","id":42,"zeit":"2026-05-31T14:30:00Z"}
+// Der Kontext (zweites Argument) landet als Felder im JSON.
+ +

Fehler zentral erfassen

+

Ein Dienst wie Sentry sammelt unbehandelte Ausnahmen samt Stacktrace und Kontext an einer Stelle – du erfährst von Fehlern, bevor Nutzer sie melden. Angebunden über den zentralen Exception-Handler (Teil 12).

+ +

Gesundheit prüfbar machen

+

Ein schlanker Endpunkt erlaubt automatische Überwachung – ist die App erreichbar, die DB verbunden?

+
// GET /health
+try {
+    $pdo->query("SELECT 1");
+    echo json_encode(["status" => "ok"]);
+} catch (\Throwable) {
+    http_response_code(503);
+    echo json_encode(["status" => "db-fehler"]);
+}
+ +
+
!
+
Keine Geheimnisse in LogsLogge niemals Passwörter, Tokens, Kreditkarten oder vollständige personenbezogene Daten – Logs werden weitergeleitet, archiviert und von vielen gelesen. Logge IDs und Kontext, nicht Inhalte. Beachte dabei auch Datenschutz (DSGVO): so wenig personenbezogen wie möglich.
+
Kapitel 95

Internationalisierung

Sobald eine Anwendung mehrere Sprachen oder Regionen bedient, reicht fest verdrahteter Text nicht mehr. Internationalisierung (i18n) trennt Inhalte und Formate von der Sprache des Nutzers.

+ +

Texte übersetzbar machen

+

Statt Texte im Code zu verteilen, hältst du sie in Übersetzungsdateien und referenzierst Schlüssel:

+
// de.php:  return ["gruss" => "Hallo, :name"];
+// en.php:  return ["gruss" => "Hello, :name"];
+$texts = require "lang/$sprache.php";
+echo strtr($texts["gruss"], [":name" => "Anna"]);   // Hallo, Anna
+ +

Zahlen, Währung & Datum lokal formatieren

+

Die intl-Erweiterung formatiert korrekt für jede Region – Tausendertrenner, Währungssymbol, Datumsreihenfolge:

+
$f = new \NumberFormatter("de_DE", \NumberFormatter::CURRENCY);
+echo $f->formatCurrency(1234.5, "EUR");   // 1.234,50 €
+
+$us = new \NumberFormatter("en_US", \NumberFormatter::CURRENCY);
+echo $us->formatCurrency(1234.5, "USD");   // $1,234.50
+ +

Pluralregeln

+

Sprachen pluralisieren unterschiedlich. Der MessageFormatter wählt die richtige Form:

+
$m = "{n, plural, =0{keine Nachrichten} one{eine Nachricht} other{# Nachrichten}}";
+echo \MessageFormatter::formatMessage("de_DE", $m, ["n" => 3]);   // 3 Nachrichten
+ +
+
+
Von Anfang an trennenEs ist viel teurer, Texte nachträglich aus dem Code zu lösen, als sie gleich auszulagern. Halte Datum, Zahl und Währung konsequent über intl formatiert und speichere intern neutral (UTC, Cent, Roh-Werte) – formatiert wird erst bei der Ausgabe, passend zur Region des Nutzers.
+
Kapitel 96

Migration: von PHP 5 zu 8.5

Viele Bestandssysteme laufen noch auf alten PHP-Versionen. Der Sprung auf 8.5 bringt enorme Vorteile bei Tempo, Sicherheit und Sprache – mit der richtigen Strategie ist er gut beherrschbar.

+ +

Warum überhaupt migrieren

+

Alte Versionen bekommen keine Sicherheitsupdates mehr und sind damit ein Risiko. Dazu kommt: PHP 8 ist um ein Vielfaches schneller als PHP 5 und bringt Typen, Enums und vieles mehr.

+ +

Schrittweise statt in einem Sprung

+

Gehe Version für Version (5.6 → 7.x → 8.x) und teste nach jeder Stufe. Die häufigsten Brüche: entfernte Funktionen, strengere Typprüfung, geändertes Standardverhalten.

+
// typischer Bruch: früher Warnung, heute TypeError
+function x(array $a) {}
+x("kein array");   // PHP 8: TypeError statt stiller Umwandlung
+ +

Werkzeuge nehmen Arbeit ab

+

Rector baut Code automatisch auf eine neue PHP-Version um; PHPStan/Psalm finden vorab, was brechen wird:

+
# Vorschau, was Rector ändern würde
+vendor/bin/rector process src --dry-run
+ +

Absichern durch Tests

+

Vor einer Migration ist eine Testabdeckung Gold wert: Sie zeigt nach jedem Schritt, ob noch alles funktioniert. Fehlen Tests, schreibe wenigstens für die kritischsten Pfade welche, bevor du beginnst.

+ +
+
+
Erst sichtbar machen, dann ändernAktiviere Fehlerberichte für Deprecations, lass statische Analyse und Rector laufen, migriere in kleinen Schritten und teste nach jedem. So wird aus einem gefürchteten Großprojekt eine Reihe überschaubarer, jeweils überprüfbarer Änderungen.
+
Kapitel 97

Weiterlernen & Community

Du hast den Bogen von der ersten Zeile bis in Architektur, Async und Betrieb gespannt. Sprachen und Ökosysteme entwickeln sich weiter – hier, wie du am Ball bleibst und wo es weitergeht.

+ +

Die verlässlichen Quellen

+

Das offizielle Handbuch auf php.net ist erstaunlich gut und immer die erste Adresse für Funktionen und Verhalten. Neuerungen entstehen transparent als RFCs im PHP-Wiki – wer sie verfolgt, sieht die Zukunft der Sprache.

+ +

Qualität im Ökosystem

+

Pakete findest und bewertest du auf packagist.org; die Standards der PHP-FIG (PSR) zeigen, wie sauberer, kompatibler Code aussieht. Die Dokumentationen von Symfony und Laravel sind selbst ohne deren Nutzung lehrreich.

+ +

Dranbleiben

+

PHP bekommt jährlich im November eine neue Version. Lies die Migrationsleitfäden, probiere Neues in einem kleinen Projekt aus und halte Abhängigkeiten regelmäßig aktuell (composer outdated, composer audit). Konferenzen und lokale User-Groups verbinden mit Menschen, die vor denselben Fragen stehen.

+ +

Wie es weitergeht

+

Der nächste Schritt ist kein weiteres Kapitel, sondern ein eigenes Projekt. Bau etwas, das dich interessiert – klein anfangen, die Prinzipien aus diesem Guide anwenden, an echten Problemen wachsen. Genau so wird aus Wissen Können.

+ +
+
+
Das eine PrinzipWenn von diesem Guide eine Sache bleiben soll: Schreib Code für den nächsten Menschen, der ihn liest – oft bist das du selbst in einem halben Jahr. Klarheit schlägt Cleverness, Lesbarkeit schlägt Kürze, und ein verständliches Programm ist mehr wert als ein raffiniertes. Viel Freude beim Bauen.
+
+``` \ No newline at end of file