```
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-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.
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
Jede Anweisung endet mit einem Semikolon; Zeilenumbrüche sind für PHP bedeutungslos:
$a = 5; $b = 10; echo $a + $b; // 15
// einzeilig # ebenfalls einzeilig /* mehrzeilig – über mehrere Zeilen */
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
php -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.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.
Ein $, dann der Name. Zuweisen mit =:
$name = "Anna"; $age = 30; echo "$name ist $age"; // Anna ist 30
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
Zahlen mit Nachkommastellen (Fließkommazahlen):
$pi = 3.14; var_dump($pi); // float(3.14)
Zeichenketten, also beliebiger Text:
$s = "Text"; var_dump($s); // string(4) "Text"
Ein Wahrheitswert mit nur zwei möglichen Inhalten, true oder false:
$ok = true; var_dump($ok); // bool(true)
Steht für „kein Wert" – eine Variable, die bewusst nichts enthält:
$empty = null; var_dump($empty); // NULL
Liste oder Schlüssel-Wert-Sammlung; ein eigenes Kapitel widmet sich ihr vollständig:
$list = [1, 2, 3]; var_dump(count($list)); // int(3)
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.
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)
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
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
$name ≠ $Name) und beginnen mit Buchstabe oder Unterstrich. Wähle sprechende Namen ($gesamtPreis statt $gp) – Code wird viel öfter gelesen als geschrieben.Operatoren verknüpfen Werte zu neuen Werten – für jede Berechnung, jeden Vergleich, jede Bit-Manipulation. Hier vollständig, jeder mit Beispiel.
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)
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
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
== 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)
$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
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)
echo "Hallo" . " " . "Welt"; // Hallo Welt (verketten) echo $age >= 18 ? "erwachsen" : "minderjährig"; // ternär echo $name ?? "Gast"; // erster nicht-null Wert
== 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 ||.Verzweigungen entscheiden, welcher Code unter welchen Umständen läuft. PHP bietet von der klassischen if-Kette bis zum modernen match mehrere Werkzeuge.
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"; }
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"
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
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 meist die bessere Wahl: typsicher, ausdrucksstark und ohne break. Den klassischen switch – und wann er noch sinnvoll ist – behandelt das nächste Kapitel.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.
Wiederholt, solange die Bedingung wahr ist – sie wird vor jedem Durchlauf geprüft:
$i = 1; while ($i <= 3) { echo $i; // 1, 2, 3 $i++; }
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
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 }
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 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 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.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.
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.
Mehrere Fälle teilen sich einen Block, indem das break weggelassen wird:
switch ($tag) { case "Sa": case "So": echo "Wochenende"; break; default: echo "Werktag"; }
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.
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";
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.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.
function greet($name) { echo "Hallo, $name!\n"; } greet("Anna"); // Hallo, Anna!
return beendet die Funktion und liefert einen Wert zurück:
function add($a, $b) { return $a + $b; } echo add(3, 5); // 8
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
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
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
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
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
Kurzschreibweise für kleine Funktionen; sie übernehmen den Geltungsbereich automatisch:
$double = fn($n) => $n * 2; echo $double(21); // 42
Text ist allgegenwärtig. PHP bietet dutzende String-Funktionen – hier die wichtigsten, jede einzeln erklärt und sofort gezeigt.
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
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;
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)
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)
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
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
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)
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)
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"]
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); // =-=-=-
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_*-Varianten. Die Byte-Funktionen zerschneiden sonst Mehrbyte-Zeichen. Setze früh mb_internal_encoding("UTF-8").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.
$colors = ["rot", "grün", "blau"]; echo $colors[0]; // rot $person = ["name" => "Anna", "alter" => 30]; echo $person["name"]; // Anna
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
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
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 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"]
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.
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
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]
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
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.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.
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
\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)
* = 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)
^ 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)
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)
. + * ? ( ) [ ] 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.Mit den Grundlagen im Rücken geht es ans Anwenden: Treffer auslesen, ersetzen, aufteilen – und die typischen Fallstricke vermeiden.
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
(?<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
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"]
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"
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"
Zerlegt einen String dort, wo ein Muster passt:
print_r(preg_split("/[\s,]+/", "a, b, c")); // ["a", "b", "c"]
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)
.* 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.Sobald Programme wachsen, teilst du sie auf mehrere Dateien auf. include und require binden eine andere PHP-Datei an Ort und Stelle ein.
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.
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
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
require brauchst du dann fast nur noch einmal für Composers vendor/autoload.php und für Konfigurationsdateien.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.
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; } }
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");
Kollidieren zwei importierte Namen, benennst du einen per as um:
use App\Service\Logger as AppLogger; use Monolog\Logger as MonoLogger;
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 \)
App\Service\Logger liegt in src/Service/Logger.php. Hältst du dich daran, findet Composers Autoloader jede Klasse automatisch – ohne ein einziges require.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.
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
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
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.
Das ^ erlaubt verträgliche Updates (gleiche Hauptversion):
"require": { "monolog/monolog": "^3.0" // 3.x, aber nicht 4.0 }
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.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.
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 }
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 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
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 }
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.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.
function repeat(string $text, int $times): string { return str_repeat($text, $times); } echo repeat("ab", 3); // ababab
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
class Account { public int $balance = 0; public string $owner; } $k = new Account(); $k->balance = 100; // ok // $k->balance = "viel"; // TypeError
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.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.
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
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 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 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
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.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.
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.
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
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 }
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.PHP 8 hat die Sprache spürbar moderner gemacht. Diese Schmankerl begegnen dir in jedem aktuellen Projekt – hier jedes mit Beispiel.
?-> 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
Argumente beim Namen nennen – die Reihenfolge wird egal, optionale gezielt setzbar:
array_slice(array: [1, 2, 3, 4], offset: 1, length: 2); // [2, 3]
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
[$a, $b] = [1, 2]; echo $a; // 1 ["name" => $name] = ["name" => "Anna", "alter" => 30]; echo $name; // Anna
(...) macht aus einer Funktion einen Wert, den man weiterreichen kann:
$fn = strtoupper(...); print_r(array_map($fn, ["a", "b"])); // ["A", "B"]
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
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
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.
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.
$dog = new Dog(); $dog->name = "Bello"; // Eigenschaft setzen echo $dog->bark(); // Bello sagt Wuff
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
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
clone – mehr dazu bei den magischen Methoden.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 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.
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
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
Der Konstruktor __construct richtet ein Objekt bei der Erzeugung ein. PHP 8 macht ihn besonders knapp – und mit readonly sogar unveränderlich.
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
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
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
$price = new Money(currency: "USD", cent: 500); echo $price->currency; // USD
readonly bleibt es das garantiert.Vererbung lässt eine Klasse Eigenschaften und Methoden einer anderen übernehmen und erweitern. Sie modelliert eine „ist ein"-Beziehung – ein Kreis ist eine Form.
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.
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
Im Beispiel oben ist $name protected und damit in Dog und Cat nutzbar, von außen aber verborgen.
final verhindert weiteres Überschreiben bzw. Erben:
final class UUID {} // kann nicht erweitert werden class Base { final public function id(): int { return 1; } // nicht überschreibbar }
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.
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
Eine Klasse kann beliebig viele Interfaces erfüllen:
interface Printable { public function output(): void; } class Receipt implements Shape, Printable { /* beide Verträge erfüllen */ }
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 €
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.
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.
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
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 } }
$this. Für echtes Teilen von Verhalten ist Komposition (ein eingebautes Hilfsobjekt) oft klarer. Nutze Traits gezielt für kleine, eigenständige Fähigkeiten.Statische Member gehören zur Klasse statt zu einem Objekt. Konstanten halten unveränderliche Werte. Beide erreichst du ohne Instanz – über den Klassennamen.
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;.
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 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)
Liefert den voll qualifizierten Klassennamen als String – tippsicher:
echo Circle::class; // Kreis (bzw. mit Namespace)
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.
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)
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 €
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
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)
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
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).
__get/__call nur, wo dynamisches Verhalten der eigentliche Zweck ist.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.
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.
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
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 = [];
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.
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
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
function pairs(): \Generator { yield "a" => 1; yield "b" => 2; } foreach (pairs() as $k => $v) echo "$k=$v "; // a=1 b=2
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); }
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
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".
$greet = function(string $name): string { return "Hallo, $name"; }; echo $greet("Anna"); // Hallo, Anna
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
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
Closures sind das, was viele Array-Funktionen erwarten:
print_r(array_map(fn($n) => $n ** 2, [1, 2, 3])); // [1, 4, 9]
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
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.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.
#[\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 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.
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 }
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.
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 }
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 }
Reflection kann Instanzen mit dynamischen Argumenten bauen – die Grundlage automatischer Verdrahtung:
$obj = (new \ReflectionClass(User::class)) ->newInstanceArgs(["Anna", 30]); echo $obj->greet(); // Hi Anna
Für Tests und Serialisierung lässt sich (mit Bedacht) auch Verborgenes lesen:
$prop = new \ReflectionProperty(User::class, "alter"); echo $prop->getValue($obj); // 30
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); }
PHP bringt für jede Rechenaufgabe eine Funktion mit – und für Geldbeträge einen wichtigen Sonderweg. Hier die Grundfunktionen, jede mit Beispiel.
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
echo pow(2, 10); // 1024 echo 2 ** 10; // 1024 (gleichbedeutend) echo sqrt(144); // 12
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.
echo number_format(1234567.5, 2, ",", "."); // 1.234.567,50
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
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.Zeit ist tückisch: Zeitzonen, Sommerzeit, Schaltjahre. PHP nimmt dir das mit den DateTime-Klassen ab – arbeite mit Objekten statt mit rohen Zeitstempeln.
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.
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)
$d = \DateTimeImmutable::createFromFormat("d.m.Y", "24.12.2026"); echo $d->format("Y-m-d"); // 2026-12-24
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
var_dump($a < $b); // true – Datumsobjekte sind direkt vergleichbar $deadline = $a->add(new \DateInterval("P30D")); // + 30 Tage
$tz = new \DateTimeZone("Europe/Berlin"); $d = new \DateTimeImmutable("now", $tz); echo $d->format("H:i e"); // Uhrzeit samt Zone
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.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.
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
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);
var_dump(file_exists("notiz.txt")); // true var_dump(is_dir("/tmp")); // true echo filesize("notiz.txt"); // Größe in Bytes
$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
mkdir("daten/cache", 0755, true); // rekursiv anlegen print_r(glob("daten/*.txt")); // alle .txt per Muster unlink("notiz.txt"); // Datei löschen
fopen & 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.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.
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
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
fwrite(STDOUT, "normale Ausgabe\n"); fwrite(STDERR, "Fehler-Ausgabe\n"); $input = fgets(STDIN); // liest eine Zeile von Tastatur/Pipe
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
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);
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
php://memory füttern statt mit echten Dateien. Für ernsthafte HTTP-Aufrufe nimmt man aber besser einen echten Client wie Guzzle (Web-Teil).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.
Zuletzt Hineingelegtes kommt zuerst heraus:
$s = new \SplStack(); $s->push("a"); $s->push("b"); echo $s->pop(); // b (zuletzt rein, zuerst raus)
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)
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)
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
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
SplStack sagt „hier wird gestapelt", SplObjectStorage verwaltet Objektmengen sauber, und SplPriorityQueue erspart eigenes Sortieren.JSON ist das Standardformat für APIs und Konfiguration. PHP wandelt mit zwei Funktionen zwischen PHP-Werten und JSON hin und her.
$data = ["name" => "Anna", "aktiv" => true, "tags" => ["a", "b"]]; echo json_encode($data); // {"name":"Anna","aktiv":true,"tags":["a","b"]}
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)
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"}
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"; }
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.XML ist sperriger als JSON, aber in Altsystemen, Konfigurationen und Standards (SOAP, RSS) verbreitet. PHP bietet drei Wege – je nach Größe und Aufgabe.
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
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
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 } }
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.
Ü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);
$f = fopen("export.csv", "r"); while (($line = fgetcsv($f)) !== false) { print_r($line); // ["Anna", "Köln; Zentrum"] } fclose($f);
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 }
print_r(str_getcsv('a,"b,c",d')); // ["a", "b,c", "d"]
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.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.
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
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"}
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 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.Neben JSON, XML und CSV begegnen dir im Alltag weitere kleine Formate – für Konfiguration, lesbare Datenstrukturen und binärsichere Kodierung.
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
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.
echo urlencode("a b&c"); // a+b%26c echo http_build_query(["q" => "php 8", "seite" => 2]); // q=php+8&seite=2
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
.php-Datei mit return [...] die einfachste Wahl – sie wird vom OPcache mitgecacht.PDO (PHP Data Objects) ist die einheitliche, sichere Schnittstelle zu Datenbanken. Derselbe Code spricht – bis auf den Verbindungsstring – MySQL, PostgreSQL, SQLite und mehr.
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 ] );
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)");
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
PDOException, die du fangen kannst. Setze außerdem charset=utf8mb4, sonst gehen Emojis und manche Zeichen verloren.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.
// GEFÄHRLICH – niemals so: $pdo->query("SELECT * FROM nutzer WHERE name = '" . $_GET["name"] . "'"); // Eingabe ' OR '1'='1 hebelt die Bedingung aus
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();
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
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]); }
ORDER BY-Richtungen. Muss so etwas dynamisch sein, prüfe es gegen eine feste Erlaubnisliste (Allowlist) – niemals direkt aus Benutzereingaben übernehmen.Nach einer Abfrage holst du die Zeilen ab. Der Fetch-Modus bestimmt die Form – Array, Objekt, einzelne Spalte oder direkt in eine Klasse.
$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
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"]
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
Ein Statement ist direkt durchlaufbar – speicherschonend, da Zeile für Zeile:
foreach ($pdo->query("SELECT name FROM nutzer") as $line) { echo $line["name"]; }
fetchAll 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.Eine Transaktion bündelt mehrere Änderungen zu einer Einheit: Entweder alle gelingen, oder keine. Das hält die Daten konsistent, wenn mittendrin etwas schiefgeht.
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.
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.
beginTransaction im try, commit am Ende, rollBack im catch. Wirf den Fehler danach weiter, damit der aufrufende Code von der gescheiterten Operation erfährt.Wie du Daten in Tabellen organisierst, entscheidet über Wartbarkeit und Geschwindigkeit. Ein paar Grundregeln ersparen später viel Schmerz.
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 );
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) );
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.
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);
WHERE- und JOIN-Bedingungen, nicht blind alles. Primär- und Fremdschlüssel sind meist automatisch oder sinnvollerweise indiziert.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.
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; }
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;
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]);
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 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
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).
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", };
Alle Daten einer HTTP-Anfrage stellt PHP in vordefinierten „Superglobals" bereit – Arrays, die überall verfügbar sind.
$_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.
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
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);
$_COOKIE (gesendete Cookies), $_FILES (Uploads), $_SESSION (Sitzungsdaten) und $_REQUEST (GET+POST gemischt – besser meiden) – jedes bekommt sein eigenes Kapitel bzw. wird gleich behandelt.
$_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.Formulare sind der häufigste Weg, Daten vom Nutzer zu bekommen. Der Ablauf: anzeigen, abschicken, auf dem Server prüfen, reagieren.
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 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); } }
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 ?? "") ?>">
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;
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).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.
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
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], ]);
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";
htmlspecialchars für HTML, Prepared Statements für SQL). Beides ist nötig – das eine ersetzt das andere nicht.Eine Session hält Daten über mehrere Anfragen desselben Nutzers hinweg – die Antwort auf PHPs Zustandslosigkeit. Typischer Einsatz: „wer ist angemeldet?".
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.
session_start(); if (passwordMatches($_POST["pw"])) { session_regenerate_id(true); // neue ID gegen Session-Fixation $_SESSION["nutzer_id"] = $id; }
session_start(); $_SESSION = []; // Daten leeren session_destroy(); // Session serverseitig beenden
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.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".
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 ]);
Gesendete Cookies stehen in $_COOKIE – verfügbar ab der nächsten Anfrage, nicht in derselben:
echo $_COOKIE["theme"] ?? "hell"; // dunkel
Ein Cookie löscht man, indem man es mit einem Ablaufdatum in der Vergangenheit überschreibt:
setcookie("theme", "", ["expires" => time() - 3600, "path" => "/"]);
httponly, secure und samesite immer; speichere Sensibles in der serverseitigen Session.Hochgeladene Dateien landen in $_FILES. Hier ist besondere Vorsicht nötig: Nichts an einer hochgeladenen Datei darf man dem Client glauben.
<form method="post" enctype="multipart/form-data"> <input type="file" name="bild"> </form>
Ohne enctype="multipart/form-data" kommt keine Datei an.
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);
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.
$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
$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);
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);
Sicherheit ist keine Funktion, die man am Ende anschraubt, sondern eine Haltung beim Schreiben jeder Zeile. Das Kernprinzip: Traue keiner Eingabe – egal woher.
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)
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.
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.
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.
// 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.
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
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
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.
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 <, 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.
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"); }
htmlspecialchars 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.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.
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$...
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 }
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 }
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).Eine Session ist nur so sicher wie ihr Cookie und ihr Lebenszyklus. Ein paar Einstellungen und Gewohnheiten verhindern die häufigsten Übernahmen.
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();
Nach Anmeldung oder Rechteänderung eine frische ID vergeben – gegen Session-Fixation:
session_regenerate_id(true); // alte ID verfällt
$_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();
secure erzwingt die Übertragung nur über verschlüsselte Verbindungen – im Web heute Pflicht, nicht Kür.Wenn du Daten verschlüsseln oder signieren musst, baue nichts selbst. PHP hat mit der Sodium-Bibliothek moderne, sichere Bausteine direkt eingebaut.
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
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.
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 }
password_hash für Passwörter, random_bytes/random_int für Zufall. Verwechsle Kodierung (Base64) nicht mit Verschlüsselung – Base64 schützt nichts.SOLID ist eine Eselsbrücke für fünf Prinzipien, die Code wandelbar und testbar halten. Jedes mit einer knappen Idee und einem Beispiel.
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
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; } }
Ein Untertyp muss überall dort funktionieren, wo der Basistyp erwartet wird – ohne Überraschungen. Wer Shipping erwartet, darf Express bekommen und sich auf cost() verlassen.
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
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).
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.
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 } }
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.
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
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.
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.
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
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.
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.Wie eine Anwendung mit Fehlern umgeht, ist eine Designentscheidung – kein Detail. Eine durchdachte Strategie macht den Unterschied zwischen einer robusten App und einem Kartenhaus.
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; }
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"; }
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 });
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.
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
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
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
Erzeugungsmuster (Creational Patterns) regeln, wie Objekte entstehen – getrennt von ihrer Verwendung. So bleibt der Code flexibel, wenn sich die Erzeugung ändert.
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}
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
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".
Strukturmuster (Structural Patterns) setzen Klassen und Objekte zu größeren Strukturen zusammen – etwa um Schnittstellen anzupassen oder Verhalten zu ergänzen.
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
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
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 }
Verhaltensmuster (Behavioral Patterns) regeln, wie Objekte zusammenarbeiten und Verantwortung verteilen – wer wen wann aufruft.
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]
Mehrere Zuhörer reagieren auf ein Ereignis – die Basis von Event-Systemen (eigenes Architektur-Kapitel folgt).
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
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
MVC und Schichten ordnen eine ganze Anwendung. Sie trennen, was zusammengehört: Darstellung, Ablaufsteuerung und Fachlogik bekommen je ihren Platz.
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 €
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.
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.
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() }
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.
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; } }
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; }
Statt dass Komponenten sich direkt aufrufen, melden sie Ereignisse, auf die andere reagieren. Das entkoppelt: Der Auslöser muss nicht wissen, wer zuhört.
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; }
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
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.
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.
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"; } }
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.
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.
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.
$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);
public function testThrowsOnInvalid(): void { $this->expectException(\InvalidArgumentException::class); new Email("kaputt"); // soll werfen }
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)); }
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.
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 }
# 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.
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); }
declare(strict_types=1) und vollständigen Typdeklarationen ist sie der wirksamste Hebel für robusten Code.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.
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
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; }
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.
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.
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
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.
Schon im Modus develop macht Xdebug Fehlermeldungen und Stacktraces deutlich lesbarer und zeigt die Variablenwerte im Aufrufpfad – oft sieht man die Ursache sofort.
var_dump-Reste im ausgelieferten Code (die statische Analyse warnt davor).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.
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.
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
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";
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.
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)
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.
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); }
echo memory_get_peak_usage(true) / 1024 / 1024 . " MB"; // Spitzenverbrauch
unset frei. Ein Worker, der Speicher anhäuft, wird üblicherweise nach N Aufträgen kontrolliert neu gestartet.PHP läuft nicht nur im Web. Auf der Kommandozeile baust du Wartungsskripte, Cronjobs, Importer und Hintergrund-Worker – ganz ohne Webserver.
$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.
fwrite(STDOUT, "Fortschritt...\n"); fwrite(STDERR, "Warnung\n"); // Fehlerkanal, getrennt umleitbar $line = trim(fgets(STDIN)); // eine Eingabezeile lesen
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
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.Manchmal muss PHP andere Programme starten oder direkt mit C-Bibliotheken sprechen. Dafür gibt es Prozess-Funktionen und – für Fortgeschrittene – FFI.
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
Baust du Argumente aus Variablen, drohen Command-Injection-Angriffe. Maskiere immer:
$file = escapeshellarg($userInput); // sicher gequotet exec("wc -l " . $file, $out);
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);
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)
escapeshellarg/escapeshellcmd nutzen). FFI ist mächtig, aber fehleranfällig und umgeht PHPs Speichersicherheit – nur einsetzen, wenn es keine PHP-Lösung gibt.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.
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
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
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.
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.
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
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
Mehrere Anfragen starten quasi gleichzeitig; die Gesamtdauer entspricht der langsamsten, nicht der Summe – ideal für APIs, die aufeinander warten.
Drei Wege, PHP über das klassische „pro Request neu starten" hinaus laufen zu lassen – mit unterschiedlichen Stärken.
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 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
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.
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.
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 }
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.
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.
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.
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
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
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
# bekannte Sicherheitslücken in Abhängigkeiten finden composer audit # welche Pakete sind veraltet? composer outdated --direct
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.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.
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/" } }
Der gemeinsame Formatierungsstandard (Teil 14). Werkzeuge wie PHP-CS-Fixer setzen ihn automatisch durch, sodass Code aus aller Welt gleich aussieht.
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"); } }
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; }
Ein einheitliches Interface für DI-Container mit nur zwei Methoden – get() und has() – sodass Frameworks und Bibliotheken denselben Container nutzen können.
Symfony ist zweierlei: eine Sammlung wiederverwendbarer Komponenten und ein darauf aufbauendes Full-Stack-Framework. Viele andere Projekte – auch Laravel – nutzen Symfony-Komponenten im Unterbau.
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
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"); } }
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.
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.
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();
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
use Illuminate\Support\Facades\Route; Route::get("/produkte/{id}", [ProductController::class, "zeigen"]);
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.
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.
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
Der Standard für ausgehende HTTP-Anfragen (Teil 10) – komfortabel und gut testbar.
Erweitert DateTime um eine ausdrucksstarke, lesbare API:
use Carbon\Carbon; echo Carbon::now()->addDays(3)->diffForHumans(); // "in 3 Tagen"
Das große, datenbankunabhängige ORM (Teil 9) – Herzstück vieler Symfony-Projekte.
Die Qualitäts-Werkzeuge aus Teil 14: Tests, statische Analyse – und Rector, das Code automatisch umbaut (etwa bei Upgrades, siehe nächster Teil).
Bibliotheken liegen auf packagist.org. Achte vor dem Einbinden auf: aktive Wartung (letzter Commit), Download-Zahlen, PHP-Versionsanforderung, offene Sicherheitsmeldungen und eine klare Lizenz.
composer audit).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.
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
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 ...
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
.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.Deployment bringt deinen Code auf den Server – idealerweise vorhersehbar, wiederholbar und ohne Ausfall. Ein paar Bausteine machen aus „Dateien hochladen" einen verlässlichen Prozess.
# 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
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.
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
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.
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()]);
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.
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).
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"]); }
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.
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
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
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
intl formatiert und speichere intern neutral (UTC, Cent, Roh-Werte) – formatiert wird erst bei der Ausgabe, passend zur Region des Nutzers.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.
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.
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
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
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.
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.
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.
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.
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.
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.