```
PHP ist eine Skriptsprache, die Code in Ausgaben verwandelt – Text, HTML, JSON. Sie läuft auf deinem Rechner und auf Webservern. Bevor wir programmieren, brauchst du eine lauffähige Installation.
PHP ist auf jedem Betriebssystem mit wenigen Befehlen installiert. Die aktuelle stabile Version ist PHP 8.5 (Stand 2026); für neue Projekte ist das die richtige Wahl, PHP 8.4 der konservative Fallback.
| System | Befehl |
|---|---|
| macOS (Homebrew) | brew install php |
| Ubuntu / Debian | sudo apt install php-cli |
| Fedora | sudo dnf install php-cli |
| Windows | WSL2 + Ubuntu, dann apt install php-cli |
Prüfe danach im Terminal, ob alles läuft:
php --version
# PHP 8.5.6 (cli) ...
PHP-Code lebt in Dateien mit der Endung .php. Lege hallo.php an:
<?php echo "Hallo Welt!";
Die Zeile <?php öffnet einen PHP-Block. echo gibt Text aus. Der Text steht in Anführungszeichen und heißt String. Jede Anweisung endet mit einem Semikolon.
Ausführen:
php hallo.php
# Hallo Welt!
PHP bringt einen Entwicklungs-Webserver mit. Damit testest du Web-Code ohne Apache oder Nginx:
php -S localhost:8000
Öffne http://localhost:8000 im Browser. Jede .php-Datei im Ordner wird nun ausgeführt und das Ergebnis ausgeliefert.
PHP wurde fürs Web erfunden. Du kannst PHP-Blöcke direkt in HTML einstreuen. Alles außerhalb von <?php ... ?> wird unverändert ausgegeben:
<h1>Meine Seite</h1> <?php echo "Heute ist " . date("d.m.Y"); ?>
?> bewusst weg. Das verhindert versehentliche Leerzeichen in der Ausgabe – ein verbreiteter Standard (PSR-12).Eine Variable ist ein benannter Speicherplatz für einen Wert – wie eine beschriftete Box. In PHP beginnen Variablennamen immer mit einem Dollar-Zeichen.
$name = "Marek"; $alter = 34; $groesse = 1.82; $istAktiv = true;
Das einfache = bedeutet nicht „ist gleich", sondern „speichere den rechten Wert links". Das nennt man Zuweisung. Variablennamen sind frei wählbar, beginnen mit Buchstabe oder Unterstrich und sind case-sensitive: $name und $Name sind verschieden.
| Typ | Bedeutung | Beispiel |
|---|---|---|
string | Text | "Hallo" |
int | Ganzzahl | 42 |
float | Kommazahl | 3.14 |
bool | Wahrheitswert | true / false |
array | Liste von Werten | [1, 2, 3] |
null | „kein Wert" | null |
PHP ist dynamisch typisiert: Eine Variable kann ihren Typ wechseln. Mit gettype() oder var_dump() siehst du, was drinsteckt:
$x = 42; var_dump($x); // int(42) $x = "jetzt Text"; var_dump($x); // string(10) "jetzt Text"
Werte, die sich nie ändern, speicherst du in Konstanten. Sie haben kein $ und werden traditionell GROSS geschrieben:
const MWST = 0.19; echo MWST; // 0.19
$ vor jeder Variable ist eine PHP-Besonderheit. Es erlaubt dem Interpreter, Variablen sofort von Schlüsselwörtern wie echo zu unterscheiden, und macht sie in eingebettetem HTML klar erkennbar.Operatoren verknüpfen Werte zu neuen Werten. Sie sind das Handwerkszeug für jede Berechnung und jeden Vergleich.
echo 7 + 3; // 10 echo 7 - 3; // 4 echo 7 * 3; // 21 echo 7 / 2; // 3.5 echo 7 % 3; // 1 (Rest der Division) echo 2 ** 8; // 256 (Potenz)
Statt $x = $x + 5 schreibt man kürzer:
$x += 5; // erhöhen um 5 $x -= 2; // verringern um 2 $x *= 3; // multiplizieren $x++; // um 1 erhöhen $x--; // um 1 verringern
| Operator | Bedeutung |
|---|---|
== | gleich (Wert) |
=== | gleich (Wert und Typ) |
!= / !== | ungleich / streng ungleich |
< > <= >= | kleiner, größer, … |
<=> | Spaceship: -1, 0 oder 1 |
$a = true; $b = false; var_dump($a && $b); // false (und) var_dump($a || $b); // true (oder) var_dump(!$a); // false (nicht)
== vergleicht nur den Wert und wandelt Typen vorher um: 0 == "text" kann überraschende Ergebnisse liefern. === prüft Wert und Typ und ist fast immer die sichere Wahl. Gewöhne dir === als Standard an.Text ist allgegenwärtig. PHP bietet viele Wege, Strings zu bauen, zu kombinieren und zu durchsuchen.
In doppelten Anführungszeichen werden Variablen direkt eingesetzt (Interpolation). In einfachen nicht:
$name = "Anna"; echo "Hallo $name"; // Hallo Anna echo 'Hallo $name'; // Hallo $name
Bei zusammengesetzten Ausdrücken nutzt du geschweifte Klammern zur Abgrenzung:
echo "Summe: {$preis €}";
Der Punkt-Operator klebt Strings zusammen:
$gruss = "Hallo, " . $name . "!";
| Funktion | Zweck |
|---|---|
strlen($s) | Länge in Bytes |
mb_strlen($s) | Länge in Zeichen (Umlaute!) |
strtoupper / strtolower | Groß-/Kleinschreibung |
trim($s) | Leerzeichen am Rand entfernen |
str_replace(a, b, $s) | ersetzen |
str_contains($s, $t) | enthält? (bool) |
explode(",", $s) | String → Array |
implode(",", $arr) | Array → String |
$mail = " Marek@example.com "; echo strtolower(trim($mail)); // marek@example.com
Für formatierte Ausgaben mit Platzhaltern:
printf("%s ist %d Jahre alt.\n", $name, $alter); $preis = sprintf("%.2f €", 3.5); // "3.50 €"
$html = <<<HTML <h1>$name</h1> <p>Willkommen!</p> HTML;
mb_*-Varianten (Multibyte). strlen("Größe") zählt Bytes, mb_strlen("Größe") zählt Zeichen – das willst du fast immer.Programme entscheiden: „Wenn X, dann Y, sonst Z." Verzweigungen steuern, welcher Code unter welchen Umständen läuft.
$punkte = 75; if ($punkte >= 90) { echo "Sehr gut"; } elseif ($punkte >= 50) { echo "Bestanden"; } else { echo "Durchgefallen"; }
Die Bedingung in der Klammer muss einen Wahrheitswert ergeben. Der erste zutreffende Block läuft, der Rest wird übersprungen.
Seit PHP 8 gibt es match: kompakt, typsicher (===) und es liefert einen Wert zurück:
$tag = 3; $name = match($tag) { 1, 2, 3, 4, 5 => "Werktag", 6, 7 => "Wochenende", default => "ungültig", };
Eine kurze if/else-Form für einfache Fälle:
$status = $alter >= 18 ? "erwachsen" : "minderjährig";
Liefert den ersten Wert, der existiert und nicht null ist – praktisch für Standardwerte:
$name = $_GET['name'] ?? "Gast";
= in einer Bedingung speichert einen Wert, statt ihn zu prüfen – und ergibt fast immer „wahr”. Das ist ein klassischer Anfängerfehler. In Bedingungen gehören mindestens zwei Gleichheitszeichen.Schleifen wiederholen Code, bis eine Bedingung erfüllt ist. Sie ersparen dir, dieselbe Anweisung hundertmal zu tippen.
$i = 1; while ($i <= 3) { echo $i; // 123 $i++; }
Drei Teile in einer Zeile: Start, Bedingung, Schritt.
for ($i = 0; $i < 5; $i++) { echo $i; // 01234 }
Die wichtigste Schleife in der Praxis: Sie geht jedes Element eines Arrays durch.
$obst = ["Apfel", "Birne", "Kirsche"]; foreach ($obst as $frucht) { echo $frucht . "\n"; }
Mit Schlüssel und Wert zugleich:
foreach ($preise as $produkt => $preis) { echo "$produkt: $preis €\n"; }
break bricht die Schleife ganz ab, continue springt zum nächsten Durchlauf:
foreach ($zahlen as $z) { if ($z < 0) continue; // negative überspringen if ($z > 100) break; // ab 100 aufhören echo $z; }
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.Ein Array speichert mehrere Werte unter einem Namen. In PHP ist es extrem flexibel: Liste, Schlüssel-Wert-Sammlung und Verschachtelung in einem.
Werte mit automatischen Nummern (beginnend bei 0):
$farben = ["rot", "grün", "blau"]; echo $farben[0]; // rot $farben[] = "gelb"; // anhängen
Werte mit eigenen Schlüsseln – ideal für strukturierte Daten:
$person = [ "name" => "Marek", "alter" => 34, "stadt" => "Kaltenkirchen", ]; echo $person["name"]; // Marek
$team = [ ["name" => "Anna", "rolle" => "Dev"], ["name" => "Ben", "rolle" => "Design"], ]; echo $team[0]["name"]; // Anna
| Funktion | Zweck |
|---|---|
count($a) | Anzahl Elemente |
in_array($x, $a) | enthält Wert? |
array_keys / array_values | Schlüssel / Werte |
sort / rsort | auf-/absteigend sortieren |
array_map | jeden Wert transformieren |
array_filter | Werte herausfiltern |
array_merge | Arrays zusammenfügen |
$zahlen = [1, 2, 3, 4]; $quadrate = array_map(fn($n) => $n ** 2, $zahlen); // [1, 4, 9, 16] $gerade = array_filter($zahlen, fn($n) => $n % 2 === 0); // [2, 4]
Eine Funktion bündelt Code, den du benennen und wiederverwenden kannst. Sie nimmt Eingaben (Parameter) und liefert oft ein Ergebnis (Rückgabewert).
function begruessen($name) { echo "Hallo, $name!\n"; } begruessen("Marek"); begruessen("Anna");
return beendet die Funktion und liefert einen Wert an den Aufrufer zurück:
function addiere($a, $b) { return $a + $b; } $summe = addiere(3, 5); // 8
Unterschied: echo gibt etwas auf dem Bildschirm aus, return gibt einen Wert zurück, mit dem du weiterrechnen kannst.
Modernes PHP gibt Parametern und Rückgabe feste Typen. Das macht Code sicherer und selbsterklärend:
function addiere(int $a, int $b): int { return $a + $b; }
function verbinde(string $host, int $port = 5432): string { return "$host:$port"; } verbinde("db.local"); // db.local:5432 verbinde("db.local", port: 5433); // benanntes Argument
Funktionen ohne Namen, oft als Argument für andere Funktionen:
$verdopple = fn($x) => $x * 2; echo $verdopple(21); // 42
Sobald ein Programm wächst, willst du es auf mehrere Dateien verteilen – nach Themen geordnet. PHP bindet Dateien mit vier Schlüsselwörtern ein.
Beide fügen den Inhalt einer anderen Datei an dieser Stelle ein. Der Unterschied liegt im Fehlerfall:
| Befehl | Wenn Datei fehlt |
|---|---|
include | Warnung, Programm läuft weiter |
require | fataler Fehler, Programm stoppt |
include_once / require_once | bindet nur einmal ein |
// funktionen.php function gruss($n) { return "Hi $n"; } // app.php require_once "funktionen.php"; echo gruss("Marek");
Bindest du dieselbe Datei zweimal ein, würde eine Funktion doppelt definiert – das ist ein Fehler. require_once merkt sich, was schon geladen wurde, und verhindert das.
require sieht man vor allem noch für den zentralen autoload.php-Einstieg.Fehler passieren: eine Datei fehlt, eine Eingabe ist ungültig, ein Server antwortet nicht. Exceptions sind PHPs strukturierter Weg, damit umzugehen.
Du umschließt riskanten Code mit try. Tritt ein Fehler auf, springt PHP in den passenden catch-Block – das Programm stürzt nicht ab:
try { $wert = 10 / $teiler; } catch (DivisionByZeroError $e) { echo "Division durch Null!"; }
Mit throw löst du selbst einen Fehler aus, wenn etwas nicht stimmt:
function alterPruefen(int $alter): void { if ($alter < 0) { throw new InvalidArgumentException("Alter negativ"); } }
Der finally-Block läuft immer – egal ob ein Fehler auftrat. Ideal zum Aufräumen:
try { $datei = fopen("log.txt", "a"); // ... schreiben ... } finally { fclose($datei); // immer schließen }
Alle Fehlerklassen erben von Throwable. Wichtig zu wissen:
Exception – „normale" Fehler, die man abfangen sollteError – schwere PHP-Fehler (Typfehler, fehlende Funktion)RuntimeException, LogicExceptioncatch, das Fehler verschluckt, ist gefährlich – du merkst nie, dass etwas schiefging. Fange nur Fehler ab, auf die du sinnvoll reagieren kannst, und logge oder melde den Rest.Mit Datum und Zeit zu rechnen ist erstaunlich fehleranfällig – Zeitzonen, Schaltjahre, Sommerzeit. PHP nimmt dir das mit der DateTime-Klasse ab.
echo date("d.m.Y"); // 29.05.2026 echo date("H:i"); // 14:30
$jetzt = new DateTime(); $termin = new DateTime("2026-12-24 18:00"); echo $termin->format("d.m.Y"); // 24.12.2026
$heute = new DateTime(); $heute->modify("+2 weeks"); $diff = $heute->diff(new DateTime("2026-12-31")); echo $diff->days . " Tage";
$tz = new DateTimeZone("Europe/Berlin"); $d = new DateTime("now", $tz);
DateTime und DateTimeImmutable. Bei DateTime verändert modify() das Objekt selbst – das führt zu Überraschungen. Nutze besser DateTimeImmutable: Operationen geben ein neues Objekt zurück und lassen das Original unangetastet.Daten dauerhaft speichern heißt oft: in eine Datei schreiben. PHP bietet dafür angenehm kurze Funktionen.
// ganze Datei lesen $inhalt = file_get_contents("notizen.txt"); // ganze Datei schreiben (überschreibt) file_put_contents("notizen.txt", "Hallo\n"); // anhängen statt überschreiben file_put_contents("log.txt", "Eintrag\n", FILE_APPEND);
Bei großen Dateien liest du nicht alles auf einmal in den Speicher, sondern Zeile für Zeile:
$f = fopen("gross.csv", "r"); while (($zeile = fgets($f)) !== false) { echo trim($zeile); } fclose($f);
if (file_exists("config.php")) { /* ... */ } if (is_dir("uploads")) { /* ... */ }
$dateien = glob("bilder/*.jpg"); foreach ($dateien as $pfad) { echo basename($pfad) . "\n"; }
__DIR__ (das Verzeichnis der aktuellen Datei), z. B. __DIR__ . "/data/log.txt", statt dich auf das Arbeitsverzeichnis zu verlassen.JSON ist das Standardformat für den Datenaustausch im Web. PHP wandelt Arrays und Objekte mit zwei Funktionen hin und her.
$daten = [ "name" => "Marek", "tags" => ["php", "godot"], ]; $json = json_encode($daten, JSON_PRETTY_PRINT); echo $json;
{
"name": "Marek",
"tags": ["php", "godot"]
}
Mit true als zweitem Argument bekommst du ein assoziatives Array statt eines Objekts:
$arr = json_decode($json, true); echo $arr["name"]; // Marek
Ist das JSON kaputt, liefert json_decode null. Sauberer: per Flag eine Exception werfen lassen:
try { $arr = json_decode($json, true, flags: JSON_THROW_ON_ERROR); } catch (JsonException $e) { echo "Ungültiges JSON"; }
fgetcsv() und fputcsv(). XML liest man am robustesten mit SimpleXML oder der DOM-Erweiterung. JSON ist aber im modernen Web der Normalfall.Composer ist PHPs Paketmanager – das Tor zur riesigen Bibliothekswelt. Er installiert fremden Code und lädt deine eigenen Klassen automatisch.
composer init
# beantwortet ein paar Fragen, erzeugt composer.json
composer require guzzlehttp/guzzle
# lädt das Paket + Abhängigkeiten nach vendor/
Composer legt zwei Dateien an: composer.json (was du willst) und composer.lock (welche Versionen exakt installiert sind). Beide gehören ins Git-Repository, der Ordner vendor/ nicht.
Das Herzstück: Du bindest eine Datei ein, und alle Klassen laden sich bei Bedarf selbst:
require "vendor/autoload.php"; $client = new GuzzleHttp\Client();
In composer.json verknüpfst du einen Namespace mit einem Ordner:
{
"autoload": {
"psr-4": { "App\\": "src/" }
}
}
Danach einmal composer dump-autoload – und die Klasse App\Service\Mailer wird automatisch in src/Service/Mailer.php gesucht.
composer require paket zum Hinzufügen und composer install zum Einrichten eines geklonten Projekts. Mit --dev markierst du Werkzeuge (Tests, Linter), die nur in der Entwicklung gebraucht werden.PHP ist dynamisch typisiert, aber du kannst – und solltest – Typen explizit angeben. Das macht Code sicherer und für Editoren verständlich.
Die Grundtypen für Parameter, Rückgaben und Eigenschaften:
| Typ | Beispielwert |
|---|---|
int | 42 |
float | 3.14 |
string | "text" |
bool | true |
array | [1, 2] |
function rabatt(float $preis, int $prozent): float { return $preis * (1 - $prozent / 100); }
Standardmäßig wandelt PHP Typen weich um: Ein String "5" wird für einen int-Parameter zu 5. Mit dieser Zeile ganz oben in der Datei schaltest du das ab:
<?php declare(strict_types=1);
Jetzt muss der Typ exakt passen, sonst gibt es einen TypeError. Das deckt Fehler früh auf.
class Konto { public float $saldo = 0.0; public string $inhaber; }
declare(strict_types=1); zur Gewohnheit – als erste Zeile jeder PHP-Datei. Die wenigen Stellen, an denen es dich zwingt, einen Wert bewusst zu casten, sind genau die Stellen, an denen sonst stille Bugs entstehen.Das Typsystem kann mehr als einzelne Grundtypen: Es drückt aus „dieses oder jenes", „dieses oder nichts" und besondere Fälle wie „gibt nie zurück".
Ein ? vor dem Typ erlaubt zusätzlich null – typisch für „nicht gefunden":
function finde(int $id): ?User { // gibt User oder null zurück }
function id(int|string $wert): string { return (string) $wert; }
| Typ | Bedeutung |
|---|---|
void | gibt nichts zurück |
never | kehrt nie zurück (wirft / beendet) |
mixed | jeder beliebige Typ |
self / static | die eigene Klasse |
function abbruch(string $msg): never { throw new RuntimeException($msg); }
Seit PHP 8.1: ein Wert, der mehrere Interfaces zugleich erfüllt:
function verarbeite(Countable&Iterator $x): void { /* ... */ }
mixed bedeutet „alles erlaubt” und schaltet damit den Schutz des Typsystems faktisch ab. Es ist gelegentlich nötig (z. B. bei generischen Containern), sollte aber die Ausnahme bleiben – je präziser dein Typ, desto mehr Fehler fängt PHP für dich ab.Ein Enum ist ein eigener Typ mit einer festen, abgeschlossenen Menge möglicher Werte. Statt loser Strings wie "aktiv" bekommst du echte, typsichere Optionen.
Früher übergab man Status als String – fehleranfällig, weil Tippfehler erst zur Laufzeit auffallen:
$status = "aktif"; // Tippfehler – PHP merkt nichts
enum Status { case Aktiv; case Pausiert; case Gesperrt; } function setze(Status $s): void { /* ... */ } setze(Status::Aktiv); // nur gültige Werte möglich
Wenn der Wert in einer Datenbank oder API auftaucht, hinterlegst du ihn:
enum Rolle: string { case Admin = "admin"; case Editor = "editor"; case Gast = "guest"; } $r = Rolle::from("admin"); // aus DB-Wert echo $r->value; // "admin" echo $r->name; // "Admin"
Enums dürfen Methoden haben – ideal für ableitbare Eigenschaften:
enum Ampel: string { case Rot = "rot"; case Gruen = "gruen"; public function darfFahren(): bool { return $this === Ampel::Gruen; } }
match zusammen: Da die Werte abgeschlossen sind, kann dein Editor warnen, wenn du einen Fall vergisst. Ersetze lose String-Konstanten in deinem Code nach und nach durch Enums.PHP 8 hat die Sprache spürbar moderner gemacht. Diese Features schreiben kürzeren, klareren Code – und sind heute Best Practice.
Argumente per Namen übergeben – die Reihenfolge wird egal, optionale Werte überspringbar:
erstelle(name: "Box", hoehe: 10, farbe: "blau");
?-> bricht eine Kette ab, sobald ein Glied null ist – statt einen Fehler zu werfen:
$land = $user?->adresse()?->land; // null, falls user oder adresse() null ist
Ganz neu: |> reicht einen Wert durch eine Kette von Funktionen – von links nach rechts lesbar statt verschachtelt:
// vorher: tief verschachtelt $r = array_sum(array_map(fn($x) => $x * 2, $zahlen)); // mit Pipe: von links nach rechts $r = $zahlen |> fn($a) => array_map(fn($x) => $x * 2, $a) |> array_sum(...);
Eine Funktion als Wert weiterreichen, ohne sie aufzurufen – das (...) macht's:
$fn = strtoupper(...); echo $fn("hallo"); // HALLO $gross = array_map(strtoupper(...), $woerter);
Array-Werte in einem Schritt auf Variablen verteilen:
[$jahr, $monat, $tag] = [2026, 5, 29]; ["name" => $name] = $person;
|>-Operator kam erst mit PHP 8.5 (Ende 2025). Er ist großartig für Datentransformationen, aber prüfe vor dem Einsatz, dass deine Zielumgebung wirklich auf 8.5 läuft – auf älteren Versionen ist es schlicht ein Syntaxfehler.Eine Klasse ist ein Bauplan, ein Objekt das fertige Ding. Die Klasse beschreibt, welche Daten (Eigenschaften) und welche Fähigkeiten (Methoden) etwas hat.
class Hund { public string $name; public function bellen(): string { return $this->name . " sagt Wuff!"; } }
Mit new erstellst du eine konkrete Instanz. Auf Eigenschaften und Methoden greifst du mit -> zu:
$bello = new Hund(); $bello->name = "Bello"; echo $bello->bellen(); // Bello sagt Wuff!
Innerhalb einer Methode verweist $this auf das aktuelle Objekt. So greift eine Methode auf die Eigenschaften ihres eigenen Objekts zu.
$a = new Hund(); $a->name = "Rex"; $b = new Hund(); $b->name = "Luna"; // $a und $b sind komplett getrennt
Hund ist der Bauplan – sie existiert einmal. Jedes mit new erzeugte Objekt ist eine eigenständige Instanz mit eigenen Daten. Aus einem Bauplan baust du beliebig viele Häuser.Nicht jeder Teil eines Objekts soll von außen erreichbar sein. Sichtbarkeits-Modifikatoren schützen die innere Logik – das nennt man Kapselung.
| Modifikator | Zugriff von … |
|---|---|
public | überall |
protected | Klasse + Unterklassen |
private | nur dieser Klasse selbst |
Eine private Eigenschaft kann nicht von außen in einen ungültigen Zustand gebracht werden. Du steuerst den Zugriff über Methoden:
class Konto { private float $saldo = 0; public function einzahlen(float $betrag): void { if ($betrag <= 0) { throw new InvalidArgumentException("> 0 nötig"); } $this->saldo += $betrag; } public function saldo(): float { return $this->saldo; } }
Von außen kann niemand $konto->saldo = -999 setzen – der Weg führt nur über einzahlen(), das prüft.
Neu: von außen lesbar, aber nur intern schreibbar – in einer Zeile:
class User { public private(set) string $id; } // $user->id lesen: ok / setzen von außen: Fehler
private, bis du einen Grund hast, es zu öffnen. Je kleiner die öffentliche Oberfläche einer Klasse, desto leichter kannst du ihre Interna später ändern, ohne anderen Code zu brechen.Der Konstruktor läuft automatisch beim Erzeugen eines Objekts. PHP 8 hat ihn drastisch verkürzt – das spart viel Tipparbeit.
class Punkt { public float $x; public float $y; public function __construct(float $x, float $y) { $this->x = $x; $this->y = $y; } }
Dasselbe in modern: Sichtbarkeit direkt in die Parameterliste schreiben – Eigenschaft und Zuweisung entstehen automatisch:
class Punkt { public function __construct( public float $x, public float $y, ) {} } $p = new Punkt(3, 4); echo $p->x; // 3
Beide Versionen sind exakt gleichwertig – die zweite ist nur viel kürzer.
Eine readonly-Eigenschaft darf nach dem Setzen im Konstruktor nicht mehr geändert werden – perfekt für Wertobjekte:
class Geld { public function __construct( public readonly int $cent, public readonly string $waehrung, ) {} } $preis = new Geld(999, "EUR"); // $preis->cent = 0; -> Error
readonly zu markieren, kannst du seit 8.2 die ganze Klasse so deklarieren: readonly class Geld { ... }. Ideal für DTOs und Wertobjekte, die nach Erzeugung unveränderlich bleiben sollen.Vererbung lässt eine Klasse die Eigenschaften und Methoden einer anderen übernehmen – und erweitern. So vermeidest du Wiederholung bei verwandten Typen.
class Tier { public function __construct(public string $name) {} public function geraeusch(): string { return "..."; } } class Katze extends Tier { public function geraeusch(): string { return "Miau"; } } $mieze = new Katze("Minka"); echo $mieze->name; // von Tier geerbt echo $mieze->geraeusch(); // Miau (überschrieben)
Beim Überschreiben kannst du die ursprüngliche Methode trotzdem mitnutzen:
class Hund extends Tier { public function __construct(string $name, public string $rasse) { parent::__construct($name); } }
final verhindert, dass eine Klasse erweitert oder eine Methode überschrieben wird:
final class Uuid { /* niemand darf erben */ }
Beide definieren Verträge: „Wer das sein will, muss diese Methoden bieten." Interfaces beschreiben reine Fähigkeiten, abstrakte Klassen liefern zusätzlich teilweise Umsetzung.
Ein Interface listet Methoden ohne Rumpf. Jede Klasse, die es implements, muss sie ausfüllen:
interface Zahlbar { public function betrag(): int; } class Rechnung implements Zahlbar { public function betrag(): int { return 4200; } }
Der Gewinn: Du programmierst gegen den Vertrag, nicht gegen eine konkrete Klasse. Eine Funktion kann jedes Zahlbar entgegennehmen:
function verbuche(Zahlbar $x): void { /* ... */ }
class Bestellung implements Zahlbar, JsonSerializable { /* ... */ }
Sie kann fertige Methoden mitbringen und abstrakte erzwingen. Selbst instanziieren lässt sie sich nicht:
abstract class Form { abstract public function flaeche(): float; public function beschreibung(): string { return "Fläche: " . $this->flaeche(); } } class Kreis extends Form { public function __construct(private float $r) {} public function flaeche(): float { return 3.14159 * $this->r ** 2; } }
Ein Trait ist ein Stück wiederverwendbarer Code, das du in mehrere Klassen „hineinkopierst" – ohne Vererbung. Er löst das Problem, dass eine Klasse nur von einer Klasse erben kann.
trait Zeitstempel { public ?DateTimeImmutable $erstellt = null; public function jetztSetzen(): void { $this->erstellt = new DateTimeImmutable(); } } class Artikel { use Zeitstempel; } class Kommentar { use Zeitstempel; // gleiche Funktionalität, keine Vererbung }
class Post { use Zeitstempel, Sluggable; }
Bringen zwei Traits eine gleichnamige Methode mit, wählst du explizit aus:
class Seite { use A, B { A::hallo insteadof B; B::hallo as halloB; } }
Manche Daten und Methoden gehören zur Klasse selbst, nicht zu einzelnen Objekten. Dafür gibt es statische Eigenschaften, Methoden und Klassen-Konstanten.
Zugriff erfolgt über den Klassennamen mit ::, ohne ein Objekt zu erzeugen:
class Zaehler { public static int $anzahl = 0; public static function hoch(): void { self::$anzahl++; } } Zaehler::hoch(); echo Zaehler::$anzahl; // 1
class Http { const OK = 200; const NOT_FOUND = 404; } echo Http::NOT_FOUND; // 404
class Config { const string ENV = "prod"; }
Statische Methoden als sprechende Alternativen zu new:
class Temperatur { private function __construct(public readonly float $celsius) {} public static function ausFahrenheit(float $f): static { return new self(($f - 32) * 5 / 9); } } $t = Temperatur::ausFahrenheit(98.6);
„Magische" Methoden beginnen mit zwei Unterstrichen und werden von PHP automatisch in bestimmten Situationen aufgerufen – etwa wenn ein Objekt als String genutzt wird.
Definiert, wie ein Objekt zu Text wird:
class Geld { public function __construct(public int $cent) {} public function __toString(): string { return number_format($this->cent / 100, 2) . " €"; } } echo new Geld(1999); // 19,99 €
Fangen Zugriffe auf nicht existierende Eigenschaften ab – Basis vieler Frameworks:
class Bag { private array $data = []; public function __get(string $k) { return $this->data[$k] ?? null; } public function __set(string $k, $v) { $this->data[$k] = $v; } } $b = new Bag(); $b->titel = "Test"; // __set echo $b->titel; // __get -> Test
__call fängt Aufrufe nicht existierender Methoden ab; __invoke macht ein Objekt aufrufbar wie eine Funktion:
class Verdoppler { public function __invoke(int $x): int { return $x * 2; } } $f = new Verdoppler(); echo $f(21); // 42
__toString für Wertobjekte) und bevorzuge sonst explizite, deklarierte Methoden.PHP hat keine echten Generics in der Sprache – aber das Denken in typisierten Sammlungen lohnt sich. Mit ein wenig Disziplin und Werkzeugen bekommst du fast denselben Komfort.
Ein array sagt nichts darüber, was drinsteckt. Eine Funktion, die User[] erwartet, kann das im Typsystem nicht ausdrücken:
function namen(array $users): array { // array von WAS? Editor weiß es nicht }
Statische Analyse-Werkzeuge (Teil 7) verstehen Generics in Kommentaren – die Sprache ignoriert sie, der Editor nicht:
/** @param User[] $users @return string[] */ function namen(array $users): array { /* ... */ }
Du kapselst das Array in einer Klasse, die nur den gewünschten Typ akzeptiert:
final class UserListe implements IteratorAggregate { private array $items = []; public function add(User $u): void { $this->items[] = $u; // nur User möglich } public function getIterator(): Iterator { return new ArrayIterator($this->items); } }
Jetzt ist foreach über die Liste möglich, und niemand kann versehentlich einen String hineinlegen.
Manchmal willst du über etwas iterieren, ohne alle Werte gleichzeitig im Speicher zu halten – etwa Millionen Zeilen aus einer Datei. Generatoren machen das mit minimalem Code.
Eine Funktion mit yield ist ein Generator: Sie liefert Werte einen nach dem anderen, pausiert dazwischen und merkt sich ihren Zustand:
function zaehleBis(int $max): Generator { for ($i = 1; $i <= $max; $i++) { yield $i; } } foreach (zaehleBis(3) as $n) { echo $n; // 123 }
Eine riesige Datei zeilenweise verarbeiten, ohne sie komplett zu laden:
function zeilen(string $pfad): Generator { $f = fopen($pfad, "r"); while (($z = fgets($f)) !== false) { yield trim($z); } fclose($f); } // verbraucht konstant wenig Speicher – egal wie groß die Datei
yield $key => $value;
Generatoren decken 95 % der Fälle ab. Für komplexe Iteration implementierst du Iterator direkt – mit current(), next(), valid(), key(), rewind().
foreach-Komfort, aber konstanter Speicherverbrauch statt linear wachsendem.Eine Closure ist eine anonyme Funktion, die sich Werte aus ihrer Umgebung „merkt". Sie ist die Grundlage von Callbacks, Event-Handlern und vielem Framework-Code.
Mit use nimmt eine anonyme Funktion Variablen von außen mit hinein:
$faktor = 3; $mal = function($x) use ($faktor) { return $x * $faktor; }; echo $mal(5); // 15
fn braucht kein use – es sieht die umgebenden Variablen automatisch (nur lesend):
$mal = fn($x) => $x * $faktor;
Mit & teilt die Closure dieselbe Variable, statt eine Kopie zu nehmen:
$summe = 0; $add = function($n) use (&$summe) { $summe += $n; }; $add(10); $add(5); echo $summe; // 15
Closures kennen das Objekt, in dem sie erzeugt wurden – mit bindTo() kannst du sie an ein anderes binden. Das nutzen Frameworks für Routing-Definitionen und Templating.
$closure = function() { return $this->name; }; $gebunden = Closure::bind($closure, $objekt, Klasse::class);
fn ist kompakt und fängt automatisch ein, kann aber nur einen Ausdruck enthalten. Die längere function() use(...)-Form erlaubt mehrere Zeilen und das Einfangen per Referenz. Für einfache Callbacks fn, für alles andere die lange Form.Attribute sind strukturierte Metadaten, die du direkt an Klassen, Methoden oder Eigenschaften heftest. Frameworks lesen sie aus, um Verhalten zu steuern – ganz ohne Konfigurationsdateien.
Attribute stehen in #[ ] direkt über dem Element:
#[Route("/users", methods: ["GET"])] public function liste(): Response { /* ... */ }
Ein Attribut ist nichts weiter als eine Klasse mit dem Marker #[Attribute]:
#[Attribute(Attribute::TARGET_METHOD)] class Route { public function __construct( public string $pfad, public array $methods = ["GET"], ) {} }
Über Reflection (nächstes Kapitel) liest ein Framework die Attribute zur Laufzeit aus:
$r = new ReflectionMethod($controller, "liste"); foreach ($r->getAttributes(Route::class) as $attr) { $route = $attr->newInstance(); echo $route->pfad; // /users }
Reflection erlaubt einem Programm, sich selbst zu untersuchen: Welche Methoden hat diese Klasse? Welche Parameter diese Funktion? Es ist die Magie hinter Dependency-Injection-Containern und Frameworks.
$r = new ReflectionClass(User::class); echo $r->getName(); // User foreach ($r->getMethods() as $m) { echo $m->getName() . "\n"; }
So findet ein DI-Container heraus, welche Abhängigkeiten ein Konstruktor braucht:
$ctor = (new ReflectionClass(Service::class))->getConstructor(); foreach ($ctor->getParameters() as $p) { echo $p->getType(); // der Typ des Parameters }
$obj = (new ReflectionClass($klassenName)) ->newInstanceArgs($argumente);
private und ist deutlich langsamer als direkter Code. In Anwendungscode brauchst du es fast nie – es ist Framework-Handwerk. Gut zu verstehen, um zu wissen, wie deine Werkzeuge funktionieren; im Alltag aber selten selbst zu schreiben.Namespaces verhindern Namenskollisionen, wenn Code aus vielen Quellen zusammenkommt. Sie sind die Ordnerstruktur des Codes – und die Basis für Autoloading.
Ein Namespace steht als erste Anweisung der Datei:
<?php namespace App\Service; class Mailer { /* voll: App\Service\Mailer */ }
Statt überall den vollen Pfad zu schreiben, importierst du oben einmal:
use App\Service\Mailer; use App\Model\User; $m = new Mailer(); // kurz statt voll qualifiziert
Heißen zwei Klassen gleich, gibst du einer per as einen anderen Namen:
use App\Pdf\Writer as PdfWriter; use App\Csv\Writer as CsvWriter;
use function App\Helpers\slugify; use const App\Config\VERSION;
Ein \ am Anfang meint „ab dem globalen Namespace". Innerhalb eines eigenen Namespaces brauchst du es, um auf eingebaute Klassen zuzugreifen:
namespace App; $d = new \DateTime(); // global, nicht App\DateTime
App\Service\Mailer liegt in src/Service/Mailer.php. Dann findet Composers Autoloader jede Klasse automatisch – du schreibst nie wieder ein require für Klassen.SOLID sind fünf Faustregeln für Klassen, die langlebig und änderbar bleiben. Sie sind keine Gesetze, sondern Werkzeuge gegen den schleichenden Verfall großer Codebasen.
Eine Klasse sollte genau einen Grund haben, sich zu ändern. Eine Klasse, die Daten lädt und formatiert und verschickt, ändert sich aus drei Richtungen – das ist eine zu viel.
Offen für Erweiterung, geschlossen für Änderung. Neues Verhalten fügst du durch neue Klassen hinzu, nicht durch das Aufbohren bestehender:
interface Rabatt { public function anwenden(float $preis): float; } // neuer Rabatt = neue Klasse, alter Code bleibt unberührt class Weihnachtsrabatt implements Rabatt { /* ... */ }
Eine Unterklasse muss überall einsetzbar sein, wo die Oberklasse erwartet wird – ohne Überraschungen. Wenn eine Unterklasse plötzlich Ausnahmen wirft, wo die Basis es nicht tut, ist das verletzt.
Lieber viele kleine, spezifische Interfaces als ein großes. Eine Klasse soll nicht Methoden implementieren müssen, die sie gar nicht braucht.
Hänge von Abstraktionen ab, nicht von konkreten Klassen. Statt im Code new MySQLConnection() zu schreiben, nimm ein Connection-Interface entgegen – das ist die Brücke zu Dependency Injection.
Dependency Injection (DI) heißt schlicht: Ein Objekt bekommt seine Abhängigkeiten von außen gereicht, statt sie selbst zu erzeugen. Das macht Code testbar und flexibel.
Erzeugt eine Klasse ihre Helfer selbst, ist sie fest verdrahtet – nicht austauschbar, nicht testbar:
class Bestellung { public function __construct() { $this->mailer = new SmtpMailer(); // fest verdrahtet } }
class Bestellung { public function __construct( private Mailer $mailer, // Interface, injiziert ) {} } // Produktion: new Bestellung(new SmtpMailer()); // Test: new Bestellung(new FakeMailer());
Bei vielen verschachtelten Abhängigkeiten verdrahtest du nicht alles von Hand. Ein Container baut Objekte samt ihrer Abhängigkeiten automatisch zusammen:
$container->get(Bestellung::class); // Container erkennt: braucht Mailer, baut SmtpMailer, ...
Container lesen die nötigen Typen über Reflection (Kapitel 31) aus den Konstruktor-Signaturen. Genau deshalb sind Typdeklarationen so wichtig.
Entwurfsmuster sind bewährte Lösungen für wiederkehrende Probleme. Du musst sie nicht auswendig kennen – aber die Namen helfen, über Architektur zu reden.
Austauschbare Algorithmen hinter einem gemeinsamen Interface – das Open/Closed-Prinzip in Aktion:
interface SortierStrategie { public function sortiere(array $daten): array; } // QuickSort, MergeSort … jeweils eine Klasse, frei tauschbar
Eine Methode, die Objekte erzeugt und die Entscheidung kapselt, welche konkrete Klasse:
class ZahlungsFactory { public static function erstelle(string $art): Zahlung { return match($art) { "paypal" => new PayPal(), "karte" => new Kreditkarte(), }; } }
Objekte abonnieren Ereignisse und werden benachrichtigt – die Basis von Event-Systemen:
interface Beobachter { public function benachrichtigt(Ereignis $e): void; }
Kapselt den Datenzugriff hinter einer sammlungsartigen Schnittstelle – der Code dahinter (SQL, API, Datei) bleibt verborgen:
interface UserRepository { public function finde(int $id): ?User; public function speichere(User $u): void; }
Statt nackte Strings und Zahlen durch den Code zu reichen, kapselst du Bedeutung in kleine Typen. Das macht Fehler unmöglich, die sonst erst zur Laufzeit auffallen.
function versende(string $email): void { /* ... */ } versende("kein-email"); // kompiliert, kracht erst später
Validiere einmal bei der Erzeugung – danach ist die Gültigkeit garantiert. readonly macht es unveränderlich:
final readonly class Email { public function __construct(public string $wert) { if (!filter_var($wert, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException("Ungültige E-Mail"); } } } function versende(Email $email): void { /* immer gültig */ }
Ein DTO bündelt zusammengehörige Daten in einem typisierten Objekt – etwa Formulareingaben oder API-Antworten. Kein Verhalten, nur Struktur:
final readonly class RegistrierDaten { public function __construct( public string $name, public Email $email, public int $alter, ) {} }
Wie eine Anwendung mit Fehlern umgeht, ist eine Architektur-Entscheidung. Gut gemacht, bleiben Fehler nachvollziehbar und die Geschäftslogik sauber.
Spezifische Exceptions erlauben gezieltes Abfangen und sprechende Fehler:
class KontoUeberzogen extends DomainException { public static function bei(float $fehlbetrag): self { return new self("Es fehlen $fehlbetrag €"); } }
Eine bewährte Regel: Fange Exceptions möglichst weit oben – an der „Grenze" der Anwendung (Controller, CLI-Einstieg). Die Geschäftslogik wirft nur, sie behandelt nicht. So bleibt sie frei von try/catch-Rauschen:
// Controller – die eine zentrale Stelle try { $service->verarbeite($daten); } catch (DomainException $e) { return fehlerSeite($e->getMessage()); }
Für erwartbare Fehlschläge (Validierung) sind Exceptions manchmal zu schwer. Eine Alternative ist ein Ergebnis-Objekt, das Erfolg oder Fehler trägt:
if ($ergebnis->istErfolg()) { $wert = $ergebnis->wert(); } else { echo $ergebnis->fehler(); }
Automatisierte Tests prüfen bei jeder Änderung, ob dein Code noch das Richtige tut. PHPUnit ist der De-facto-Standard dafür in der PHP-Welt.
composer require --dev phpunit/phpunit
Ein Test ist eine Methode, die etwas ausführt und mit assert-Aufrufen das erwartete Ergebnis prüft:
use PHPUnit\Framework\TestCase; final class RechnerTest extends TestCase { public function testAddition(): void { $r = new Rechner(); $this->assertSame(5, $r->addiere(2, 3)); } }
Ausführen:
vendor/bin/phpunit
# OK (1 test, 1 assertion)
| Assertion | Prüft |
|---|---|
assertSame($a, $b) | identisch (===) |
assertEquals($a, $b) | gleich (==) |
assertTrue / assertFalse | Wahrheitswert |
assertNull | ist null |
assertCount(3, $arr) | Anzahl Elemente |
expectException(...) | Fehler wird geworfen |
Statt fünf fast gleiche Tests zu schreiben, fütterst du einen Test mit Datensätzen:
#[DataProvider("zahlen")] public function testQuadrat(int $ein, int $aus): void { $this->assertSame($aus, quadrat($ein)); } public static function zahlen(): array { return [[2, 4], [3, 9], [5, 25]]; }
Um eine Klasse isoliert zu testen, ersetzt du ihre Abhängigkeiten durch kontrollierte Attrappen:
$mailer = $this->createMock(Mailer::class); $mailer->expects($this->once())->method("sende");
Statische Analyse findet Fehler, ohne den Code auszuführen – allein durch das Lesen. Werkzeuge wie PHPStan und Psalm fangen ganze Fehlerklassen ab, bevor sie je zur Laufzeit auftreten.
string, wo ein int erwartet wirdnull-Zugriffe, die zur Laufzeit krachen würdencomposer require --dev phpstan/phpstan vendor/bin/phpstan analyse src --level 6
Die Level reichen von 0 (locker) bis 10 (sehr streng). Ein guter Weg: niedrig starten und Stufe für Stufe erhöhen, während du die gemeldeten Probleme abarbeitest.
Hier zahlt sich das Generics-Denken aus Kapitel 27 aus. PHPStan versteht die Annotationen und prüft sie:
/** @return list<User> */ public function alle(): array { /* ... */ }
In bestehendem Code meldet PHPStan oft hunderte Probleme. Eine Baseline friert die aktuellen Funde ein, sodass nur neue Fehler auffallen:
vendor/bin/phpstan analyse --generate-baseline
declare(strict_types=1) und Tests bildet das ein engmaschiges Sicherheitsnetz.Einheitlicher Stil macht Code lesbar und beendet sinnlose Diskussionen über Klammern und Einrückung. Werkzeuge erledigen die Formatierung automatisch.
Die PHP-Community hat sich auf einen gemeinsamen Stil geeinigt: PSR-12. Er regelt Einrückung (4 Leerzeichen), Klammersetzung, Import-Reihenfolge und mehr. Du musst ihn nicht auswendig lernen – Werkzeuge setzen ihn durch.
Formatiert deinen Code automatisch nach festgelegten Regeln:
composer require --dev friendsofphp/php-cs-fixer vendor/bin/php-cs-fixer fix src
Häufige Befehle bündelst du in composer.json – ein Befehl statt vieler:
{
"scripts": {
"check": [
"@php vendor/bin/phpunit",
"@php vendor/bin/phpstan analyse src",
"@php vendor/bin/php-cs-fixer fix --dry-run"
]
}
}
Danach genügt composer check, um Tests, Analyse und Stilprüfung in einem Rutsch laufen zu lassen.
| Aufgabe | Werkzeug |
|---|---|
| Pakete | Composer |
| Tests | PHPUnit / Pest |
| Statische Analyse | PHPStan / Psalm |
| Formatierung | PHP-CS-Fixer / PHPCS |
| Debugging | Xdebug |
Früher oder später läuft Code anders als gedacht. Statt mit echo zu raten, schaust du mit einem Debugger Schritt für Schritt zu, was wirklich passiert.
Für einen kurzen Blick reicht oft eine Ausgabe. var_dump zeigt Wert und Typ:
var_dump($daten); // in Frameworks oft: dump($daten) oder dd($daten) (dump and die)
Xdebug ist eine PHP-Erweiterung. Einmal eingerichtet, kannst du im Editor Breakpoints setzen: Das Programm hält dort an, und du untersuchst alle Variablen live.
# Installation (Beispiel Ubuntu)
sudo apt install php-xdebug
Im Editor (z. B. PHPStorm oder VS Code) aktivierst du „Listen for debug connections", setzt einen Breakpoint per Klick an den Zeilenrand und lädst die Seite. Der Ablauf stoppt – du siehst den kompletten Aufruf-Stack.
| Aktion | Bedeutung |
|---|---|
| Step Over | nächste Zeile, Funktionen am Stück |
| Step Into | in die aufgerufene Funktion hinein |
| Step Out | aktuelle Funktion zu Ende |
| Watch | einen Ausdruck dauerhaft beobachten |
In der Entwicklung sollten alle Fehler angezeigt werden – verstecke sie nie still:
error_reporting(E_ALL); ini_set("display_errors", "1"); // In Produktion: display_errors aus, dafür ins Log schreiben
var_dump-Zeilen kommt man weit – aber bei verschachtelten Aufrufen oder Schleifen verliert man schnell den Überblick. Die halbe Stunde, Xdebug einmal einzurichten, spart über die Zeit viele Stunden Rätselraten.PHP ist schnell – wenn man es lässt. Die größten Hebel liegen selten im Mikro-Tuning einzelner Zeilen, sondern in Caching, weniger Arbeit und dem richtigen Werkzeug.
Normalerweise übersetzt PHP jede Datei bei jedem Aufruf neu. OPcache speichert das Übersetzungsergebnis im Speicher – ein gewaltiger Gewinn, fast geschenkt:
; php.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
In Produktion ist OPcache praktisch Pflicht. Es ist der wirkungsvollste einzelne Performance-Schalter.
Optimiere nie nach Bauchgefühl. Ein Profiler (Xdebug, Blackfire, SPX) zeigt, wo die Zeit wirklich verbraucht wird – oft an völlig anderen Stellen als vermutet:
$start = hrtime(true); // ... Code ... $ms = (hrtime(true) - $start) / 1_000_000;
Ein klassischer Datenbank-Fehler: In einer Schleife für jeden Datensatz eine eigene Abfrage. 100 Nutzer = 101 Abfragen statt einer. Lade verwandte Daten gebündelt.
Seit PHP 8 gibt es einen JIT-Compiler. Er beschleunigt rechenintensive Aufgaben (Bildverarbeitung, Mathematik) spürbar, bringt bei typischem Web-Code mit viel I/O aber kaum etwas.
PHP verwaltet Speicher automatisch, aber zu verstehen, wie Werte kopiert und freigegeben werden, hilft bei großen Datenmengen und kniffligen Bugs.
Weist du eine Variable einer anderen zu, kopiert PHP den Wert nicht sofort – erst wenn einer der beiden geändert wird. Das spart Speicher, ohne dass du etwas tun musst:
$a = range(1, 1_000_000); $b = $a; // noch keine Kopie, beide teilen sich $b[0] = 99; // jetzt erst wird kopiert
Eine Referenz ist ein zweiter Name für dieselbe Variable. Änderungen wirken auf beide:
$a = 1; $b = &$a; // $b ist $a $b = 99; echo $a; // 99
PHP zählt, wie viele Namen auf einen Wert zeigen. Sinkt der Zähler auf null, wird der Speicher frei. Zirkuläre Referenzen (A zeigt auf B, B auf A) fängt ein zusätzlicher Collector ab.
echo memory_get_usage(true); // aktuell echo memory_get_peak_usage(true); // Höchststand
&, um „Performance zu sparen” – dank Copy-on-Write ist das fast nie nötig und führt zu schwer auffindbaren Bugs, wenn unerwartet eine entfernte Variable mitverändert wird. Nutze Referenzen nur, wenn du sie wirklich brauchst (etwa sort(), das sein Argument verändert).Drei fortgeschrittene Wege, über die übliche Request-Verarbeitung hinauszugehen: externe Programme starten, C-Bibliotheken einbinden und kooperatives Multitasking.
Manchmal ist das beste Werkzeug ein anderes Programm. proc_open gibt dir volle Kontrolle über dessen Ein- und Ausgabe:
$out = shell_exec("git rev-parse HEAD"); // für einfache Fälle; bei Nutzereingaben unbedingt escapen!
Mit der Foreign Function Interface bindest du vorhandene C-Bibliotheken direkt ein, ohne eine PHP-Erweiterung zu schreiben:
$ffi = FFI::cdef( "int abs(int);", "libc.so.6" ); echo $ffi->abs(-42); // 42
FFI ist ein Nischenwerkzeug – nützlich, um Spezialbibliotheken anzuzapfen, aber selten im Alltag.
Seit PHP 8.1 gibt es Fibers: Funktionen, die sich selbst pausieren und später fortsetzen können. Sie sind die Grundlage moderner asynchroner Frameworks wie ReactPHP und Amp:
$fiber = new Fiber(function() { $wert = Fiber::suspend("pausiert"); echo "weiter mit $wert"; }); $x = $fiber->start(); // "pausiert" $fiber->resume("Daten"); // weiter mit Daten
PHP ist nicht nur für Webseiten da. Kommandozeilen-Programme – für Wartung, Datenimport, Cronjobs – sind ein wichtiger Einsatzbereich.
Das Array $argv enthält die übergebenen Argumente, $argc ihre Anzahl:
// php import.php datei.csv --dry-run echo $argv[1]; // datei.csv
CLI-Programme nutzen drei Standard-Kanäle. Fehler gehören nach STDERR, damit sie sich von der normalen Ausgabe trennen lassen:
fwrite(STDOUT, "Fertig\n"); fwrite(STDERR, "Warnung!\n"); $eingabe = fgets(STDIN); // vom Nutzer lesen
Ein Programm meldet Erfolg oder Misserfolg über seinen Rückgabewert: 0 = alles gut, alles andere = Fehler. Skripte und CI verlassen sich darauf:
if ($fehler) { fwrite(STDERR, "Abbruch\n"); exit(1); } exit(0);
Für ernsthafte CLI-Werkzeuge lohnt eine Bibliothek. Symfony Console liefert Argument-Parsing, Hilfetexte, Farben, Fortschrittsbalken und Tabellen:
composer require symfony/console
Damit definierst du Befehle als Klassen, bekommst --help automatisch und eine saubere Struktur für wachsende Werkzeuge.
#!/usr/bin/env php und ist es ausführbar (chmod +x), kannst du es ohne vorangestelltes php direkt aufrufen – wie jedes andere Kommandozeilen-Programm.Die meisten Sicherheitslücken sind seit Jahren dieselben – und seit Jahren vermeidbar. Wer diese Klassiker kennt, schließt die gefährlichsten Türen.
Niemals Nutzereingaben in SQL einbauen. Prepared Statements trennen Befehl und Daten – die Eingabe kann nie als Code interpretiert werden:
// FALSCH – angreifbar $db->query("SELECT * FROM users WHERE id = $id"); // RICHTIG – Prepared Statement $stmt = $db->prepare("SELECT * FROM users WHERE id = ?"); $stmt->execute([$id]);
Gibst du Nutzereingaben in HTML aus, ohne sie zu maskieren, kann jemand Schadcode einschleusen. htmlspecialchars entschärft das:
echo htmlspecialchars($kommentar, ENT_QUOTES);
Ein Angreifer bringt den Browser eines eingeloggten Nutzers dazu, ungewollt Aktionen auszuführen. Schutz: ein geheimes, pro Formular einzigartiges Token, das der Server prüft.
Niemals im Klartext, niemals mit MD5 oder SHA1. PHP bringt die richtige Funktion mit – sie hasht, salzt und passt die Stärke automatisch an:
$hash = password_hash($passwort, PASSWORD_DEFAULT); if (password_verify($eingabe, $hash)) { // Login korrekt }
| Regel | Warum |
|---|---|
| Eingaben validieren | traue keiner Quelle von außen |
| Ausgaben maskieren | verhindert XSS |
| Prepared Statements | verhindert SQL-Injection |
password_hash | sichere Passwörter |
| Abhängigkeiten aktuell halten | composer audit |
composer audit meldet zudem bekannte Lücken in deinen Abhängigkeiten – führe es regelmäßig aus.