Skip to content

PHP Spickzettel

Beliebte Allzweck-Skriptsprache für Web-Entwicklung.

01

Grundlagen

Variablen, Typen & Konstanten

PHP-Variablen beginnen mit $ und sind dynamisch typisiert. PHP 7.4+ unterstützt typisierte Properties. Verwenden Sie define() für Runtime-Konstanten und const für Compile-Zeit-Konstanten (schneller). PHP 8.1 führte Enums ein — ein typisiertes Enumeration-System, das Klassen-Konstanten überlegen ist. Deklarieren Sie immer Typen, wo möglich (PHP 7+), für frühe Fehlererkennung und besseren IDE-Support.

php
<?php
$name = "Alice";          // string
$age = 30;                // integer
$price = 19.99;           // float (double)
$active = true;           // boolean
$items = [1, 2, 3];       // array
$null = null;             // null

// constants
define("MAX_USERS", 100); // runtime constant
const PI = 3.14159;       // compile-time constant
echo MAX_USERS;            // no $ prefix

// PHP 8.1+ enums
enum Status: string {
    case Active = 'active';
    case Inactive = 'inactive';
}
$s = Status::Active;

Echo, Print & Debugging

echo ist ein Sprach-Konstrukt (keine Funktion) — am schnellsten für Output. print gibt 1 zurück, sodass es in Ausdrücken verwendet werden kann. printf/sprintf verwenden C-Stil-Format-Spezifizierer (%s String, %d Int, %f Float, %x Hex). var_dump() ist das primäre Debugging-Tool — zeigt Typen und Werte. error_log() schreibt in das PHP-Fehlerlog oder Syslog. In Produktion niemals Debug-Output an Benutzer exposeieren.

php
<?php
// echo: no return, multiple args (fastest)
echo "Hello", " ", "World";

// print: returns 1, single arg
print "Hello World";

// printf: formatted output
printf("Name: %s, Age: %d, Price: %.2f", "Alice", 30, 19.99);

// debugging output
print_r($array);           // human-readable
var_dump($variable);       // type + value (detailed)
var_export($array, true);  // valid PHP code (for caching)

// sprintf: return formatted string (don't print)
$log = sprintf("[%s] %s", date('H:i:s'), "Started");
error_log($log);           // log to error log

Operatoren & Vergleiche

Verwenden Sie immer === (strikter Vergleich), um Type-Coercion-Bugs zu vermeiden. == konvertiert Typen vor dem Vergleichen, was zu überraschenden Ergebnissen führt (0 == 'abc' war true in PHP 7). Der Spaceship-Operator (<=>) gibt -1/0/1 zurück — nützlich für usort. Der Null-Coalescing-Operator (??) ist die idiomatische Methode, Defaults zu提供. Der Null-Safe-Operator (?->) (PHP 8+) short-circuited Method-Chains auf null und ersetzt umständliche isset()-Checks.

php
<?php
// arithmetic
$sum = 10 + 3;       // 13
$mod = 10 % 3;       // 1
$pow = 2 ** 3;       // 8 (PHP 5.6+)

// comparison: == vs ===
echo (0 == "abc");   // true (loose, PHP 7-); false (PHP 8+)
echo (0 === "abc");  // false (strict — type + value)
echo ("1" == 1);     // true (loose)
echo ("1" === 1);    // false (strict)

// spaceship operator (PHP 7+)
echo 1 <=> 2;        // -1 (less than)
echo 2 <=> 2;        // 0 (equal)
echo 3 <=> 2;        // 1 (greater than)

// null coalescing
$name = $input ?? "default";    // if $input is null
$deep = $data['user']['name'] ?? "Anonymous";

// null safe operator (PHP 8+)
$country = $user?->getAddress()?->country; // null if any step is null

Superglobals & Web

Superglobals sind eingebaute assoziative Arrays, die in allen Scopes verfügbar sind. $_GET und $_POST enthalten Benutzereingaben — IMMER vor der Verwendung sanitisieren/validieren. filter_input() ist sicherer als direkter Zugriff. Vertrauen Sie niemals $_SERVER-Werten, die von Clients gespooft werden können (wie HTTP_USER_AGENT). Rufen Sie immer exit nach header('Location:') auf — PHP führt sonst weiter aus. Starten Sie Sessions mit session_start() vor jeglichem Output.

php
<?php
// superglobals available everywhere
$_GET['name'];           // query string params
$_POST['email'];         // form POST data
$_REQUEST['x'];          // GET + POST + COOKIE
$_SERVER['HTTP_HOST'];   // server/env info
$_SERVER['REQUEST_METHOD']; // GET, POST, etc.
$_COOKIE['session'];     // cookies
$_FILES['upload'];       // file uploads
$_SESSION['user_id'];    // session data (after session_start())

// get client IP
$ip = $_SERVER['REMOTE_ADDR'];

// check request method
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING);
}

// redirect
header("Location: /dashboard");
exit; // always exit after redirect

Include & Require

include/require führen die spezifizierte Datei aus. require verursacht einen fatalen Fehler, wenn die Datei fehlt (verwenden Sie für kritische Abhängigkeiten); include warnt nur (verwenden Sie für optionale Templates). Die _once-Varianten tracken inkludierte Dateien, um Doppel-Inklusion zu verhindern — unerlässlich für Funktions-/Klassen-Definitionen. Composers Autoloader (require_once 'vendor/autoload.php') eliminiert manuelles Include-Management. Dateien können Werte zurückgeben, was sie für Konfiguration nützlich macht.

php
<?php
// include: warning on failure, continues
include 'header.php';
include_once 'config.php'; // only once

// require: fatal error on failure, stops
require 'database.php';
require_once 'vendor/autoload.php'; // Composer autoload

// _once variants prevent re-declaration errors
// use require for critical files (DB config, autoloaders)
// use include for optional files (templates)

// return values from included files
$config = include 'config.php';
// config.php: return ['db' => 'mysql://...', 'debug' => true];
02

Strings

String-Funktionen

PHP hat 100+ String-Funktionen. strpos() gibt false zurück, wenn nicht gefunden — verwenden Sie === false zum Prüfen (0 ist eine gültige Position). str_replace() kann Arrays für Suche/Ersetzung akzeptieren. substr() unterstützt negative Offsets (vom Ende). Für Multibyte-Strings (UTF-8) verwenden Sie mb_*-Äquivalente (mb_strlen, mb_substr) — strlen zählt Bytes, nicht Zeichen. Setzen Sie immer default_charset='UTF-8' und verwenden Sie mb_*-Funktionen für nicht-ASCII-Text.

php
<?php
$s = "Hello, World";

// length and case
echo strlen($s);              // 12
echo str_word_count($s);      // 2
echo strtoupper($s);          // HELLO, WORLD
echo strtolower($s);          // hello, world
echo ucfirst("hello");        // Hello
echo ucwords("hello world");  // Hello World

// search and replace
echo strpos($s, "World");     // 7 (false if not found)
echo str_replace("o", "0", $s); // Hell0, W0rld
echo substr($s, 0, 5);        // Hello
echo substr($s, -5);          // World
echo strrev($s);              // dlroW ,olleH

// trim
echo trim("  hi  ");          // "hi"
echo ltrim("  hi");           // "hi"
echo rtrim("hi  ");           // "hi"

String-Interpolation & Heredoc

Doppelte Anführungszeichen interpolieren Variablen; einfache Anführungszeichen nicht (verwenden Sie einfache Anführungszeichen für literalen Text — minimal schneller). Verwenden Sie {$var} für komplexe Ausdrücke (Objekt-Properties, Array-Zugriff, Methoden-Aufrufe). Heredoc (<<<ID) ist ideal für Multi-Zeilen-Strings wie SQL oder HTML — es interpoliert Variablen. Nowdoc (<<<'ID') ist die nicht-interpretierende Version, nützlich für Regex-Patterns mit Backslashes.

php
<?php
$name = "Alice";
$age = 30;

// double quotes: variable interpolation
echo "Hello, $name!";          // Hello, Alice!
echo "Hello, {$name}!";        // Hello, Alice! (braces for clarity)
echo "Age: {$age}";            // Age: 30

// single quotes: no interpolation (faster)
echo 'Hello, $name';           // Hello, $name (literal)

// complex expressions need braces
echo "Result: {$obj->method()}";
echo "Item: {$array['key']}";

// heredoc: multi-line, interpolates
$sql = <<<SQL
    SELECT * FROM users
    WHERE name = '$name'
    AND age > $age
SQL;

// nowdoc: multi-line, NO interpolation (like single quotes)
$regex = <<<'REGEX'
    \d{3}-\d{4}
REGEX;

sprintf & Formatierung

sprintf() ist unerlässlich, um formatierte Strings sicher zu bauen — anders als String-Interpolation behandelt es Typ-Konvertierungen und Padding. Verwenden Sie %d für Integer (nicht %s), um numerische Formatierung sicherzustellen. number_format() formatiert Zahlen mit Tausender-Trennzeichen — kritisch für Währungs-Anzeige. Verwenden Sie sprintf immer für SQL-Fragmente in Prepared Statements (obwohl Prepared Statements für Benutzereingaben weiterhin erforderlich sind).

php
<?php
// sprintf: format string (returns, doesn't print)
$formatted = sprintf(
    "%-10s | %5d | %8.2f",
    "Alice", 42, 19.99
);
echo $formatted;  // "Alice      |    42 |    19.99"

// format specifiers:
// %s string, %d int, %f float, %x hex, %b binary, %c char
// %5d  right-padded to width 5
// %-5d left-padded
// %05d zero-padded
// %.2f 2 decimal places
// %8.2f width 8, 2 decimals

// numbered placeholders
echo sprintf("Hi %1$s, bye %1$s", "Alice"); // Hi Alice, bye Alice

// number_format
echo number_format(1234567.891, 2);  // 1,234,567.89
echo number_format(1234567.89, 2, ',', '.'); // European format

Regex (PCRE)

PHP verwendet PCRE (Perl-Compatible Regular Expressions) mit /pattern/-Begrenzern. preg_match gibt 1 zurück, wenn übereinstimmend, 0 wenn nicht (verwenden Sie ===, nicht ==, da 0 falsy ist). Validieren Sie immer Benutzereingaben mit Regex, aber verlassen Sie sich nicht allein darauf — verwenden Sie filter_var() für E-Mails, URLs. preg_replace ist mächtig, kann aber bei großen Strings langsam sein. Verwenden Sie [^...] zum Whitelisten von Zeichen statt Blacklisting. Das i-Flag macht Matching Groß-/Kleinschreibung-insensitiv.

php
<?php
// preg_match: test pattern (returns 0 or 1)
if (preg_match('/^[a-z]+$/i', "Hello")) {
    echo "alphabetic";
}

// capture groups
preg_match('/(d{4})-(d{2})-(d{2})/', "2024-01-15", $matches);
// $matches[0] = "2024-01-15" (full match)
// $matches[1] = "2024" (group 1)
// $matches[2] = "01"
// $matches[3] = "15"

// preg_match_all: all matches
preg_match_all('/d+/', "a1 b22 c333", $matches);
// $matches[0] = ["1", "22", "333"]

// preg_replace: substitute
$clean = preg_replace('/[^a-z0-9]/i', '', "Hello, World!"); // HelloWorld

// preg_split: split by pattern
$parts = preg_split('/[s,]+/', "one, two,three  four");
// ["one", "two", "three", "four"]

Multibyte & Encoding

PHPs Standard-String-Funktionen sind byte-orientiert, nicht zeichen-orientiert — sie brechen bei Multibyte-Zeichen (UTF-8, Chinesisch, Emoji). Verwenden Sie immer mb_*-Funktionen (mb_strlen, mb_substr, mb_strpos, mb_strtoupper) für nicht-ASCII-Text. Setzen Sie mb_internal_encoding('UTF-8') am Anfang Ihrer Anwendung. Verwenden Sie JSON_UNESCAPED_UNICODE, um Chinesisch/Emoji in JSON-Output lesbar zu halten. Dies ist eine häufige Fehlerquelle in internationalen Anwendungen.

php
<?php
// UTF-8 strings: use mb_* functions
$text = "Café";  // 4 chars, 5 bytes (é = 2 bytes)

echo strlen($text);      // 5 (bytes!)
echo mb_strlen($text);   // 4 (characters!)

echo substr($text, 0, 3);    // "Caf" (might break UTF-8!)
echo mb_substr($text, 0, 3); // "Caf" (safe)

// case conversion
echo strtoupper("straße");    // "STRAßE" (wrong!)
echo mb_strtoupper("straße"); // "STRAßE" (correct, locale-aware)

// encoding detection
$encoding = mb_detect_encoding($text);
$utf8 = mb_convert_encoding($text, 'UTF-8', 'auto');

// set internal encoding
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');

// JSON with UTF-8
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
03

Arrays

Indizierte & assoziative Arrays

PHP-Arrays sind eigentlich geordnete Hash-Maps — sie funktionieren als sowohl Listen als auch Dictionaries. Indizierte Arrays weisen automatisch numerische Keys zu; assoziative Arrays verwenden String-Keys. isset() gibt false für null-Werte zurück; array_key_exists() gibt true zurück, selbst für null. unset() entfernt ein Element, reindiziert aber nicht. Für eine echte Liste (ohne Lücken), verwenden Sie array_values() zum Reindizieren nach Löschung. PHP 8.1+ hat einen readonly-Array-Typ.

php
<?php
// indexed array (numeric keys)
$nums = [10, 20, 30];
$nums[] = 40;              // append
echo $nums[0];             // 10
echo count($nums);         // 4

// associative array (string keys)
$user = [
    "name" => "Alice",
    "age" => 30,
    "email" => "[email protected]",
];
echo $user["name"];        // Alice
$user["phone"] = "555-1234"; // add key

// mixed keys
$mixed = [0 => "a", "name" => "b", 5 => "c"];

// check key existence
if (isset($user["email"])) { /* ... */ }
if (array_key_exists("name", $user)) { /* ... */ }

// remove element
unset($user["phone"]);

Mehrdimensional & Iteration

Mehrdimensionale Arrays sind Arrays von Arrays. foreach ist die idiomatische Methode zu iterieren — es ist schneller und lesbarer als for-Schleifen. Verwenden Sie &$value, um Elemente in Place zu modifizieren (unsetzen Sie die Referenz immer nach der Schleife, um Bugs zu vermeiden). array_column() extrahiert eine einzelne Spalte aus einem 2D-Array — extrem nützlich zum Transformieren von Datenbank-Result-Sets. PHP-Arrays erhalten die Einfüge-Reihenfolge.

php
<?php
$users = [
    ["name" => "Alice", "age" => 30],
    ["name" => "Bob", "age" => 25],
    ["name" => "Carol", "age" => 35],
];

// iterate with key + value
foreach ($users as $index => $user) {
    echo "$index: {$user['name']} ({$user['age']})\n";
}

// modify by reference
foreach ($users as &$user) {
    $user['age'] += 1; // increment each age
}
unset($user); // break reference!

// nested iteration
$matrix = [[1, 2], [3, 4], [5, 6]];
foreach ($matrix as $row) {
    foreach ($row as $cell) {
        echo $cell . " ";
    }
    echo "\n";
}

// extract column
$names = array_column($users, 'name'); // ["Alice", "Bob", "Carol"]

Array-Funktionen: map, filter, reduce

array_map, array_filter und array_reduce sind das funktionale Programmier-Trio für Arrays. Arrow-Functions (fn() =>) machen diese prägnant. array_filter erhält Keys — verwenden Sie array_values() zum Reindizieren, wenn nötig. array_merge reindiziert numerische Keys, erhält aber String-Keys (spätere Werte überschreiben). array_column, array_chunk und array_slice sind unerlässlich für Daten-Manipulation. Diese Funktionen sind das Rückgrat der Daten-Verarbeitung in PHP.

php
<?php
$nums = [1, 2, 3, 4, 5];

// map: transform each element
$doubled = array_map(fn($n) => $n * 2, $nums);  // [2, 4, 6, 8, 10]

// filter: keep elements matching condition
$evens = array_filter($nums, fn($n) => $n % 2 === 0);  // [2, 4]

// reduce: accumulate to single value
$sum = array_reduce($nums, fn($carry, $n) => $carry + $n, 0);  // 15

// walk: like map but modifies in place (by reference)
array_walk($nums, fn(&$n) => $n *= 2);

// combining arrays
$merged = array_merge([1, 2], [3, 4]);        // [1, 2, 3, 4]
$combined = array_combine(['a', 'b'], [1, 2]); // ['a' => 1, 'b' => 2]
$sliced = array_slice($nums, 1, 2);            // [2, 3]
$chunked = array_chunk($nums, 2);              // [[1,2], [3,4], [5]]

Arrays sortieren

PHP-Sortier-Funktionen modifizieren das Array in Place (Pass-by-Reference). sort/rsort reindizieren; asort/arsort erhalten Keys. usort mit einer Vergleichs-Funktion (mit <=>) sortiert nach benutzerdefinierter Logik. natsort() macht natürliche Sortierung (img2 vor img10) — unerlässlich für Dateinamen. Für mehrdimensionale Arrays verwenden Sie usort mit einer Closure, die das gewünschte Feld vergleicht. Der Spaceship-Operator (<=>) vereinfacht Vergleichs-Funktionen.

php
<?php
$nums = [3, 1, 4, 1, 5, 9, 2, 6];

// sort by value (reindexes)
sort($nums);                          // [1, 1, 2, 3, 4, 5, 6, 9]
rsort($nums);                         // descending

// sort preserving keys
asort($nums);   // ascending, preserve keys
arsort($nums);  // descending, preserve keys

// sort by key
ksort($nums);   // by key ascending
krsort($nums);  // by key descending

// custom sort with callback
$users = [["name" => "Bob", "age" => 25], ["name" => "Alice", "age" => 30]];
usort($users, fn($a, $b) => $a['age'] <=> $b['age']);
// sorted by age ascending

// natural sort (for strings with numbers)
$files = ["img10.jpg", "img2.jpg", "img1.jpg"];
natsort($files); // ["img1.jpg", "img2.jpg", "img10.jpg"]
sort($files);    // ["img1.jpg", "img10.jpg", "img2.jpg"] (wrong!)

Array-Inspektion & Manipulation

in_array mit strict=true (dritter Parameter) verhindert Type-Coercion-Bugs. array_search gibt den Key zurück (verwenden Sie === false zum Prüfen). array_push/pop implementieren LIFO (Stack); array_shift/unshift implementieren FIFO (Queue) — aber shift ist O(n). Für große Queues verwenden Sie SplQueue oder SplDoublyLinkedList. array_unique erhält Keys. array_diff/intersect vergleichen Werte; verwenden Sie array_diff_key/intersect_key für Key-basierten Vergleich.

php
<?php
$arr = [1, 2, 3, 4, 5];

// inspection
echo count($arr);                  // 5
echo in_array(3, $arr);            // true
echo in_array("3", $arr, true);    // false (strict)
echo array_search(3, $arr);        // 2 (key, false if not found)
print_r(array_keys($arr));         // [0, 1, 2, 3, 4]
print_r(array_values($arr));       // [1, 2, 3, 4, 5]

// stack/queue operations
array_push($arr, 6);    // push to end
$last = array_pop($arr); // pop from end
$first = array_shift($arr); // remove from front
array_unshift($arr, 0); // add to front

// set operations
$unique = array_unique([1, 2, 2, 3]); // [1, 2, 3]
$diff = array_diff([1, 2, 3], [2, 3, 4]); // [1] (in first, not second)
$intersect = array_intersect([1, 2, 3], [2, 3, 4]); // [2, 3]

// flip and reverse
$flipped = array_flip(['a' => 1, 'b' => 2]); // [1 => 'a', 2 => 'b']
$reversed = array_reverse([1, 2, 3]); // [3, 2, 1]
04

Kontrollfluss

If / Else / Elseif

PHP verwendet elseif (ein Wort) — nicht 'else if' mit Leerzeichen (obwohl das auch funktioniert). Die alternative Syntax (if: ... endif;) ist nützlich in HTML-Templates, um Brace-Matching-Verwirrung zu vermeiden. Der ternäre Operator ist rechts-assoziativ — vermeiden Sie Verschachtelung. Der Null-Coalescing-Zuweisungs-Operator (??=) setzt einen Wert nur, wenn er aktuell null ist — perfekt für lazy Initialisierung von Config-Defaults.

php
<?php
$score = 85;

if ($score >= 90) {
    $grade = "A";
} elseif ($score >= 80) {
    $grade = "B";
} elseif ($score >= 70) {
    $grade = "C";
} else {
    $grade = "F";
}

// alternative syntax (for templates)
if ($score >= 90):
    echo "Excellent";
elseif ($score >= 80):
    echo "Good";
else:
    echo "Try harder";
endif;

// ternary
$status = $age >= 18 ? "adult" : "minor";

// null coalescing assignment (PHP 7.4+)
$config['timeout'] ??= 30; // set if not set

Switch & Match

switch verwendet losen Vergleich (==) und erfordert break, um Fall-Through zu verhindern — eine häufige Fehlerquelle. match (PHP 8+) verwendet strikten Vergleich (===), gibt direkt einen Wert zurück und wirft eine Exception, wenn kein Arm übereinstimmt (keine stillen Fehler). match ist der moderne Ersatz für switch, wenn Sie einen Wert benötigen. Verwenden Sie switch für komplexe Multi-Statement-Cases; verwenden Sie match für einfache Wert-Auswahl. Schließen Sie immer einen Default-Case ein.

php
<?php
// switch: loose comparison (==)
$day = "Mon";
switch ($day) {
    case "Mon":
    case "Tue":
    case "Wed":
        echo "Weekday";
        break;
    case "Sat":
    case "Sun":
        echo "Weekend";
        break;
    default:
        echo "Unknown";
}

// match (PHP 8+): strict comparison (===), returns value
$status = 404;
$message = match($status) {
    200, 201 => "Success",
    301, 302 => "Redirect",
    404 => "Not Found",
    500 => "Server Error",
    default => "Unknown",
};
// match throws UnhandledMatchError if no match and no default

Schleifen: for, while, foreach, do-while

foreach ist die idiomatische Schleife für Arrays — es ist schneller und sicherer als for mit count(). Verwenden Sie continue, um Iterationen zu überspringen, und break, um zu beenden. PHP hat kein labeled break/continue (im Gegensatz zu Java/Rust). Für assoziative Arrays ist foreach ($arr as $key => $value) das Standard-Muster. do-while läuft mindestens einmal — nützlich für Eingabe-Validierung. Vermeiden Sie das Modifizieren des Arrays während foreach (verwenden Sie ein separates Array für Ergebnisse).

php
<?php
// for loop
for ($i = 0; $i < 5; $i++) {
    echo $i;  // 01234
}

// foreach (most common in PHP)
$fruits = ["apple", "banana", "cherry"];
foreach ($fruits as $fruit) {
    echo $fruit;
}

// foreach with key
foreach ($fruits as $index => $fruit) {
    echo "$index: $fruit";
}

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

// do-while (runs at least once)
do {
    $line = readline("> ");
} while ($line !== "quit");

// break and continue
for ($i = 0; $i < 10; $i++) {
    if ($i === 3) continue; // skip 3
    if ($i === 7) break;    // stop at 7
    echo $i;
}

Kontrollfluss in Templates

PHPs alternative Kontroll-Syntax (if:/elseif:/else:/endif;, foreach:/endforeach;) ist für HTML-Templates entwickelt. <?= $var ?> ist die Kurzform für <?php echo $var; ?> — verwenden Sie es immer in Templates für Lesbarkeit. Escapen Sie immer Output mit htmlspecialchars(), um XSS zu verhindern. Die Trennung von PHP-Logik und HTML-Präsentation ist die Grundlage von Templating-Systemen wie Twig und Blade, die sauberere Syntax und automatisches Escaping bieten.

php
<?php // template file ?>
<?php if ($user->isAdmin()): ?>
    <div class="admin-panel">Admin Tools</div>
<?php elseif ($user->isEditor()): ?>
    <div class="editor-tools">Edit Tools</div>
<?php else: ?>
    <div class="user-view">Read Only</div>
<?php endif; ?>

<?php foreach ($products as $p): ?>
    <div class="product">
        <?= htmlspecialchars($p['name']) ?>
        - $<?= number_format($p['price'], 2) ?>
    </div>
<?php endforeach; ?>

<?php // shorthand echo ?>
<h1><?= $title ?></h1>

<?php // ternary in templates ?>
<span class="<?= $active ? 'on' : 'off' ?>"><?= $active ? 'Active' : 'Inactive' ?></span>

Exceptions & Fehlerbehandlung

PHP 7+ verwendet Exceptions für die meisten Fehler. Fangen Sie immer spezifische Exception-Typen (nicht nur Exception), um verschiedene Fehler angemessen zu behandeln. finally führt immer aus — verwenden Sie es für Cleanup (Dateien schließen, Verbindungen). Custom Exceptions erweitern Exception und fügen Domain-Kontext hinzu. PHP 8+ erlaubt das Fangen mehrerer Exception-Typen mit |. Setzen Sie PDO auf Exception-Modus für konsistente Fehlerbehandlung. Fangen Sie niemals Exceptions ohne Logging — stille Fehler verbergen Bugs.

php
<?php
// try / catch / finally
try {
    $pdo = new PDO("mysql:host=localhost;dbname=test", "user", "pass");
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    error_log("DB connection failed: " . $e->getMessage());
    die("Service unavailable");
} finally {
    // always runs, even after return/throw
    echo "Cleanup";
}

// custom exception
class ValidationException extends Exception {
    public function __construct(string $field, string $message = "") {
        parent::__construct("$field: $message");
    }
}

// throw
if (empty($email)) {
    throw new ValidationException("email", "is required");
}

// catch multiple types (PHP 8+)
try {
    riskyOperation();
} catch (PDOException | RuntimeException $e) {
    // catches either type
    log_error($e);
}
05

Funktionen

Funktionen definieren

PHP 7+ unterstützt typisierte Parameter und Rückgabe-Typen (int, string, array, ?Type für nullable). PHP 8+ fügt benannte Argumente (Defaults überspringen, Parameter neu ordnen), Union-Types (int|string) und mixed-Type hinzu. Variadische Parameter (...$nums) sammeln zusätzliche Argumente in einem Array. Der Spread-Operator (...$arr) entpackt ein Array als Argumente. Pass-by-Reference (&) modifiziert das Original — verwenden Sie es sparsam, da es Code schwerer nachvollziehbar macht.

php
<?php
// basic function with return type
function add(int $a, int $b): int {
    return $a + $b;
}

// default parameters
function greet(string $name, string $greeting = "Hello"): string {
    return "$greeting, $name!";
}

// named arguments (PHP 8+)
echo greet(name: "Alice", greeting: "Hi");

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

// spread operator
$nums = [1, 2, 3];
echo sum(...$nums);  // 6

// pass by reference
function increment(int &$n): void {
    $n++;
}
$x = 5;
increment($x);
echo $x;  // 6

Arrow-Functions & Closures

Arrow-Functions (fn() =>) sind prägnante, einzeilige Closures, die automatisch äußere Variablen per Wert erfassen. Traditionelle Closures (function() use ($var)) werden für Multi-Zeilen-Bodies oder Erfassen per Referenz (&$var) benötigt. Closures sind unerlässlich für array_map, array_filter, usort und Event-Handler. Arrow-Functions können keine Statements haben (kein if, for) — verwenden Sie traditionelle Closures für komplexe Logik. Closures sind First-Class-Objekte (Closure-Klasse).

php
<?php
// arrow function (PHP 7.4+): single expression, auto-capture
$square = fn($x) => $x * $x;
echo $square(5);  // 25

// auto-captures outer variables by value
$multiplier = 3;
$multiply = fn($x) => $x * $multiplier;
echo $multiply(5);  // 15

// traditional closure (multi-line, explicit capture)
$factor = 10;
$scale = function ($x) use ($factor) {
    return $x * $factor + 1;
};

// capture by reference
$count = 0;
$increment = function () use (&$count) {
    $count++;
};
$increment();
echo $count;  // 1

// closures as callbacks
$nums = [1, 2, 3, 4];
$evens = array_filter($nums, fn($n) => $n % 2 === 0);
$doubled = array_map(fn($n) => $n * 2, $nums);

Variablen-Scope & Globals

PHP hat Funktions-Level-Scope — Variablen, die außerhalb einer Funktion definiert sind, sind NICHT ohne 'global' oder $GLOBALS zugänglich. Vermeiden Sie 'global' — es erstellt versteckte Abhängigkeiten und macht Testing schwer. Verwenden Sie stattdessen Dependency Injection. static-Variablen persistieren über Funktions-Aufrufe, sind aber auf die Funktion scope — nützlich für Caching/Memoization, kann aber in langlaufenden Prozessen Probleme verursachen. Closures müssen Variablen explizit mit 'use' erfassen.

php
<?php
$global = "I'm global";

function testScope(): void {
    // echo $global; // ERROR: not in scope
    global $global;  // import global
    echo $global;    // OK

    // $GLOBALS superglobal (alternative)
    echo $GLOBALS['global'];
}

// static variables: persist across calls
function counter(): int {
    static $count = 0;
    return ++$count;
}
echo counter();  // 1
echo counter();  // 2
echo counter();  // 3

// closures don't see outer scope by default
$outer = "hello";
$closure = function () {
    // echo $outer; // ERROR
};
$closure2 = function () use ($outer) {
    echo $outer;  // OK, captured
};

Typ-Deklarationen & Strict Types

declare(strict_types=1) muss die erste Anweisung sein — es erzwingt strikte Typ-Prüfung (keine Coercion) für die gesamte Datei. Ohne es coercet PHP Typen (int 5, das an einen String-Param übergeben wird, wird zu '5'). Verwenden Sie immer Strict Types in neuem Code. PHP 8+ fügt Union-Types, mixed, never (Funktion gibt nie zurück) und static (gibt die Klasse zurück) hinzu. First-Class-Callable-Syntax (func(...)) erstellt Closures aus jedem Callable — sauberer als Funktions-Referenzen.

php
<?php
// declare strict types (FIRST line of file)
declare(strict_types=1);

function divide(float $a, float $b): float {
    if ($b === 0.0) {
        throw new DivisionByZeroError();
    }
    return $a / $b;
}

// nullable types (?Type or Type|null)
function findUser(?int $id): ?string {
    if ($id === null) return null;
    return "User $id";
}

// union types (PHP 8+)
function process(int|string $input): int|string {
    return is_int($input) ? $input * 2 : strtoupper($input);
}

// return types: void, never, mixed, static
function log(string $msg): void { /* no return */ }
function redirect(): never { header("Location: /"); exit; }

// first-class callable syntax (PHP 8.1+)
$func = strlen(...);  // creates Closure from function
echo $func("hello");  // 5

Generatoren & Yield

Generatoren (Funktionen mit yield) produzieren Werte lazy — sie berechnen nicht alle Werte vorab, was Speicher spart. Dies ist unerlässlich für das Verarbeiten großer Dateien oder Datensätze. yield pausiert die Funktion und gibt einen Wert zurück; die Funktion setzt fort, wenn der nächste Wert angefordert wird. Generatoren implementieren Iterator, sodass sie mit foreach funktionieren. Verwenden Sie Generatoren für: Datei-Verarbeitung, Datenbank-Zeilen-Iteration, unendliche Sequenzen und Pipelines. Sie können auch Key=>Value-Paare yielden und Werte via send() akzeptieren.

php
<?php
// generator: memory-efficient iteration
function readLines(string $file): Generator {
    $handle = fopen($file, 'r');
    while (($line = fgets($handle)) !== false) {
        yield trim($line);
    }
    fclose($handle);
}

foreach (readLines("large.txt") as $line) {
    echo $line;  // one line at a time, low memory
}

// infinite generator
function fibonacci(): Generator {
    [$a, $b] = [0, 1];
    while (true) {
        yield $a;
        [$a, $b] = [$b, $a + $b];
    }
}

// take first 10
$fib = fibonacci();
for ($i = 0; $i < 10; $i++) {
    echo $fib->current() . " ";
    $fib->next();
}

// yield with key
function pairs(): Generator {
    yield 'a' => 1;
    yield 'b' => 2;
    yield 'c' => 3;
}
06

OOP & Klassen

Klasse, Properties & Konstruktor

PHP 8 Constructor Promotion eliminiert Boilerplate — deklarieren Sie Properties als Konstruktor-Parameter. Property-Visibility: public (überall), protected (Klasse + Subklassen), private (nur Klasse). readonly (PHP 8.1) verhindert Modifikation nach Initialisierung. self referenziert die aktuelle Klasse; static referenziert die aufrufende Klasse (für late static binding). Verwenden Sie static:: statt self:: in Vererbungs-Hierarchien für korrektes Polymorphismus.

php
<?php
class Person {
    // typed properties (PHP 7.4+)
    public string $name;
    protected int $age;
    private string $email;
    public readonly string $id;  // PHP 8.1+

    // constructor promotion (PHP 8+)
    public function __construct(
        string $name,
        int $age,
        string $email = "",
        string $id = ""
    ) {
        $this->name = $name;
        $this->age = $age;
        $this->email = $email;
        $this->id = $id;
    }

    // methods
    public function greet(): string {
        return "Hi, I'm {$this->name}";
    }

    // static method
    public static function create(string $name): self {
        return new self($name, 0);
    }
}

$p = new Person("Alice", 30);
echo $p->greet();
echo Person::create("Bob")->name;

Vererbung & Abstrakte Klassen

Abstrakte Klassen können nicht instanziiert werden — sie definieren eine Vorlage für Subklassen. Abstrakte Methoden müssen von konkreten Subklassen implementiert werden. PHP unterstützt nur Einfachvererbung (ein extends). Verwenden Sie final, um Vererbung/Überschreiben zu verhindern, wenn die Implementierung nicht geändert werden sollte. protected-Mitglieder sind in Subklassen zugänglich — verwenden Sie für interne APIs. Rufen Sie immer parent::__construct() auf, wenn der Parent einen Konstruktor hat. instanceof prüft den Typ: if ($dog instanceof Animal).

php
<?php
abstract class Animal {
    protected string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    // abstract method: must be implemented by subclasses
    abstract public function speak(): string;

    // concrete method: inherited as-is
    public function describe(): string {
        return "{$this->name} says {$this->speak()}";
    }
}

class Dog extends Animal {
    public function speak(): string {
        return "Woof";
    }
}

class Cat extends Animal {
    public function speak(): string {
        return "Meow";
    }
}

$dog = new Dog("Rex");
echo $dog->describe();  // Rex says Woof

// final class/method: cannot be extended/overridden
final class Singleton { /* ... */ }

Interfaces & Traits

Interfaces definieren Verträge — Klassen können mehrere Interfaces implementieren (im Gegensatz zu Einfachvererbung). Alle Interface-Methoden müssen public sein. Traits bieten Code-Wiederverwendung ohne Vererbung — sie sind 'Copy-Paste' auf Sprachebene. Traits können Properties, Methoden und sogar abstrakte Methoden haben. Verwenden Sie Traits für Querschnittsanliegen (Timestamps, Logging, Soft Deletes). Konfliktlösung: verwenden Sie TraitA::method insteadof TraitB, wenn Traits gleiche Methodennamen haben.

php
<?php
// interface: contract (no implementation)
interface Comparable {
    public function compareTo(object $other): int;
}

interface JsonSerializable {
    public function jsonSerialize(): mixed;
}

// a class can implement multiple interfaces
class Product implements Comparable, JsonSerializable {
    public function __construct(public float $price) {}

    public function compareTo(object $other): int {
        return $this->price <=> $other->price;
    }

    public function jsonSerialize(): mixed {
        return ['price' => $this->price];
    }
}

// trait: reusable code (horizontal reuse)
trait Timestampable {
    public DateTime $createdAt;

    public function setCreatedAt(): void {
        $this->createdAt = new DateTime();
    }

    public function age(): DateInterval {
        return $this->createdAt->diff(new DateTime());
    }
}

class Article {
    use Timestampable; // use trait
}

Magic Methods

Magic Methods sind spezielle Methoden, die Objekt-Operationen abfangen. __get/__set implementieren Property-Overloading (dynamische Properties). __toString ermöglicht String-Casting. __invoke macht Objekte callable. __clone läuft beim Klonen (clone $obj). Verwenden Sie sie sparsam — sie fügen 'Magie' hinzu, die schwer zu tracen ist. __get/__set sind nützlich für Data Transfer Objects oder lazy Loading. Dokumentieren Sie Magic-Verhalten immer klar. PHP 8.2 deprecates dynamische Properties — verwenden Sie __get/__set oder #[AllowDynamicProperties].

php
<?php
class Magic {
    private array $data = [];

    // called when accessing undefined property
    public function __get(string $name): mixed {
        return $this->data[$name] ?? null;
    }

    // called when setting undefined property
    public function __set(string $name, mixed $value): void {
        $this->data[$name] = $value;
    }

    // called when isset() or empty() on undefined property
    public function __isset(string $name): bool {
        return isset($this->data[$name]);
    }

    // called when object is used as string
    public function __toString(): string {
        return json_encode($this->data);
    }

    // called when object is called as function
    public function __invoke(string $arg): string {
        return "Called with: $arg";
    }

    // called on clone
    public function __clone(): void {
        $this->data = []; // reset on clone
    }
}

$m = new Magic();
$m->foo = "bar";       // __set
echo $m->foo;          // __get -> "bar"
echo $m;               // __toString -> {"foo":"bar"}
echo $m("test");       // __invoke

Namespaces & Autoloading

Namespaces verhindern Klassen-Namens-Kollisionen — wie Packages in Java. Der Namespace muss die erste Anweisung sein. use importiert Klassen (mit optionalen Aliassen: use Foo\Bar as B). PSR-4-Autoloading bildet Namespaces auf Dateipfade ab: App\Models\User → src/Models/User.php. Composers Autoloader (require 'vendor/autoload.php') handhabt dies automatisch. Verwenden Sie immer Namespaces in modernem PHP. Der \\ in Strings ist ein escapeter Backslash (Namespace-Separator).

php
<?php
// file: src/Models/User.php
namespace App\Models;

use App\Database\Connection;
use App\Exceptions\UserNotFoundException;

class User {
    private Connection $db;

    public function __construct(Connection $db) {
        $this->db = $db;
    }

    public function find(int $id): ?self {
        // ...
        throw new UserNotFoundException("User $id not found");
    }
}

// composer.json (PSR-4 autoloading)
// {
//   "autoload": {
//     "psr-4": { "App\\": "src/" }
//   }
// }

// usage
use App\Models\User;
$user = new User($db);
07

Web, Formulare & Datei-I/O

Formular-Behandlung & Validierung

Validieren Sie immer serverseitig — clientseitige Validierung ist für UX, nicht Security. filter_input/filter_var mit FILTER_VALIDATE_* geben false bei ungültiger Eingabe zurück. Trimmen Sie Strings vor der Validierung. Verwenden Sie Prepared Statements für Datenbank-Inserts. CSRF-Tokens verhindern Cross-Site Request Forgery — generieren Sie pro Session und verifizieren Sie bei POST. bin2hex(random_bytes(32)) generiert ein kryptografisch sicheres Token. Vertrauen Sie niemals Benutzereingaben — validieren, sanitisieren und escapen.

php
<?php
// process POST form
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = trim($_POST['name'] ?? '');
    $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
    $age = filter_var($_POST['age'] ?? 0, FILTER_VALIDATE_INT);

    $errors = [];

    if (empty($name)) {
        $errors[] = "Name is required";
    } elseif (strlen($name) > 100) {
        $errors[] = "Name too long";
    }

    if ($email === false) {
        $errors[] = "Valid email required";
    }

    if ($age === false || $age < 18) {
        $errors[] = "Must be 18+";
    }

    if (empty($errors)) {
        // process valid data
        // save to database, redirect, etc.
        header("Location: /success");
        exit;
    }
}

// CSRF token
session_start();
$token = bin2hex(random_bytes(32));
$_SESSION['csrf'] = $token;
// in form: <input type="hidden" name="csrf" value="<?= $token ?>">

Sessions & Cookies

Sessions speichern Daten serverseitig (identifiziert durch ein Session-ID-Cookie). session_start() muss vor jeglichem Output aufgerufen werden (oder verwenden Sie ob_start()). Speichern Sie minimale Daten in Sessions — sie verbrauchen Server-Speicher. Für Cookies setzen Sie immer secure (nur HTTPS), httponly (XSS-Zugriff verhindern) und samesite (CSRF-Schutz). Zerstören Sie Sessions ordnungsgemäß: Variablen unsetzen, Session zerstören, Cookie löschen. Für skalierbare Apps verwenden Sie einen Session-Handler, der auf Redis/Datenbank statt Dateien basiert.

php
<?php
// start session (must be before any output)
session_start();

// set session data
$_SESSION['user_id'] = 42;
$_SESSION['username'] = "Alice";
$_SESSION['login_time'] = time();

// read session data
$userId = $_SESSION['user_id'] ?? null;

// check if logged in
function isLoggedIn(): bool {
    return isset($_SESSION['user_id']);
}

// destroy session
session_unset();     // clear variables
session_destroy();   // destroy session
setcookie(session_name(), '', time() - 3600, '/'); // clear cookie

// cookies
setcookie("theme", "dark", [
    'expires' => time() + 86400 * 30, // 30 days
    'path' => '/',
    'secure' => true,     // HTTPS only
    'httponly' => true,   // not accessible via JS
    'samesite' => 'Strict', // CSRF protection
]);

// read cookie
$theme = $_COOKIE['theme'] ?? 'light';

Datei-I/O

file_get_contents/file_put_contents sind bequem für kleine Dateien. Für große Dateien verwenden Sie fopen/fread/fwrite mit Streams. fgetcsv/fputcsv handhaben CSV-Format (inklusive Quoting/Escaping). json_decode mit true gibt assoziative Arrays zurück (standardmäßig Objekte). Prüfen Sie immer file_exists und behandeln Sie Fehler (Berechtigungen, volle Festplatte). Für Datei-Uploads verwenden Sie move_uploaded_file() für Security. Locken Sie Dateien mit flock() beim gleichzeitigen Schreiben.

php
<?php
// read entire file
$content = file_get_contents("data.txt");
$lines = file("data.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

// write file
file_put_contents("output.txt", "Hello World");
file_put_contents("log.txt", "entry\n", FILE_APPEND); // append

// CSV
$csv = fopen("data.csv", "r");
while (($row = fgetcsv($csv)) !== false) {
    print_r($row); // array of column values
}
fclose($csv);

// write CSV
$out = fopen("export.csv", "w");
fputcsv($out, ["Name", "Email", "Age"]);
fputcsv($out, ["Alice", "[email protected]", 30]);
fclose($out);

// JSON
$data = json_decode(file_get_contents("config.json"), true); // assoc array
file_put_contents("config.json", json_encode($data, JSON_PRETTY_PRINT));

// file info
file_exists("data.txt");  // bool
filesize("data.txt");     // bytes
filemtime("data.txt");    // modification timestamp
is_dir("folder");         // bool

Datei-Uploads

Datei-Uploads kommen über $_FILES, nicht $_POST. Validieren Sie immer: Error-Code prüfen, MIME-Typ mit finfo verifizieren (nicht $_FILES['type'], das client-bereitgestellt und spooftbar ist), Größen-Limits durchsetzen und sichere Dateinamen generieren (niemals dem Original-Namen vertrauen). move_uploaded_file() ist eine Security-Funktion — sie verifiziert, dass die Datei via HTTP POST hochgeladen wurde. Speichern Sie Uploads außerhalb des Web-Roots oder serven Sie sie durch PHP, um direkten Zugriff zu verhindern. Ziehen Sie in Betracht, Uploads auf Malware zu scannen.

php
<?php
// HTML: <form method="POST" enctype="multipart/form-data">
//   <input type="file" name="document">
//   <input type="submit">
// </form>

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $file = $_FILES['document'];

    // $file contains:
    // ['name'] => original filename
    // ['type'] => MIME type (unreliable!)
    // ['tmp_name'] => temporary path
    // ['error'] => UPLOAD_ERR_OK (0) on success
    // ['size'] => file size in bytes

    if ($file['error'] !== UPLOAD_ERR_OK) {
        die("Upload error: " . $file['error']);
    }

    // validate
    $allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mimeType = $finfo->file($file['tmp_name']);

    if (!in_array($mimeType, $allowedTypes)) {
        die("Invalid file type");
    }

    if ($file['size'] > 5 * 1024 * 1024) { // 5MB
        die("File too large");
    }

    // move to permanent location
    $dest = "uploads/" . uniqid() . "_" . $file['name'];
    move_uploaded_file($file['tmp_name'], $dest);
    echo "Uploaded to: $dest";
}

cURL & HTTP-Requests

cURL ist der Standard-HTTP-Client in PHP — es handhabt HTTPS, Weiterleitungen, Cookies und Authentifizierung. Setzen Sie immer CURLOPT_RETURNTRANSFER, um die Antwort als String zu erhalten (sonst wird sie gedruckt). Setzen Sie Timeouts, um Hängen zu vermeiden. Für einfache Requests funktioniert file_get_contents mit stream_context, aber es fehlen Features. Für Produktion verwenden Sie Guzzle (composer require guzzlehttp/guzzle) oder Symfony HTTP Client — sie bieten bessere APIs, Retry-Logik und PSR-18-Compliance.

php
<?php
// GET request
$ch = curl_init("https://api.example.com/users");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode === 200) {
    $data = json_decode($response, true);
    print_r($data);
}

// POST request with JSON
$ch = curl_init("https://api.example.com/users");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode(['name' => 'Alice']),
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $token,
    ],
]);
$response = curl_exec($ch);
curl_close($ch);

// simpler: file_get_contents with context
$context = stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/json\r\n",
        'content' => json_encode(['name' => 'Alice']),
    ],
]);
$result = file_get_contents("https://api.example.com/users", false, $context);
08

Datenbank (PDO)

PDO-Verbindung & Grundlagen

PDO (PHP Data Objects) ist die Standard-Datenbank-Abstraktions-Schicht — unterstützt MySQL, PostgreSQL, SQLite und mehr. Setzen Sie immer ERRMODE_EXCEPTION für korrekte Fehlerbehandlung und ATTR_EMULATE_PREPARES=false für echte Prepared Statements (bessere Security). FETCH_ASSOC gibt assoziative Arrays zurück (verwenden Sie FETCH_OBJ für Objekte, FETCH_CLASS zum Mappen auf Klassen). Verwenden Sie immer utf8mb4-Charset für vollständigen Unicode-Support (inklusive Emoji). Speichern Sie die Verbindung in einem Singleton oder DI-Container.

php
<?php
// connect (always use exception mode)
$dsn = "mysql:host=localhost;dbname=test;charset=utf8mb4";
$pdo = new PDO($dsn, "username", "password", [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false, // use real prepared statements
]);

// simple query
$stmt = $pdo->query("SELECT * FROM users LIMIT 5");
$users = $stmt->fetchAll(); // array of associative arrays

// fetch one row
$user = $pdo->query("SELECT * FROM users WHERE id = 1")->fetch();

// fetch column
$count = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();

// execute (no params)
$pdo->exec("DELETE FROM logs WHERE created_at < '2023-01-01'");
$deleted = $pdo->rowCount(); // affected rows

Prepared Statements (SQL-Injection-Prävention)

Prepared Statements sind PFLICHT für jede Query mit Benutzereingaben — sie trennen SQL-Struktur von Daten und machen Injektion unmöglich. Verwenden Sie ? für positionale oder :name für benannte Parameter. Für IN-Klauseln müssen Sie die Placeholder-String dynamisch bauen (aber Werte sind weiterhin parametrisiert). lastInsertId() gibt den letzten Auto-Increment-Wert zurück. Konkatenieren Sie niemals Benutzereingaben in SQL — selbst mit Escaping-Funktionen. Dies ist die #1 Security-Regel in PHP.

php
<?php
// prepared statements: THE way to prevent SQL injection
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ? AND status = ?");
$stmt->execute([$email, 'active']);
$user = $stmt->fetch();

// named parameters (more readable)
$stmt = $pdo->prepare(
    "INSERT INTO users (name, email, age) VALUES (:name, :email, :age)"
);
$stmt->execute([
    ':name' => 'Alice',
    ':email' => '[email protected]',
    ':age' => 30,
]);
$id = $pdo->lastInsertId(); // get auto-increment ID

// IN clause with prepared statements
$ids = [1, 2, 3, 4];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);
$users = $stmt->fetchAll();

// NEVER do this (SQL injection!):
// $pdo->query("SELECT * FROM users WHERE name = '$_GET[name]'");

Transaktionen & Fehlerbehandlung

Transaktionen stellen Atomizität sicher — alle Operationen erfolgreich oder alle fehlschlagen. beginTransaction/commit/rollBack umschließen die Arbeitseinheit. Umschließen Sie Transaktionen immer in try/catch und rollen Sie bei jeder Exception zurück. PDO wirft PDOException bei Fehlern (mit ERRMODE_EXCEPTION). Halten Sie Transaktionen kurz, um Lock-Contention zu reduzieren. Für verschachtelte Transaktionen verwenden Sie Savepoints oder einen Transaktions-Manager. Lassen Sie niemals eine Transaktion offen — committen oder immer zurückrollen.

php
<?php
try {
    $pdo->beginTransaction();

    $pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ?")
        ->execute([$amount, $fromId]);

    $pdo->prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?")
        ->execute([$amount, $toId]);

    // log transaction
    $pdo->prepare("INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)")
        ->execute([$fromId, $toId, $amount]);

    $pdo->commit();
    echo "Transfer complete";
} catch (PDOException $e) {
    $pdo->rollBack(); // undo all changes
    error_log("Transfer failed: " . $e->getMessage());
    throw new RuntimeException("Transfer failed", 0, $e);
}

// check if in transaction
if ($pdo->inTransaction()) {
    $pdo->commit();
}

Daten-Abruf-Muster

Wählen Sie den richtigen Fetch-Modus für Ihren Anwendungsfall. FETCH_ASSOC ist am häufigsten (Array mit Spaltennamen). FETCH_CLASS mappt Zeilen auf Objekte — großartig für Domain-Modelle. FETCH_KEY_PAIR erstellt id=>value-Maps (für Dropdowns). FETCH_GROUP gruppiert Zeilen nach der ersten Spalte — nützlich für One-to-Many-Beziehungen. Für große Result-Sets verwenden Sie fetch() in einer Schleife statt fetchAll(), um Speicher zu sparen. Schließen Sie Cursor immer mit $stmt->closeCursor(), wenn Sie fertig sind.

php
<?php
$stmt = $pdo->prepare("SELECT id, name, email FROM users WHERE active = 1");
$stmt->execute();

// fetch modes
$row = $stmt->fetch(PDO::FETCH_ASSOC);  // ['id' => 1, 'name' => 'Alice']
$row = $stmt->fetch(PDO::FETCH_NUM);    // [1, 'Alice', '[email protected]']
$row = $stmt->fetch(PDO::FETCH_BOTH);   // both (default)
$obj = $stmt->fetch(PDO::FETCH_OBJ);    // stdClass with properties

// fetch all
$all = $stmt->fetchAll(PDO::FETCH_ASSOC);

// fetch into class
class User {
    public int $id;
    public string $name;
}
$stmt->setFetchMode(PDO::FETCH_CLASS, User::class);
$users = $stmt->fetchAll(); // array of User objects

// fetch key-value pairs
$pairs = $pdo->query("SELECT id, name FROM users")
    ->fetchAll(PDO::FETCH_KEY_PAIR); // [1 => 'Alice', 2 => 'Bob']

// fetch grouped
$grouped = $pdo->query("SELECT dept, name FROM employees")
    ->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC);
// ['IT' => [['name' => 'Alice']], 'HR' => [['name' => 'Bob']]]

Datenbank-Best Practices

Das Repository-Pattern trennt Daten-Zugang von Business-Logik — macht Code testbar (PDO mocken) und wartbar. Verwenden Sie Dependency Injection, um die PDO-Verbindung zu übergeben. Erstellen Sie niemals neue PDO-Verbindungen pro Query — verwenden Sie eine einzelne Verbindung (oder Pool) wieder. Für High-Traffic-Apps ziehen Sie einen Connection Pooler in Betracht (ProxySQL für MySQL, PgBouncer für PostgreSQL). Profilieren Sie immer langsame Queries mit EXPLAIN und fügen Sie passende Indexes hinzu. Ziehen Sie ein ORM (Doctrine, Eloquent) für komplexe Domänen in Betracht.

php
<?php
// 1. Connection singleton (or use DI container)
class Database {
    private static ?PDO $instance = null;

    public static function conn(): PDO {
        if (self::$instance === null) {
            self::$instance = new PDO(
                "mysql:host=localhost;dbname=app;charset=utf8mb4",
                "user", "pass",
                [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
            );
        }
        return self::$instance;
    }
}

// 2. Repository pattern
class UserRepository {
    public function __construct(private PDO $db) {}

    public function findById(int $id): ?array {
        $stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
        $stmt->execute([$id]);
        $user = $stmt->fetch();
        return $user ?: null;
    }
}

// 3. Always close statements (or let them go out of scope)
// 4. Use LIMIT for queries that might return huge results
// 5. Index columns used in WHERE, JOIN, ORDER BY
// 6. Use EXPLAIN to analyze slow queries
09

Datum/Zeit & Security

Datum & Zeit

PHPs Datum-Funktionen verwenden standardmäßig die Zeitzone des Servers — setzen Sie immer date_default_timezone_set('Asia/Shanghai') oder verwenden Sie DateTimeZone explizit. Die DateTime-Klasse ist objektorientiert und handhabt Zeitzonen, Intervalle und Formatierung besser als prozedurale Funktionen. strtotime() parst englische Datums-Beschreibungen ('next Monday', '+1 month') — bequem, kann aber an Monatsgrenzen überraschen. Für Datum-Mathe verwenden Sie DateTime::diff() und DateInterval. Speichern Sie Daten immer in UTC in Datenbanken.

php
<?php
// current time
$now = date('Y-m-d H:i:s');           // 2024-06-15 14:30:00
$timestamp = time();                   // Unix timestamp
$dt = new DateTime();                  // DateTime object

// formatting
echo date('Y-m-d');                    // 2024-06-15
echo date('d/m/Y H:i:s');              // 15/06/2024 14:30:00
echo date('l, F j, Y');                // Saturday, June 15, 2024

// create from string
$dt = new DateTime('2024-01-15');
$dt = DateTime::createFromFormat('d/m/Y', '15/01/2024');

// modify dates
$dt->modify('+1 month');
$dt->modify('-2 days');
$tomorrow = date('Y-m-d', strtotime('tomorrow'));
$nextWeek = date('Y-m-d', strtotime('+1 week'));

// difference
$start = new DateTime('2024-01-01');
$end = new DateTime('2024-12-31');
$diff = $start->diff($end);
echo $diff->days;  // 365

// timezone
$dt = new DateTime('now', new DateTimeZone('Asia/Shanghai'));
$dt->setTimezone(new DateTimeZone('UTC'));

Passwort-Hashing & Security

password_hash() verwendet bcrypt (oder Argon2, wenn verfügbar) mit automatischer Salt-Generierung — rollen Sie niemals Ihr eigenes Hashing. password_verify() prüft Passwörter sicher gegen Hashes (Constant-Time-Vergleich, um Timing-Angriffe zu verhindern). password_needs_rehash() lässt Sie Hashes upgraden, wenn Sie den Cost-Faktor erhöhen. Für zufällige Tokens (CSRF, API-Keys, Passwort-Resets) verwenden Sie immer random_bytes() — nicht rand() oder mt_rand(), die vorhersagbar sind. Verwenden Sie hash_hmac für Nachrichten-Authentifizierung.

php
<?php
// hash password (bcrypt by default)
$hash = password_hash("mypassword", PASSWORD_DEFAULT);
// $2y$10$... (includes algorithm, cost, salt)

// verify password
if (password_verify($input, $hash)) {
    echo "Valid password";
}

// check if hash needs rehash (algorithm upgrade)
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $newHash = password_hash($input, PASSWORD_DEFAULT);
    // update stored hash
}

// NEVER use md5() or sha1() for passwords!
// NEVER store plaintext passwords!

// generate secure random
$token = bin2hex(random_bytes(32));    // 64-char hex string
$bytes = random_bytes(16);             // raw bytes
$int = random_int(1, 1000);            // cryptographically secure

// hash for integrity (not passwords)
$checksum = hash('sha256', $data);
$hmac = hash_hmac('sha256', $data, $secretKey);

Output-Escaping & XSS-Prävention

XSS ist die #1 Web-Schwachstelle — escapen Sie immer Output basierend auf Kontext. htmlspecialchars() für HTML (ENT_QUOTES escapet sowohl einfache als auch doppelte Anführungszeichen). urlencode() für URLs. json_encode() mit Hex-Flags für JavaScript-Kontexte. Vertrauen Sie niemals Benutzereingaben — escapen Sie bei Output, nicht bei Input (Sie benötigen die rohen Daten möglicherweise woanders). Setzen Sie Content-Security-Policy-Header als Defense-in-Depth. Ziehen Sie eine Template-Engine (Twig, Blade) in Betracht, die standardmäßig auto-escapet.

php
<?php
// XSS: Cross-Site Scripting — always escape output!
$name = $_GET['name']; // could be: <script>alert('xss')</script>

// HTML context
echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
// converts < > " ' & to HTML entities

// in HTML template
?>
<p>Hello, <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></p>
<input value="<?= htmlspecialchars($value, ENT_QUOTES, 'UTF-8') ?>">

<?php
// URL context
$url = "https://example.com/search?q=" . urlencode($query);

// JavaScript context
$json = json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT);
?>
<script>
    var data = <?= $json ?>;
</script>

<?php
// Content Security Policy header
header("Content-Security-Policy: default-src 'self'; script-src 'self'");

JSON & API-Antworten

json_encode/decode sind die Standard-JSON-Funktionen. Setzen Sie immer Content-Type: application/json für API-Antworten. Verwenden Sie JSON_UNESCAPED_UNICODE, um Chinesisch/Emoji lesbar zu halten (sonst werden sie \uXXXX). json_decode mit true gibt assoziative Arrays zurück (häufiger in PHP). Prüfen Sie immer json_last_error() nach dem Dekodieren von nicht-vertrauenswürdigem JSON. Für REST-APIs setzen Sie passende HTTP-Status-Codes (200, 201, 400, 404, 500) und verwenden Sie eine konsistente Antwort-Struktur.

php
<?php
// encode PHP array/object to JSON
$data = ['name' => 'Alice', 'age' => 30, 'hobbies' => ['reading', 'coding']];
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
// {"name": "Alice", "age": 30, "hobbies": ["reading", "coding"]}

// decode JSON to PHP
$obj = json_decode($json);       // stdClass object
$arr = json_decode($json, true); // associative array

// handle errors
$data = json_decode($badJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
    throw new RuntimeException("JSON error: " . json_last_error_msg());
}

// API response
header('Content-Type: application/json; charset=utf-8');
http_response_code(200);
echo json_encode([
    'status' => 'success',
    'data' => $users,
    'meta' => ['page' => 1, 'total' => 100],
], JSON_UNESCAPED_UNICODE);

// API request handling
$input = json_decode(file_get_contents('php://input'), true);

Composer & Abhängigkeits-Management

Composer ist PHPs Package Manager — unerlässlich für modernes PHP. require spezifiziert Produktions-Abhängigkeiten; require-dev für Entwicklung (Tests usw.). PSR-4-Autoloading bildet Namespaces auf Verzeichnisse ab. Committen Sie immer composer.json und composer.lock (sperrt exakte Versionen). Verwenden Sie composer install (von Lock) in Produktion, composer update, um neueste zu bekommen. Beliebte Packages: Monolog (Logging), Guzzle (HTTP), PHPUnit (Testing), Symfony-Komponenten, Laravel-Framework. Committen Sie niemals das vendor/-Verzeichnis.

php
<?php
// composer.json
// {
//   "require": {
//     "monolog/monolog": "^3.0",
//     "guzzlehttp/guzzle": "^7.0"
//   },
//   "autoload": {
//     "psr-4": {"App\\": "src/"}
//   }
// }

// install: composer install
// update:  composer update
// add:    composer require monolog/monolog

// autoload (at the top of your app)
require 'vendor/autoload.php';

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$log = new Logger('app');
$log->pushHandler(new StreamHandler('app.log', Logger::WARNING));
$log->warning('User not found', ['user_id' => 42]);

// environment variables (vlucas/phpdotenv)
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$dbHost = $_ENV['DB_HOST'];
10

Sessions & Cookies

Cookies setzen & lesen

Cookies speichern kleine Datenmengen im Browser des Benutzers. setcookie() muss vor jeglichem HTML-Output aufgerufen werden (es setzt HTTP-Header). Das httponly-Flag verhindert, dass JavaScript das Cookie liest (mildert XSS), und secure stellt sicher, dass es nur über HTTPS gesendet wird. Cookies werden mit jeder Anfrage an die passende Domain/Pfad gesendet, also speichern Sie keine großen Daten. Für sensible Daten verwenden Sie Sessions stattdessen (Daten bleiben auf dem Server). Validieren und sanitisieren Sie Cookie-Werte immer — sie kommen vom Client und können manipuliert werden.

php
// Set a cookie (must be before any output!)
setcookie("user", "Alice", time() + 3600, "/", "", true, true);
// params: name, value, expiry, path, domain, secure, httponly

// Read cookies (from $_COOKIE superglobal)
if (isset($_COOKIE['user'])) {
    echo "Welcome back, " . htmlspecialchars($_COOKIE['user']);
}

// Delete a cookie: set expiry in the past
setcookie("user", "", time() - 3600, "/");

// Cookie limitations:
// - Sent with every HTTP request (adds bandwidth)
// - Max 4KB per cookie, ~50 per domain
// - Stored client-side (don't store sensitive data!)
// - httponly=true prevents JavaScript access (XSS protection)
// - secure=true sends only over HTTPS

Session-Management

Sessions speichern Daten auf dem Server, identifiziert durch eine Session-ID, die in einem Cookie gespeichert ist. Im Gegensatz zu Cookies sind Session-Daten für den Benutzer nicht sichtbar (sicherer für sensible Daten). session_start() muss auf jeder Seite aufgerufen werden, die Sessions verwendet, vor jeglichem Output. session_regenerate_id(true) verhindert Session-Fixation-Angriffe, indem es eine neue ID erstellt und die alte löscht — rufen Sie es nach Login auf. Zerstören Sie Sessions immer beim Logout. Für große Apps speichern Sie Sessions in Redis/Datenbank statt Dateien (Standard), um über mehrere Server zu teilen.

php
// Start or resume a session (must be before output)
session_start();

// Store data in the session (lives on server, identified by session ID cookie)
$_SESSION['user_id'] = 42;
$_SESSION['username'] = 'alice';
$_SESSION['cart'] = ['item1', 'item2'];

// Read session data
if (isset($_SESSION['user_id'])) {
    echo "User: " . $_SESSION['username'];
}

// Remove a single session variable
unset($_SESSION['cart']);

// Destroy the entire session
session_unset();   // clear all variables
session_destroy(); // destroy session data on server
setcookie(session_name(), '', time() - 3600, '/'); // clear session cookie

// Regenerate ID to prevent session fixation attacks
session_regenerate_id(true);

Session-Konfiguration (php.ini)

Session-Security-Konfiguration ist kritisch. cookie_httponly verhindert, dass XSS Session-IDs stiehlt. cookie_secure stellt sicher, dass Sessions nur über HTTPS funktionieren. SameSite=Strict verhindert CSRF (das Cookie wird nicht bei Cross-Site-Requests gesendet). use_strict_mode lehnt uninitialisierte Session-IDs ab. gc_maxlifetime setzt den Inaktivitäts-Timeout. Für Multi-Server-Deployments implementieren Sie ein benutzerdefiniertes SessionHandlerInterface, um Sessions in einer Datenbank oder Redis zu speichern — der Standard-Datei-basierte Speicher funktioniert nicht über Server hinweg. Konfigurieren Sie diese Einstellungen immer in Produktion.

php
// php.ini session settings (or ini_set at runtime)
ini_set('session.cookie_lifetime', 0);     // 0 = until browser closes
ini_set('session.cookie_httponly', 1);     // prevent JS access
ini_set('session.cookie_secure', 1);       // HTTPS only
ini_set('session.cookie_samesite', 'Strict'); // CSRF protection
ini_set('session.use_strict_mode', 1);     // reject uninitialized IDs
ini_set('session.gc_maxlifetime', 1800);   // 30 min inactivity timeout

// Custom session handler (store in database/Redis)
class MySessionHandler implements SessionHandlerInterface {
    public function open($savePath, $sessionName) { /* connect DB */ }
    public function close() { /* close DB */ }
    public function read($id) { /* fetch from DB */ }
    public function write($id, $data) { /* save to DB */ }
    public function destroy($id) { /* delete from DB */ }
    public function gc($maxlifetime) { /* cleanup old sessions */ }
}
session_set_save_handler(new MySessionHandler(), true);
session_start();

Flash-Messages (Einmal-Benachrichtigungen)

Flash-Messages sind Session-basierte Benachrichtigungen, die einmal angezeigt werden (z.B. 'Erfolgreich gespeichert!') und dann automatisch gelöscht werden. Das Muster: speichern Sie die Message in $_SESSION bei der POST-Anfrage, dann lesen und unsetzen Sie sie bei der nächsten GET-Anfrage. Dies implementiert das Post/Redirect/Get (PRG)-Muster — nach einer Formular-Übermittlung leiten Sie um, um erneute Übermittlung bei Refresh zu verhindern, und zeigen Sie die Flash-Message auf der weitergeleiteten Seite. Frameworks wie Laravel ($request->session()->flash()) und Symfony bieten eingebauten Flash-Message-Support.

php
// Flash messages: shown once, then deleted
session_start();

// Set a flash message (e.g., after form submission)
$_SESSION['flash'] = [
    'type' => 'success',
    'message' => 'Item added to cart!'
];

// On the next page, display and clear:
if (isset($_SESSION['flash'])) {
    $flash = $_SESSION['flash'];
    unset($_SESSION['flash']);  // delete so it shows only once
    echo "<div class='alert alert-{$flash['type']}'>{$flash['message']}</div>";
}

// Helper function pattern
function flash(string $key, string $value = null): ?string {
    if ($value !== null) {
        $_SESSION['flash_' . $key] = $value;
        return null;
    }
    $val = $_SESSION['flash_' . $key] ?? null;
    unset($_SESSION['flash_' . $key]);
    return $val;
}

Session-Security Best Practices

Session-Security erfordert mehrere Schichten. Regenerieren Sie die Session-ID nach Login, um Fixation-Angriffe zu verhindern (wo ein Angreifer eine bekannte Session-ID setzt). Tracken Sie last_activity, um Idle-Timeouts zu implementieren. Optional binden Sie Sessions an IP/User-Agent für Hijacking-Erkennung (Hinweis: dies kann False Positives mit mobilen Netzwerken verursachen, die IPs ändern). Speichern Sie niemals Passwörter oder Kreditkartennummern in Sessions — speichern Sie nur eine Benutzer-ID und rufen Sie sensible Daten aus der Datenbank ab, wenn nötig. Verwenden Sie immer HTTPS in Produktion, um Session-ID-Abfangen zu verhindern.

php
// 1. Always start session before output
session_start();

// 2. Regenerate ID after login (prevent fixation)
if ($loginSuccessful) {
    session_regenerate_id(true);
    $_SESSION['user_id'] = $user->id;
}

// 3. Validate session on each request
if (isset($_SESSION['user_id'])) {
    // Check session hasn't expired
    if (isset($_SESSION['last_activity']) &&
        time() - $_SESSION['last_activity'] > 1800) {
        session_unset();
        session_destroy();
        header('Location: /login');
        exit;
    }
    $_SESSION['last_activity'] = time();

    // Optional: verify IP/user-agent hasn't changed (anti-hijacking)
    if ($_SESSION['ip'] !== $_SERVER['REMOTE_ADDR']) {
        session_destroy();
        exit('Session hijacking detected');
    }
}

// 4. Use prepared statements for session storage in DB
// 5. Set secure cookie flags (see previous example)
// 6. Never store sensitive data (passwords, CC numbers) in sessions
11

REST-API-Entwicklung

JSON-Requests & -Antworten handhaben

REST-APIs tauschen JSON aus. Im Gegensatz zu Formular-Übermittlungen (die $_POST füllen) müssen JSON-Requests aus php://input gelesen und mit json_decode dekodiert werden. Setzen Sie immer Content-Type: application/json für Antworten und verwenden Sie http_response_code() für korrekte HTTP-Status-Codes. Validieren Sie alle Eingaben — json_decode garantiert nicht die erwartete Struktur. Verwenden Sie den Null-Coalescing-Operator (??) für sicheren Zugriff. JSON_PRETTY_PRINT ist nützlich zum Debuggen, aber lassen Sie es in Produktion weg für kleinere Payloads.

php
// Get JSON from request body (not $_POST for JSON!)
$json = file_get_contents('php://input');
$data = json_decode($json, true);  // true = associative array

if (json_last_error() !== JSON_ERROR_NONE) {
    http_response_code(400);
    echo json_encode(['error' => 'Invalid JSON']);
    exit;
}

// Process the data
$name = $data['name'] ?? '';
$email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);

// Return JSON response
header('Content-Type: application/json');
http_response_code(200);  // 200 OK, 201 Created, 400 Bad Request, etc.
echo json_encode([
    'success' => true,
    'data' => ['id' => 1, 'name' => $name],
    'message' => 'User created'
], JSON_PRETTY_PRINT);

Routing & HTTP-Methoden

REST-APIs bilden HTTP-Methoden auf CRUD-Operationen ab: GET (lesen), POST (erstellen), PUT/PATCH (aktualisieren), DELETE (löschen). Routing matcht die Methode + URL-Pfad auf einen Handler. Verwenden Sie preg_match für parametrisierte Routen (z.B. /api/users/42). In Produktion verwenden Sie eine Router-Bibliothek (FastRoute, Symfony Routing) oder ein Framework (Laravel, Slim) für saubereres Routing, Middleware und Dependency Injection. Geben Sie immer passende HTTP-Status-Codes zurück: 200 (OK), 201 (Created), 204 (No Content), 400 (Bad Request), 404 (Not Found), 500 (Server Error).

php
// Simple REST router based on method + path
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$path = trim($path, '/');

// Route table: method => [pattern => handler]
switch (true) {
    case ($method === 'GET' && $path === 'api/users'):
        echo json_encode(getAllUsers());
        break;
    case ($method === 'GET' && preg_match('#^api/users/(\d+)$#', $path, $m)):
        echo json_encode(getUser((int)$m[1]));
        break;
    case ($method === 'POST' && $path === 'api/users'):
        $data = json_decode(file_get_contents('php://input'), true);
        echo json_encode(createUser($data));
        http_response_code(201);
        break;
    case ($method === 'PUT' && preg_match('#^api/users/(\d+)$#', $path, $m)):
        $data = json_decode(file_get_contents('php://input'), true);
        echo json_encode(updateUser((int)$m[1], $data));
        break;
    case ($method === 'DELETE' && preg_match('#^api/users/(\d+)$#', $path, $m)):
        deleteUser((int)$m[1]);
        http_response_code(204);  // No Content
        break;
    default:
        http_response_code(404);
        echo json_encode(['error' => 'Not found']);
}

API-Authentifizierung (JWT)

JWT ermöglicht zustandslose Authentifizierung — der Server muss keine Sessions speichern. Das Token enthält einen Payload (Benutzer-ID, Ablauf), der mit einem Secret-Key signiert ist. Der Client sendet das Token im Authorization-Header (Bearer-Token). Der Server verifiziert die Signatur, um sicherzustellen, dass das Token nicht manipuliert wurde. JWT ist großartig für APIs und Microservices (kein geteilter Session-Store nötig). JWTs können jedoch vor Ablauf nicht widerrufen werden — verwenden Sie kurze Ablaufzeiten und eine Refresh-Token-Strategie. In Produktion verwenden Sie die firebase/php-jwt-Bibliothek, statt Krypto selbst zu implementieren. Speichern Sie niemals sensible Daten im JWT-Payload — es ist nur base64-kodiert, nicht verschlüsselt.

php
// JWT (JSON Web Token) authentication flow
// 1. Login: verify credentials, issue JWT
function login($email, $password) {
    $user = findUserByEmail($email);
    if ($user && password_verify($password, $user['password_hash'])) {
        // Create JWT: header.payload.signature
        $header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
        $payload = base64url_encode(json_encode([
            'user_id' => $user['id'],
            'exp' => time() + 3600  // expires in 1 hour
        ]));
        $signature = hash_hmac('sha256', "$header.$payload", SECRET_KEY, true);
        $jwt = "$header.$payload." . base64url_encode($signature);
        return $jwt;
    }
    return null;
}

// 2. Verify JWT on protected routes
function verifyJWT($token) {
    $parts = explode('.', $token);
    if (count($parts) !== 3) return false;
    [$header, $payload, $signature] = $parts;
    $expected = base64url_encode(
        hash_hmac('sha256', "$header.$payload", SECRET_KEY, true)
    );
    if (!hash_equals($expected, $signature)) return false;
    $data = json_decode(base64url_decode($payload), true);
    return ($data['exp'] ?? 0) > time() ? $data : false;
}

// Use firebase/php-jwt library in production!

Eingabe-Validierung & Sanitisierung

Eingabe-Validierung ist kritisch für API-Security. PHPs filter_var bietet eingebaute Validatoren (FILTER_VALIDATE_EMAIL, FILTER_VALIDATE_INT, FILTER_VALIDATE_URL) mit Optionen wie Min/Max-Bereich. Validieren Sie immer serverseitig — clientseitige Validierung ist für UX, nicht Security. Sanitisieren Sie Strings mit htmlspecialchars, um XSS beim Output von HTML zu verhindern. Für JSON-APIs geben Sie 422 (Unprocessable Entity) für Validierungs-Fehler mit beschreibenden Messages zurück. Ziehen Sie eine Validierungs-Bibliothek (Respect/Validation, Symfony Validator) für komplexe Regeln in Betracht. Vertrauen Sie niemals Benutzereingaben — validieren Sie Typ, Länge, Format und Business-Regeln.

php
// Validate and sanitize API input
function validateUserInput(array $data): array {
    $errors = [];

    // Required fields
    if (empty($data['name'])) {
        $errors[] = 'Name is required';
    }

    // Email validation
    $email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);
    if (!$email) {
        $errors[] = 'Valid email is required';
    }

    // Age: integer between 18 and 120
    $age = filter_var($data['age'] ?? null, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 18, 'max_range' => 120]
    ]);
    if ($age === false) {
        $errors[] = 'Age must be between 18 and 120';
    }

    // String sanitization (remove tags, trim)
    $name = htmlspecialchars(trim($data['name'] ?? ''), ENT_QUOTES, 'UTF-8');

    // URL validation
    $website = filter_var($data['website'] ?? '', FILTER_VALIDATE_URL);

    return ['errors' => $errors, 'data' => compact('email', 'age', 'name')];
}

$result = validateUserInput($input);
if (!empty($result['errors'])) {
    http_response_code(422);  // Unprocessable Entity
    echo json_encode(['errors' => $result['errors']]);
    exit;
}

CORS (Cross-Origin Resource Sharing)

CORS kontrolliert, welche Domänen aus einem Browser auf Ihre API zugreifen können. Browser senden eine Preflight-OPTIONS-Anfrage für nicht-einfache Requests (PUT/DELETE, benutzerdefinierte Header). Ihr Server muss mit den passenden Access-Control-Allow-*-Headern antworten. Für Security spezifizieren Sie exakte Origins statt '*' (besonders mit Credentials). Access-Control-Allow-Credentials: true wird benötigt, wenn die API Cookies oder Authorization-Header verwendet. Vary: Origin teilt Caches mit, dass die Antwort nach Origin variiert. Fehlkonfiguriertes CORS kann Ihre API für jede Website exponieren — whitelisten Sie immer vertrauenswürdige Origins.

php
// Enable CORS for API requests from browsers
header('Access-Control-Allow-Origin: https://example.com');  // specific origin
// OR: header('Access-Control-Allow-Origin: *');  // any origin (less secure)
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Credentials: true');  // for cookies/auth
header('Access-Control-Max-Age: 86400');  // cache preflight for 24h

// Handle preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

// Dynamic origin (check against whitelist)
$allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) {
    header('Access-Control-Allow-Origin: ' . $origin);
    header('Vary: Origin');  // important for caching
}
12

Security (XSS, CSRF, SQL-Injection)

XSS-Prävention (Cross-Site Scripting)

XSS tritt auf, wenn nicht-vertrauenswürdige Daten ohne Escaping in HTML eingefügt werden, was Angreifern erlaubt, JavaScript in den Browsern der Opfer auszuführen. Die Lösung: escapen Sie immer Output mit htmlspecialchars (konvertiert <, >, &, ", ' zu HTML-Entities). Verschiedene Kontexte benötigen unterschiedliches Escaping: HTML-Body (htmlspecialchars), HTML-Attribute (htmlspecialchars mit ENT_QUOTES), JavaScript (json_encode), URLs (urlencode). Content Security Policy (CSP)-Header fügen Defense-in-Depth hinzu, indem sie einschränken, woher Skripte geladen werden können. Verwenden Sie niemals eval(), innerHTML oder document.write() mit Benutzereingaben. Frameworks wie Twig und Blade auto-escapen standardmäßig.

php
// XSS: attacker injects malicious JavaScript into your page
// BAD: outputting user input without escaping
echo "<p>" . $_GET['name'] . "</p>";
// If name = <script>alert('xss')</script>, it executes!

// GOOD: escape output with htmlspecialchars
echo "<p>" . htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8') . "</p>";
// & < > " ' are converted to HTML entities

// For HTML attributes:
echo '<input value="' . htmlspecialchars($value, ENT_QUOTES) . '">';

// For JavaScript context (JSON encode):
echo '<script>var name = ' . json_encode($name) . ';</script>';

// For URL parameters:
echo '<a href="?q=' . urlencode($query) . '">Search</a>';

// Content Security Policy (CSP) header — defense in depth
header("Content-Security-Policy: default-src 'self'; script-src 'self'");

// Use prepared statements (prevents SQL injection too)

CSRF-Prävention (Cross-Site Request Forgery)

CSRF täuscht den Browser eines authentifizierten Benutzers, eine Anfrage an Ihre Site zu senden (z.B. eine Geldüberweisung), ohne sein Wissen. Die Verteidigung: fügen Sie ein unvorhersehbares Token in Formulare ein, das der Angreifer nicht erraten kann. Das Token wird in der Session gespeichert und bei Übermittlung verifiziert. Verwenden Sie hash_equals() für Timing-sicheren Vergleich (verhindert Timing-Angriffe). Für AJAX/API-Aufrufe bieten SameSite=Strict-Cookies und das Erfordern benutzerdefinierter Header (wie X-Requested-With) Schutz. GET-Requests sollten niemals Daten modifizieren (sie können durch Bild-Tags oder Links getriggert werden). Frameworks wie Laravel und Symfony haben eingebauten CSRF-Middleware.

php
// CSRF: attacker tricks a logged-in user into submitting a form
// to your site (using their session cookie)

// Defense: anti-CSRF tokens
session_start();

// Generate a token (once per session)
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// Include token in forms as a hidden field
echo '<form method="POST" action="/transfer">';
echo '<input type="hidden" name="csrf_token" value="' . $_SESSION['csrf_token'] . '">';
echo '<input type="text" name="amount">';
echo '<button type="submit">Transfer</button>';
echo '</form>';

// Verify token on POST/PUT/DELETE requests
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf_token'] ?? '';
    if (!hash_equals($_SESSION['csrf_token'], $token)) {
        http_response_code(403);
        die('CSRF token validation failed');
    }
    // Process the form...
}

// For APIs: use SameSite cookies + custom headers
// (browsers block cross-origin requests without explicit CORS permission)

SQL-Injection-Prävention

SQL-Injection ist die #1 Web-Schwachstelle — sie lässt Angreifer Ihre gesamte Datenbank lesen/modifizieren/löschen. Die universelle Lösung: Prepared Statements (parametrisierte Queries). Die Query-Struktur und Daten werden separat gesendet, sodass Benutzereingaben niemals als SQL interpretiert werden können. Konkatenieren Sie niemals Benutzereingaben in Queries. PDOs prepare/execute handhabt Escaping automatisch. Binden Sie Parameter mit ihren Typen (PDO::PARAM_INT, PDO::PARAM_STR). Für IN-Klauseln mit variablen Items generieren Sie Placeholder dynamisch. Setzen Sie PDO::ATTR_EMULATE_PREPARES auf false für echte serverseitige Prepared Statements (bessere Security).

php
// SQL injection: attacker manipulates queries via unescaped input
// BAD: string concatenation
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
// If id = "1 OR 1=1", returns ALL users!
// If id = "1; DROP TABLE users; --", deletes the table!

// GOOD: prepared statements with parameter binding
$pdo = new PDO('mysql:host=localhost;dbname=myapp', $user, $pass);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);  // real prepared statements

$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);

// Multiple parameters:
$stmt = $pdo->prepare('INSERT INTO users (name, email, age) VALUES (:name, :email, :age)');
$stmt->execute([
    ':name' => $name,
    ':email' => $email,
    ':age' => $age
]);

// IN clause with variable arguments:
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);

Passwort-Hashing & Authentifizierung

password_hash() verwendet bcrypt (oder Argon2) mit einem zufälligen Salt — der Industrie-Standard für Passwort-Speicherung. Der Salt ist im Hash eingebettet, sodass Sie ihn nicht separat verwalten müssen. password_verify() vergleicht die Eingabe sicher gegen den gespeicherten Hash (Timing-sicher). password_needs_rehash() lässt Sie Hashes upgraden, wenn Sie Cost-Faktoren erhöhen oder Algorithmen wechseln — es prüft, ob der Hash zu aktuellen Einstellungen passt, und rehasht beim nächsten Login. Verwenden Sie niemals MD5, SHA1 oder Klartext für Passwörter — sie sind trivial knackbar. Erzwingen Sie starke Passwort-Richtlinien, aber bevorzugen Sie Länge über Komplexität (NIST empfiehlt 8+ Zeichen Minimum).

php
// NEVER store plain-text or MD5/SHA1 passwords!
// Use password_hash() (bcrypt/argon2 by default)

// Hash a password (on registration)
$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_DEFAULT);
// PASSWORD_DEFAULT = bcrypt (or argon2id in PHP 7.3+)
// Store $hash in the database

// Verify a password (on login)
if (password_verify($inputPassword, $storedHash)) {
    // Password is correct
    session_regenerate_id(true);
    $_SESSION['user_id'] = $user['id'];
} else {
    echo "Invalid credentials";
}

// Rehash if algorithm was upgraded (migration)
if (password_verify($input, $hash) &&
    password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $newHash = password_hash($input, PASSWORD_DEFAULT);
    // Update database with $newHash
}

// Password requirements validation
if (strlen($password) < 8 ||
    !preg_match('/[A-Z]/', $password) ||
    !preg_match('/[a-z]/', $password) ||
    !preg_match('/[0-9]/', $password)) {
    echo "Password must be 8+ chars with upper, lower, and number";
}

Datei-Upload-Security

Datei-Uploads sind ein großer Angriffsvektor. Vertrauen Sie niemals $_FILES['type'] (vom Browser gesetzt, leicht gefälscht) — verwenden Sie finfo, um den echten MIME-Typ zu erkennen. Verwenden Sie niemals den benutzerbereitgestellten Dateinamen (er könnte Path-Traversal wie ../../script.php enthalten) — generieren Sie einen zufälligen Namen. Speichern Sie Uploads außerhalb des Web-Roots oder in einem Verzeichnis mit deaktivierter PHP-Ausführung. Für Bilder re-encodieren Sie sie (imagecreatefromjpeg + imagejpeg), um eingebetteten PHP-Code in EXIF-Daten zu strippen. Limitieren Sie Dateigröße, um Denial-of-Service zu verhindern. Validieren Sie Erweiterung, MIME-Typ und Magic Bytes. Ziehen Sie in Betracht, Uploads mit einem Antivirus (ClamAV) zu scannen.

php
// Secure file upload handling
$upload = $_FILES['avatar'];

// 1. Check for upload errors
if ($upload['error'] !== UPLOAD_ERR_OK) {
    die('Upload failed: error ' . $upload['error']);
}

// 2. Validate file size
$maxSize = 2 * 1024 * 1024;  // 2MB
if ($upload['size'] > $maxSize) {
    die('File too large (max 2MB)');
}

// 3. Validate MIME type (don't trust $_FILES['type']!)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($upload['tmp_name']);
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mimeType, $allowedTypes)) {
    die('Invalid file type');
}

// 4. Generate a safe filename (never use user-supplied name)
$safeName = bin2hex(random_bytes(16)) . '.jpg';

// 5. Store OUTSIDE the web root (or in a non-executable dir)
$dest = __DIR__ . '/uploads/' . $safeName;
if (!move_uploaded_file($upload['tmp_name'], $dest)) {
    die('Failed to save file');
}

// 6. For images: re-encode to strip malicious metadata
$img = imagecreatefromjpeg($dest);
imagejpeg($img, $dest, 90);  // re-encode strips embedded PHP/scripts
13

cURL & HTTP-Requests

Basis-cURL GET & POST

cURL ist PHPs mächtigster HTTP-Client und unterstützt GET, POST, benutzerdefinierte Methoden, Header, Cookies und SSL. Setzen Sie immer CURLOPT_RETURNTRANSFER, um die Antwort als String zu erhalten (sonst wird sie direkt gedruckt). CURLOPT_TIMEOUT verhindert Hängen an langsamen Servern. Für POST mit JSON setzen Sie Content-Type- und Content-Length-Header explizit. Prüfen Sie curl_errno() auf Verbindungsfehler und curl_getinfo(CURLINFO_HTTP_CODE) für den HTTP-Status. Schließen Sie cURL-Handles immer mit curl_close(), um Ressourcen freizugeben. Für einfacheren Code ziehen Sie Guzzle (einen cURL-Wrapper mit saubererer API) in Betracht.

php
// GET request
$ch = curl_init('https://api.example.com/users');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
    echo 'Error: ' . curl_error($ch);
}
curl_close($ch);
$data = json_decode($response, true);

// POST request with JSON body
$ch = curl_init('https://api.example.com/users');
$payload = json_encode(['name' => 'Alice', 'email' => '[email protected]']);
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $token,
        'Content-Length: ' . strlen($payload)
    ],
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);

cURL mit Authentifizierung & Cookies

cURL unterstützt mehrere Authentifizierungs-Methoden. CURLOPT_USERPWD setzt HTTP Basic Auth. Bearer-Tokens gehen in den Authorization-Header. Für Cookie-basierte Sessions (wie das Einloggen auf einer Website) verwenden Sie CURLOPT_COOKIEJAR, um Cookies zu speichern, und CURLOPT_COOKIEFILE, um sie bei nachfolgenden Requests zu senden — dies erhält eine Session über mehrere cURL-Aufrufe. Verwenden Sie eine Temp-Datei für Cookies und bereinigen Sie sie mit unlink(). Für API-Aufrufe bevorzugen Sie Token-basierte Auth (Bearer) über Cookies. Verwenden Sie immer HTTPS (cURL verifiziert SSL standardmäßig — deaktivieren Sie CURLOPT_SSL_VERIFYPEER nicht in Produktion).

php
// Basic Auth
$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_USERPWD, 'username:password');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);

// Bearer token (API key)
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $apiKey,
    'Accept: application/json'
]);

// Cookie-based session (login then reuse cookies)
$cookieFile = tempnam(sys_get_temp_dir(), 'cookie');

// Step 1: Login (saves cookies)
$ch = curl_init('https://example.com/login');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query(['user' => 'alice', 'pass' => 'secret']),
    CURLOPT_COOKIEJAR => $cookieFile,   // save cookies here
    CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);

// Step 2: Access protected page (sends saved cookies)
$ch = curl_init('https://example.com/dashboard');
curl_setopt_array($ch, [
    CURLOPT_COOKIEFILE => $cookieFile,  // send cookies from here
    CURLOPT_RETURNTRANSFER => true,
]);
$dashboard = curl_exec($ch);
curl_close($ch);
unlink($cookieFile);  // cleanup

Datei-Downloads & Streaming

Für große Datei-Downloads verwenden Sie CURLOPT_FILE, um direkt in ein File-Handle zu schreiben — dies vermeidet das Laden der gesamten Antwort in den Speicher. CURLOPT_FOLLOWLOCATION folgt HTTP-Weiterleitungen (301, 302). Für Streaming (z.B. Echtzeit-Daten) verwenden Sie CURLOPT_WRITEFUNCTION, um Chunks zu verarbeiten, wie sie ankommen — nützlich für APIs, die Daten streamen. CURLOPT_PROGRESSFUNCTION überwacht Download/Upload-Fortschritt. Setzen Sie einen großzügigen CURLOPT_TIMEOUT für große Dateien. Für sehr große Uploads verwenden Sie CURLOPT_INFILE, um aus einer Datei zu streamen, statt in den Speicher zu laden. Schließen Sie immer File-Handles und cURL-Handles, um Ressourcen-Lecks zu verhindern.

php
// Download a file to disk
$ch = curl_init('https://example.com/large-file.zip');
$fp = fopen('downloaded.zip', 'w');
curl_setopt_array($ch, [
    CURLOPT_FILE => $fp,           // write directly to file
    CURLOPT_FOLLOWLOCATION => true, // follow redirects
    CURLOPT_TIMEOUT => 300,        // 5 min for large files
]);
curl_exec($ch);
fclose($fp);
curl_close($ch);

// Stream response with callback (progress monitoring)
$ch = curl_init('https://example.com/stream');
curl_setopt_array($ch, [
    CURLOPT_WRITEFUNCTION => function($ch, $chunk) {
        echo $chunk;  // stream to output
        ob_flush();
        flush();
        return strlen($chunk);
    },
    CURLOPT_PROGRESSFUNCTION => function($ch, $dlTotal, $dlNow, $ulTotal, $ulNow) {
        if ($dlTotal > 0) {
            printf("
Progress: %.1f%%", ($dlNow / $dlTotal) * 100);
        }
        return 0;
    },
]);
curl_exec($ch);
curl_close($ch);

Gleichzeitige Requests (Multi-cURL)

curl_multi_exec führt mehrere HTTP-Requests parallel aus — dramatisch schneller als sequenzielle Requests, wenn Sie Daten von mehreren Endpoints benötigen. Das Muster: erstellen Sie ein Multi-Handle, fügen Sie einzelne cURL-Handles hinzu, führen Sie das Multi-Handle in einer Schleife aus (curl_multi_exec + curl_multi_select für Effizienz), dann sammeln Sie Ergebnisse. Dies ist nützlich zum Aggregieren von Daten aus mehreren APIs, Prefetching von Ressourcen oder Batch-Operationen. Für fortgeschrittenere Concurrency ziehen Sie ReactPHP oder Amp (async PHP-Frameworks) in Betracht. Beachten Sie, dass Multi-cURL den PHP-Prozess weiterhin blockiert — für echtes Async verwenden Sie Event-Loops oder Message-Queues.

php
// Fetch multiple URLs in parallel (much faster than sequential)
$urls = [
    'https://api.example.com/users',
    'https://api.example.com/posts',
    'https://api.example.com/comments'
];

$multi = curl_multi_init();
$handles = [];

// Create individual handles
foreach ($urls as $i => $url) {
    $handles[$i] = curl_init($url);
    curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
    curl_multi_add_handle($multi, $handles[$i]);
}

// Execute all requests concurrently
$active = null;
do {
    $status = curl_multi_exec($multi, $active);
    if ($active) {
        curl_multi_select($multi);  // wait for activity
    }
} while ($active && $status === CURLM_OK);

// Collect results
$responses = [];
foreach ($handles as $i => $ch) {
    $responses[$i] = json_decode(curl_multi_getcontent($ch), true);
    curl_multi_remove_handle($multi, $ch);
    curl_close($ch);
}
curl_multi_close($multi);

// $responses[0], [1], [2] now contain all three API responses

Guzzle verwenden (moderner HTTP-Client)

Guzzle ist der Standard-HTTP-Client für modernes PHP — viel sauberer als rohes cURL. Es bietet eine fluent API, PSR-7-kompatible Request/Response-Objekte, Middleware (Logging, Retry) und async Requests via Promises. Die 'json'-Option auto-encodiert den Body und setzt Content-Type. getAsync/postAsync geben Promises für gleichzeitige Requests ohne Multi-cURL-Komplexität zurück. Exception-Handling ist eingebaut: RequestException fängt HTTP-Fehler (4xx, 5xx). Guzzle wird von den meisten Frameworks verwendet (Laravels HTTP-Client wrapt Guzzle). Installieren via Composer: composer require guzzlehttp/guzzle.

php
// Guzzle is the most popular PHP HTTP client (composer require guzzlehttp/guzzle)
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

$client = new Client([
    'base_uri' => 'https://api.example.com',
    'timeout'  => 30,
    'headers'  => ['Accept' => 'application/json'],
]);

// GET request
$response = $client->get('/users/42');
$body = json_decode($response->getBody(), true);
echo $response->getStatusCode();  // 200

// POST with JSON
$response = $client->post('/users', [
    'json' => ['name' => 'Alice', 'email' => '[email protected]'],
    'headers' => ['Authorization' => 'Bearer ' . $token],
]);

// Concurrent requests (Promise-based)
$promises = [
    'users' => $client->getAsync('/users'),
    'posts' => $client->getAsync('/posts'),
];
$results = GuzzleHttp\Promise\Utils::settle($promises)->wait();

// Error handling with try/catch
try {
    $response = $client->get('/nonexistent');
} catch (RequestException $e) {
    echo $e->getResponse()->getStatusCode();  // 404
}
14

DateTime Deep Dive

DateTime erstellen & manipulieren

DateTime ist PHPs robuste Datum/Zeit-Klasse. DateTimeImmutable wird gegenüber DateTime bevorzugt — es gibt ein neues Objekt bei Modifikation zurück und verhindert versehentliche Mutations-Bugs (kritisch, wenn dasselbe Datum an mehreren Stellen verwendet wird). createFromFormat parst benutzerdefinierte Formate. modify() akzeptiert relative Ausdrücke wie '+1 week' oder 'last day of next month'. Spezifizieren Sie immer Zeitzonen explizit, um Server-Konfig-abhängiges Verhalten zu vermeiden. Für Datum-Mathe (Intervalle hinzufügen) verwenden Sie DateInterval ('P1D' = 1 Tag, 'P2W' = 2 Wochen, 'PT2H' = 2 Stunden) mit add()/sub().

php
$now = new DateTime();  // current time
$now->setTimezone(new DateTimeZone('Asia/Shanghai'));

// From string (many formats supported)
$date = new DateTime('2024-06-15 14:30:00');
$date = new DateTime('first day of next month');
$date = new DateTime('next Friday');

// From format (precise parsing)
$date = DateTime::createFromFormat('d/m/Y', '15/06/2024');

// Modify dates with relative expressions
$date->modify('+1 week');
$date->modify('-2 days');
$date->modify('last day of this month');
$date->modify('+1 year 3 months');

// Immutable version (recommended — doesn't modify original)
$dt = new DateTimeImmutable('2024-06-15');
$next = $dt->modify('+1 day');  // $dt unchanged, $next is new object
echo $dt->format('Y-m-d');   // 2024-06-15 (unchanged!)
echo $next->format('Y-m-d'); // 2024-06-16

Formatierung & Zeitzonen

format() verwendet Pattern-Buchstaben zum Anpassen des Outputs — Y (4-stelliges Jahr), m (2-stelliger Monat), d (2-stelliger Tag), H (24-Stunden), i (Minuten), s (Sekunden). Für ISO 8601 (in APIs verwendet) verwenden Sie 'Y-m-d\TH:i:sP' oder das 'c'-Kürzel. Zeitzonen-Konvertierung: mit der Quell-Zeitzone erstellen, dann setTimezone zum Konvertieren. Speichern Sie Daten immer in UTC in der Datenbank und konvertieren Sie nur für die Anzeige in die Zeitzone des Benutzers. PHPs Zeitzonen-Datenbank ist umfassend (inklusive DST-Regeln). Verwenden Sie DateTimeZone::listIdentifiers(), um alle unterstützten Zonen zu erhalten. Das 'T'-Escape-Zeichen in format() gibt ein literales 'T' aus (für ISO 8601).

php
// Format codes (most common):
// Y=2024  y=24  m=06  n=6  d=15  j=15
// H=14 (24h)  h=02 (12h)  i=30  s=00  A=PM  a=pm
// D=Mon  l=Monday  M=Jun  F=June  N=1 (Mon=1..Sun=7)

$date = new DateTime('2024-06-15 14:30:00');
echo $date->format('Y-m-d H:i:s');  // 2024-06-15 14:30:00
echo $date->format('l, F j, Y');    // Saturday, June 15, 2024
echo $date->format('Y-m-d\TH:i:sP'); // 2024-06-15T14:30:00+08:00 (ISO 8601)

// Timezone conversion
$utc = new DateTime('2024-06-15 14:00:00', new DateTimeZone('UTC'));
$utc->setTimezone(new DateTimeZone('Asia/Shanghai'));
echo $utc->format('H:i');  // 22:00 (UTC+8)

// List supported timezones
$tzs = DateTimeZone::listIdentifiers();
// ['UTC', 'America/New_York', 'Asia/Shanghai', ...]

// Get timezone offset
$tz = new DateTimeZone('America/Los_Angeles');
$offset = $tz->getOffset(new DateTime());  // seconds from UTC

Datum-Intervalle & Differenzen

DateInterval repräsentiert eine Zeitdauer im ISO-8601-Dauerformat (P1Y2M3DT4H5M6S). add() und sub() wenden Intervalle auf Daten an. diff() gibt ein DateInterval zurück, das die Differenz zwischen zwei Daten repräsentiert — die 'days'-Property gibt totale Tage, während 'y', 'm', 'd' Komponenten-Aufschlüsselungen geben. Die 'invert'-Property zeigt die Richtung an (1, wenn das zweite Datum früher ist). Seien Sie vorsichtig mit Monats-Arithmetik: 'P1M' zu Jan 31 hinzufügen gibt Mar 2 (Feb hat 28-29 Tage), nicht Feb 31. Für Business-Day-Berechnungen iterieren Sie und überspringen Sie Wochenenden/Feiertage manuell oder verwenden Sie eine Bibliothek wie nesbot/carbon.

php
// DateInterval: represents a duration
$interval = new DateInterval('P1Y2M3DT4H5M6S');
// P = period, 1Y = 1 year, 2M = 2 months, 3D = 3 days
// T = time separator, 4H = 4 hours, 5M = 5 minutes, 6S = 6 seconds

$date = new DateTime('2024-06-15');
$date->add(new DateInterval('P1M'));  // +1 month → 2024-07-15
$date->sub(new DateInterval('P10D')); // -10 days → 2024-07-05

// Difference between two dates
$d1 = new DateTime('2024-01-01');
$d2 = new DateTime('2024-12-31');
$diff = $d1->diff($d2);
echo $diff->days;     // 365 (total days)
echo $diff->m;        // 0 (months component)
echo $diff->y;        // 0 (years component)
echo $diff->format('%a days');  // 365 days
echo $diff->format('%y years, %m months, %d days');  // 0 years, 11 months, 30 days

// Check if inverted (d2 < d1)
echo $diff->invert;   // 0 if d2 >= d1, 1 if d2 < d1

DatePeriod (Datums-Bereiche iterieren)

DatePeriod iteriert über einen Datums-Bereich in einem spezifizierten Intervall — perfekt zum Generieren von Kalendern, Reports oder wiederkehrenden Events. Der Konstruktor nimmt (Start, Intervall, Ende) oder (Start, Intervall, Wiederholungen). Das End-Datum ist exklusiv. Häufige Anwendungsfälle: Generieren aller Tage in einem Monat für eine Kalender-Ansicht, Auflisten von Zahlungsperioden oder Erstellen wiederkehrender Event-Zeitpläne. Verwenden Sie iterator_to_array(), um die Periode in ein Array zu materialisieren. Für komplexe Wiederholungs-Regeln (z.B. 'jeden 2. Dienstag') ziehen Sie eine dedizierte Bibliothek wie rrule (RFC 5545 Wiederholungs-Regeln) in Betracht.

php
// DatePeriod iterates over a range of dates
$start = new DateTime('2024-06-01');
$end = new DateTime('2024-06-10');
$interval = new DateInterval('P1D');  // 1 day

$period = new DatePeriod($start, $interval, $end);
foreach ($period as $day) {
    echo $day->format('Y-m-d (D)') . "\n";
}
// 2024-06-01 (Sat)
// 2024-06-02 (Sun)
// ... through 2024-06-09 (end is exclusive)

// Generate next 12 months
$start = new DateTime('first day of this month');
$interval = new DateInterval('P1M');
$period = new DatePeriod($start, $interval, 12);  // 12 recurrences
foreach ($period as $month) {
    echo $month->format('F Y') . "\n";
}

// Every Monday for a year
$start = new DateTime('next Monday');
$end = new DateTime('+1 year');
$period = new DatePeriod($start, new DateInterval('P1W'), $end);
$mondays = iterator_to_array($period);

Carbon-Bibliothek (erweiterte DateTime)

Carbon erweitert DateTime mit einer fluent, ausdrucksstarken API — es ist der De-facto-Standard im PHP-Ökosystem (von Laravel verwendet). diffForHumans() produziert 'vor 5 Tagen', 'in 3 Stunden' — perfekt für UI-Zeitstempel. Die fluent API verkettet Methoden (addYear()->subMonth()->endOfMonth()). Vergleichs-Methoden (isWeekend, isPast, isToday) vereinfachen häufige Prüfungen. Lokalisierung unterstützt 50+ Sprachen für menschenlesbaren Output. Carbon 3 (2024+) ist standardmäßig immutable. Installieren via Composer: composer require nesbot/carbon. Wenn Sie Laravel verwenden, ist Carbon bereits inkludiert.

php
// Carbon: the most popular DateTime library (composer require nesbot/carbon)
use Carbon\Carbon;

$now = Carbon::now('Asia/Shanghai');
$tomorrow = Carbon::tomorrow();
$lastWeek = Carbon::now()->subWeek();

// Human-readable differences
echo Carbon::now()->diffForHumans(Carbon::now()->subDays(5));
// "5 days ago"
echo Carbon::now()->addHours(3)->diffForHumans();
// "3 hours from now"

// Fluent API
$date = Carbon::create(2024, 6, 15, 14, 30, 0)
    ->addYear()
    ->subMonth()
    ->endOfMonth()
    ->setTimezone('UTC');

// Comparison methods
if ($date->isWeekend()) { echo "Weekend!"; }
if ($date->isPast()) { echo "Past"; }
if ($date->isFuture()) { echo "Future"; }
if ($date->isToday()) { echo "Today"; }
if ($date->isLeapYear()) { echo "Leap year"; }

// Localization
Carbon::setLocale('zh');
echo Carbon::now()->subDay()->diffForHumans();  // "1天前"
15

Namespaces & Autoloading

Namespace-Grundlagen

Namespaces organisieren Code in hierarchische Packages und verhindern Klassen-Namens-Kollisionen zwischen Bibliotheken. Die Namespace-Deklaration muss die erste Anweisung sein (nach declare()). Die 'use'-Anweisung importiert Klassen aus anderen Namespaces — platzieren Sie use-Anweisungen am Anfang der Datei. Aliasing (as) löst Konflikte, wenn zwei Klassen denselben Namen haben. Der führende Backslash (\DateTime) referenziert den globalen Namespace. PHP-Namespaces verwenden Backslashes (\) als Separatoren, die auf Verzeichnisstruktur in PSR-4-Autoloading abgebildet werden. Group-use-Anweisungen (use App\Models\{User, Post}) reduzieren Boilerplate.

php
<?php
// Namespaces prevent name collisions (like packages in Java)
namespace App\Services;

class UserService {
    public function find($id) { /* ... */ }
}

// Using namespaced classes
use App\Services\UserService;
use App\Models\User;

$service = new UserService();
$user = new User();

// Aliasing (resolve conflicts or shorten names)
use App\Services\UserService as USvc;
use App\Models\User as UserModel;

// Global namespace (backslash prefix)
$now = new \DateTime();  // \ means root/global namespace
$array = new \ArrayObject();

// Multiple use statements grouped
use App\Models\{User, Post, Comment};
use App\Services\{UserService, PostService};

PSR-4-Autoloading-Standard

PSR-4 ist die Standard-Autoloading-Spezifikation — sie bildet Namespaces auf Verzeichnispfade ab, sodass Sie nie manuelle require/include-Anweisungen benötigen. Die Regel: App\Services\UserService mappt auf src/Services/UserService.php (App\ → src/). Konfigurieren Sie das Mapping in composer.jsons autoload-Sektion. Nach dem Hinzufügen neuer Klassen führen Sie 'composer dump-autoload' aus, um die Class-Map zu regenerieren. Die vendor/autoload.php-Datei (von Composer generiert) handhabt das Laden — inkludieren Sie sie einmal in Ihrem Entry-Point (index.php). PSR-4 erzwingt, dass Klassennamen mit Dateinamen übereinstimmen (UserService → UserService.php).

php
// PSR-4: namespace structure maps to file paths
// App\Services\UserService → src/Services/UserService.php

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

// File: src/Services/UserService.php
namespace App\Services;

class UserService {
    public function getUser($id) {
        return "User $id";
    }
}

// File: src/Models/User.php
namespace App\Models;

class User {
    public $name;
    public function __construct($name) {
        $this->name = $name;
    }
}

// After adding classes, regenerate autoloader:
// $ composer dump-autoload

// The autoloader is included once in your entry point:
require __DIR__ . '/vendor/autoload.php';

$service = new App\Services\UserService();
$user = new App\Models\User('Alice');

Autoloading ohne Composer (spl_autoload)

spl_autoload_register registriert eine Funktion, die aufgerufen wird, wenn eine Klasse noch nicht geladen ist — sie empfängt den vollqualifizierten Klassennamen und sollte die entsprechende Datei requiren. Sie können mehrere Autoloader registrieren (sie werden der Reihe nach aufgerufen). Dies ist, was Composer intern verwendet. Für Produktion verwenden Sie immer Composers PSR-4-Autoloader — er ist optimiert, handhabt Edge-Cases und generiert Class-Maps für schnellere Lookups. Verwenden Sie spl_autoload_register direkt nur für winzige Projekte oder wenn Composer nicht verfügbar ist. Der 'true'-Parameter in class_exists() triggert Autoloading, wenn die Klasse nicht geladen ist.

php
<?php
// Simple autoloader for small projects (no Composer needed)
spl_autoload_register(function ($className) {
    // Convert namespace separators to directory separators
    $file = __DIR__ . '/src/' . str_replace('\\', '/', $className) . '.php';

    // App\Services\UserService → src/App/Services/UserService.php
    // Adjust prefix mapping as needed:
    $file = str_replace('App/', '', $file);  // remove 'App' prefix

    if (file_exists($file)) {
        require $file;
    }
});

// Now classes are loaded automatically on first use
$service = new App\Services\UserService();

// Multiple autoloaders (called in order until one loads the class)
spl_autoload_register(function ($class) {
    $path = __DIR__ . '/lib/' . $class . '.php';
    if (file_exists($path)) require $path;
});

// Check if a class is autoloadable
if (class_exists('App\Helper', true)) {  // true = attempt autoload
    $helper = new App\Helper();
}

Namespace-Konstanten & Funktionen

Namespaces können Konstanten und Funktionen enthalten, nicht nur Klassen. Importieren Sie sie mit 'use const' und 'use function' (PHP 5.6+). Dies ist nützlich für Konfigurations-Konstanten und Utility-Funktionen. Unqualifizierte Funktions-/Konstanten-Aufrufe haben ein Fallback-Verhalten: PHP sucht zuerst im aktuellen Namespace, dann fällt es auf den globalen Namespace zurück. Deshalb können Sie strlen() ohne Backslash aufrufen — aber für Performance und Klarheit präfixen Sie globale Funktionen mit \ in namespaced Code. Group-Imports (use App\Config\{const DB_HOST, function connect}) reduzieren Verbosität.

php
<?php
namespace App\Config;

// Constants in namespaces
const DB_HOST = 'localhost';
const DB_PORT = 3306;

// Functions in namespaces
function connect() {
    return 'Connected to ' . DB_HOST;
}

// Using namespaced constants and functions
use const App\Config\DB_HOST;
use function App\Config\connect;

echo DB_HOST;      // localhost
echo connect();    // Connected to localhost

// Or with fully-qualified names:
echo \App\Config\DB_HOST;
echo \App\Config\connect();

// Namespace-level use for multiple imports
use App\Config\{const DB_HOST, const DB_PORT, function connect};

// Fallback: unqualified function calls fall back to global
// if not found in current namespace
namespace App;
$len = strlen('hello');  // calls global strlen() (fallback)

Anonyme Klassen & Autoloading

Anonyme Klassen (PHP 7+) lassen Sie einfache, einmalige Objekte erstellen, ohne eine benannte Klasse zu definieren — nützlich für Interfaces, Mock-Objekte und Callbacks. Sie können Interfaces implementieren, Klassen erweitern, Konstruktoren haben und Traits verwenden. Die Klasse wird zur Runtime mit einem auto-generierten Namen generiert (class@anonymous). Anonyme Klassen werden sofort geladen (kein Autoloading nötig). Verwenden Sie sie für: einfache Strategy-Pattern, Test-Doubles/Mocks, Event-Listener und DTOs. Für wiederverwendbare Klassen definieren Sie immer benannte Klassen mit korrektem PSR-4-Autoloading. Anonyme Klassen sind besonders praktisch in Tests zum Erstellen leichter Stubs.

php
<?php
namespace App\Factory;

// Anonymous class (PHP 7+): create a one-off class inline
interface Logger {
    public function log(string $msg): void;
}

class App {
    private $logger;
    public function setLogger(Logger $logger) {
        $this->logger = $logger;
    }
}

$app = new App();
$app->setLogger(new class implements Logger {
    public function log(string $msg): void {
        echo "[LOG] $msg\n";
    }
});

// Anonymous class with constructor
$comparator = new class($ascending = true) {
    private $asc;
    public function __construct(bool $asc) { $this->asc = $asc; }
    public function compare($a, $b): int {
        return $this->asc ? $a <=> $b : $b <=> $a;
    }
};

// Get the auto-generated class name
echo get_class($comparator);  // class@anonymous...
16

OOP Deep Dive (Traits, Interfaces, Abstract)

Abstrakte Klassen & Methoden

Abstrakte Klassen stellen eine Basis mit geteilter Implementierung bereit, die Subklassen erweitern. Sie können nicht direkt instanziiert werden. Abstrakte Methoden definieren einen Vertrag (nur Signatur), den konkrete Subklassen implementieren müssen — dies ist das 'Template Method Pattern'. Im Gegensatz zu Interfaces können abstrakte Klassen Properties, Konstruktoren und konkrete Methoden haben. Verwenden Sie abstrakte Klassen, wenn Subklassen signifikante Implementierung teilen (die 'is-a'-Beziehung). Verwenden Sie Interfaces, wenn Sie nur einen Vertrag benötigen, den jede Klasse implementieren kann (die 'can-do'-Beziehung). Eine Klasse kann nur eine abstrakte Klasse erweitern, aber mehrere Interfaces implementieren.

php
<?php
// Abstract class: can't be instantiated, may have abstract methods
abstract class Animal {
    protected $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    // Abstract method: must be implemented by subclasses
    abstract public function makeSound(): string;

    // Concrete method: shared implementation
    public function describe(): string {
        return $this->name . " says " . $this->makeSound();
    }
}

class Dog extends Animal {
    public function makeSound(): string {
        return "Woof!";
    }
}

class Cat extends Animal {
    public function makeSound(): string {
        return "Meow!";
    }
}

$dog = new Dog("Rex");
echo $dog->describe();  // Rex says Woof!
// new Animal("test");  // Error: cannot instantiate abstract class

Interfaces & mehrfache Implementierung

Interfaces definieren einen Vertrag — Methoden-Signaturen ohne Implementierung. Eine Klasse kann mehrere Interfaces implementieren (im Gegensatz zu Einfachvererbung für Klassen). Interfaces ermöglichen Polymorphismus: jede Klasse, die Comparable implementiert, kann sortiert werden, unabhängig von ihrem konkreten Typ. Verwenden Sie Interfaces, um Fähigkeiten zu definieren (Comparable, Serializable, Iterable), die Klassen-Hierarchien übergreifen. Type-Hinting mit Interfaces (function sort(Comparable $a)) ist flexibler als konkrete Klassen. Interface-Vererbung (interface A extends B, C) kombiniert Verträge. Modernes PHP unterstützt auch Interface-Konstanten und statische Methoden in Interfaces.

php
<?php
// Interface: pure contract (no implementation)
interface Comparable {
    public function compareTo($other): int;
}

interface Serializable {
    public function serialize(): string;
    public function unserialize(string $data): void;
}

// A class can implement MULTIPLE interfaces
class Product implements Comparable, Serializable {
    private $price;

    public function __construct(float $price) {
        $this->price = $price;
    }

    public function compareTo($other): int {
        return $this->price <=> $other->price;  // spaceship operator
    }

    public function serialize(): string {
        return serialize($this->price);
    }

    public function unserialize(string $data): void {
        $this->price = unserialize($data);
    }
}

// Type hinting with interfaces
function sortItems(array $items): array {
    usort($items, fn($a, $b) => $a->compareTo($b));
    return $items;
}

// Interface inheritance
interface Repository extends Comparable, Serializable {
    public function find(int $id): ?object;
}

Traits (Code-Wiederverwendung ohne Vererbung)

Traits bieten horizontale Code-Wiederverwendung — Methoden, die in jede Klasse 'eingefügt' werden können, ohne Vererbung. Dies löst das Diamant-Problem (PHP hat Einfachvererbung). Häufige Trait-Verwendungen: Logging, Singleton-Pattern, Soft Deletes, Timestamps. Eine Klasse kann mehrere Traits verwenden. Wenn Traits konfliktierende Methoden haben, verwenden Sie 'insteadof', um eine zu wählen, und 'as', um die andere zu aliasen. Traits können abstrakte Methoden haben (die verwendende Klasse zwingen, sie zu implementieren) und statische Methoden/Properties. Seien Sie vorsichtig, Traits nicht zu überbeanspruchen — sie können Code schwerer nachvollziehbar machen. Bevorzugen Sie Komposition (Dependency Injection) über Traits für komplexes Verhalten.

php
<?php
// Trait: reusable method groups (PHP's answer to multiple inheritance)
trait Logger {
    protected function log(string $msg, string $level = 'INFO'): void {
        echo "[$level] " . date('Y-m-d H:i:s') . " $msg\n";
        // In real code: write to file/database
    }
}

trait Singleton {
    private static $instance;
    public static function getInstance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

class UserService {
    use Logger, Singleton;

    public function findUser($id) {
        $this->log("Finding user $id");
        return "User $id";
    }
}

$svc = UserService::getInstance();
$svc->findUser(42);  // [INFO] 2024-06-15 14:30:00 Finding user 42

// Conflict resolution when traits have same method
trait A { public function hello() { return 'A'; } }
trait B { public function hello() { return 'B'; } }
class C {
    use A, B {
        B::hello insteadof A;  // use B's hello
        A::hello as helloFromA; // alias A's as helloFromA
    }
}

Late Static Binding (static:: vs self::)

Late Static Binding (LSB) ist der Unterschied zwischen self:: (Compile-Zeit, referenziert immer die definierende Klasse) und static:: (Runtime, referenziert die aufrufende Klasse). Dies ist in Vererbung wichtig: Wenn Base eine Methode mit self::$table hat, sieht es immer Bases $table, selbst wenn auf Child aufgerufen. Die Verwendung von static::$table lässt es Childs $table sehen. LSB ist unerlässlich für Factory-Pattern (new static() erstellt Instanzen der aufgerufenen Klasse), ActiveRecord (jedes Model hat seine eigene Tabelle) und das Singleton-Pattern. Der 'static'-Rückgabe-Typ (PHP 8+) deklariert, dass die Methode eine Instanz der aufgerufenen Klasse zurückgibt.

php
<?php
class Base {
    protected static $table = 'base';

    public static function getTable(): string {
        // self:: refers to the class where the method is DEFINED
        return self::$table;  // always 'base'
    }

    public static function getTableStatic(): string {
        // static:: refers to the class that was CALLED (runtime)
        return static::$table;  // late static binding
    }

    public static function create(): static {
        // 'static' return type + new static() = factory pattern
        return new static();
    }
}

class Child extends Base {
    protected static $table = 'child';
}

echo Child::getTable();        // 'base' (self:: = Base)
echo Child::getTableStatic();  // 'child' (static:: = Child)
echo get_class(Child::create()); // 'Child' (new static = Child)

// self:: is resolved at compile time (the defining class)
// static:: is resolved at runtime (the calling class)
// This is "Late Static Binding" — essential for factory patterns

Magic Methods

Magic Methods sind spezielle Methoden, die Objekt-Operationen abfangen. __get/__set erstellen dynamische Properties (nützlich für Data Transfer Objects, ORMs). __toString ermöglicht echo $object. __invoke macht ein Objekt callable wie eine Funktion. __isset/__unset unterstützen isset()/unset() auf dynamischen Properties. __debugInfo passt var_dump-Output an. Andere Magic Methods: __construct, __destruct, __clone (für tiefes Klonen), __call/__callStatic (für undefinierte Methoden, ermöglicht fluent APIs und Mixins), __serialize/__unserialize (ersetzt __sleep/__wakeup in PHP 7.4+). Verwenden Sie Magic Methods sparsam — sie fügen 'magisches' Verhalten hinzu, das schwer zu debuggen sein kann. Dokumentieren Sie sie klar.

php
<?php
class MagicBox {
    private $data = [];

    // Called when accessing undefined properties
    public function __get($name) {
        return $this->data[$name] ?? null;
    }

    // Called when setting undefined properties
    public function __set($name, $value) {
        $this->data[$name] = $value;
    }

    // Called when isset() or empty() on undefined property
    public function __isset($name): bool {
        return isset($this->data[$name]);
    }

    // Called when unset() on undefined property
    public function __unset($name): void {
        unset($this->data[$name]);
    }

    // Called when object is used as string
    public function __toString(): string {
        return json_encode($this->data);
    }

    // Called when object is called as function
    public function __invoke($arg) {
        return "Called with $arg";
    }

    // Called for var_dump/debugging
    public function __debugInfo(): array {
        return ['keys' => array_keys($this->data)];
    }
}

$box = new MagicBox();
$box->name = "Alice";       // __set
echo $box->name;            // __get → Alice
echo $box;                  // __toString → {"name":"Alice"}
echo $box("test");          // __invoke → Called with test
17

Composer Package-Management

composer.json-Grundlagen

composer.json ist das Manifest für PHP-Projekte. require listet Produktions-Abhängigkeiten mit Versions-Constraints auf (^ erlaubt Minor-Updates, ~ erlaubt Patch). autoload definiert PSR-4-Namespace-zu-Verzeichnis-Mapping. require-dev hält entwicklungs-only Abhängigkeiten. Führen Sie composer install aus, um das Projekt einzurichten.

php
{
    "name": "myorg/myapp",
    "type": "project",
    "require": {
        "php": ">=8.1",
        "monolog/monolog": "^3.0",
        "symfony/console": "^7.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^11.0"
    },
    "autoload": {
        "psr-4": { "MyApp\\": "src/" }
    }
}

Installieren & Aktualisieren

composer install liest composer.lock für exakte Versionen (reproduzierbare Builds). composer require fügt ein Package hinzu und löst Abhängigkeiten. composer update fetcht neuere Versionen innerhalb der Constraints. Verwenden Sie --no-dev für Produktion. --optimize-autoloader konvertiert PSR-4 zu Classmap für schnelleres Autoloading in Produktion.

php
# Install all dependencies from composer.lock
composer install

# Add a package (modifies composer.json)
composer require monolog/monolog
composer require --dev phpunit/phpunit

# Update all packages to latest allowed versions
composer update

# Update a single package
composer update monolog/monolog

# Production install (no dev, optimized autoloader)
composer install --no-dev --optimize-autoloader

# Show installed packages
composer show

Versions-Constraints

Caret (^) ist das häufigste Constraint: erlaubt Änderungen, die die am weitesten links stehende non-zero Ziffer nicht modifizieren. Tilde (~) sperrt auf Patch-Level. Für 0.x-Versionen erlaubt ^0.3 0.3.x aber nicht 0.4. Verwenden Sie immer Constraints, um Security-Patches zu bekommen, während Breaking Changes vermieden werden. Pinnen Sie exakte Versionen in composer.lock für Reproduzierbarkeit.

php
"require": {
    // Caret: >=1.2.0, <2.0.0 (allows minor+patch)
    "vendor/pkg": "^1.2",

    // Tilde: >=1.2.0, <1.3.0 (patch only)
    "vendor/pkg2": "~1.2",

    // Exact version
    "vendor/pkg3": "1.2.3",

    // Range
    "vendor/pkg4": ">=1.0 <2.0",

    // Wildcard
    "vendor/pkg5": "1.2.*",

    // Stability flags
    "vendor/pkg6": "dev-main",
    "vendor/pkg7": "2.0@beta"
}

PSR-4-Autoloading

PSR-4-Autoloading mappt Namespace-Präfixe auf Verzeichnisse: MyApp\Services\UserService löst zu src/Services/User.php auf. Führen Sie composer dump-autoload nach dem Hinzufügen neuer Klassen aus. Für Produktion verwenden Sie --optimize, um eine Classmap zu generieren (ein Array-Lookup statt Dateisystem-Checks). Classmap-Autoloading scannt Verzeichnisse und ist am schnellsten für feste Codebasen.

php
// PSR-4: "MyApp\\": "src/" means
// MyApp\User -> src/User.php
// MyApp\Services\Auth -> src/Services/Auth.php

namespace MyApp\Services;

class UserService
{
    public function find(int $id): ?User
    {
        // ...
    }
}

// After running composer dump-autoload:
// require 'vendor/autoload.php';
// $service = new \MyApp\Services\UserService();

// Classmap (faster for production)
// "autoload": { "classmap": ["src/", "lib/"] }

Skripte & Hooks

Composer-Skripte definieren projektspezifische Befehle. Ausführen mit composer <Name>. Eingebaute Events (post-install-cmd, post-update-cmd, pre-autoload-dump) feuern automatisch. Skripte können andere Skripte mit @name referenzieren. Verwenden Sie Skripte, um Entwicklungs-Workflows teamweit zu standardisieren.

php
{
    "scripts": {
        "test": "phpunit",
        "lint": "phpcs --standard=PSR12 src/",
        "fix": "phpcbf --standard=PSR12 src/",
        "post-install-cmd": [
            "MyApp\\Setup::postInstall",
            "php artisan migrate"
        ],
        "post-update-cmd": "@post-install-cmd"
    }
}

// Run: composer test, composer lint, etc.
18

Fortgeschrittenes cURL

Multi-Request (Parallel)

curl_multi_exec führt mehrere Requests parallel aus und reduziert die Gesamtzeit für Batch-API-Aufrufe drastisch. curl_multi_select blockiert, bis Aktivität herrscht, und vermeidet Busy-Waiting. Schließen Sie immer Handles und das Multi-Handle, um Ressourcen freizugeben. Dies ist die Grundlage von High-Performance-HTTP-Scraping und API-Aggregation.

php
<?php
$urls = [
    'https://api.example.com/users',
    'https://api.example.com/posts',
    'https://api.example.com/comments',
];

$multi = curl_multi_init();
$handles = [];

foreach ($urls as $i => $url) {
    $handles[$i] = curl_init($url);
    curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
    curl_setopt($handles[$i], CURLOPT_TIMEOUT, 10);
    curl_multi_add_handle($multi, $handles[$i]);
}

do {
    $status = curl_multi_exec($multi, $active);
    if ($active) curl_multi_select($multi);
} while ($active && $status === CURLM_OK);

foreach ($handles as $i => $ch) {
    $responses[$i] = curl_multi_getcontent($ch);
    curl_multi_remove_handle($multi, $ch);
}
curl_multi_close($multi);

Streaming-Antworten

CURLOPT_WRITEFUNCTION stellt einen Callback für jeden Chunk der Antwort bereit und ermöglicht Streaming-Verarbeitung großer Dateien, ohne sie vollständig in den Speicher zu laden. Geben Sie die Chunk-Länge zurück, um Konsum zu signalisieren. Dies ist unerlässlich für das Herunterladen großer Dateien, das Verarbeiten von Streaming-APIs oder das inkrementelle Parsen von CSV/JSON.

php
<?php
$ch = curl_init('https://example.com/large-file.csv');
$file = fopen('download.csv', 'w');

curl_setopt($ch, CURLOPT_FILE, $file);  // Write to file
// OR use callback for streaming processing:
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $chunk) use ($file) {
    fwrite($file, $chunk);
    // Or parse incrementally:
    // $lines = explode("\n", $chunk);
    return strlen($chunk);  // Must return bytes consumed
});

curl_exec($ch);
fclose($file);
curl_close($ch);

Authentifizierung & Cookies

Setzen Sie benutzerdefinierte Header mit CURLOPT_HTTPHEADER für Authentifizierung (Bearer-Tokens, API-Keys). COOKIEJAR/COOKIEFILE persistieren Cookies zwischen Requests für Session-basierte Auth. CURLOPT_USERPWD setzt HTTP Basic Auth. Für POST setzen Sie CURLOPT_POSTFIELDS mit JSON und dem Content-Type-Header. Setzen Sie immer Accept, um das Antwort-Format zu kontrollieren.

php
<?php
$ch = curl_init('https://api.example.com/data');

// Bearer token
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $token,
    'Content-Type: application/json',
]);

// Cookie jar (persist across requests)
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');   // Save
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');  // Load

// Basic auth
curl_setopt($ch, CURLOPT_USERPWD, 'username:password');

// POST with JSON body
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['key' => 'value']));

$response = curl_exec($ch);

Fehlerbehandlung & Wiederholungen

Überprüfen Sie immer den Rückgabewert von curl_exec (false bei Fehlschlag) und curl_error für die Meldung. curl_getinfo liefert den HTTP-Statuscode, Timing- und Weiterleitungsinformationen. Implementieren Sie exponentielles Backoff für Wiederholungen, um Ratenlimits und vorübergehende Fehler zu behandeln. Unterscheiden Sie zwischen Netzwerkfehlern (curl-Fehler) und HTTP-Fehlern (Statuscode).

php
<?php
function fetchWithRetry(string $url, int $max = 3): ?string
{
    for ($attempt = 1; $attempt <= $max; $attempt++) {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);

        $response = curl_exec($ch);
        $error = curl_error($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($response !== false && $code >= 200 && $code < 300) {
            return $response;
        }

        if ($attempt < $max) {
            sleep(pow(2, $attempt));  // Exponential backoff
        }
    }
    throw new RuntimeException("Failed: {$error}");
}

cURL Options-Referenz

CURLOPT_FOLLOWLOCATION folgt HTTP-Weiterleitungen (3xx). Behalten Sie SSL_VERIFYPEER in der Produktion immer auf true, um MITM-Angriffe zu verhindern; laden Sie cacert.pem von curl.haxx.se herunter. CURLOPT_ENCODING aktiviert Komprimierung. Verwenden Sie CURLOPT_VERBOSE mit STDERR zum Debuggen von Verbindungsproblemen. Setzen Sie angemessene Timeouts, um ein Hängen zu vermeiden.

php
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://example.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);  // Return not echo
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);   // Follow redirects
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);

// SSL (keep VERIFYPEER true in production!)
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacert.pem');

// Performance
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_ENCODING, 'gzip');

// Debug
curl_setopt($ch, CURLOPT_VERBOSE, true);
curl_setopt($ch, CURLOPT_STDERR, fopen('curl.log', 'w'));
19

Bildverarbeitung (GD)

Bilder erstellen & laden

imagecreatetruecolor erstellt ein True-Color-Bild (Millionen von Farben). imagecolorallocate registriert eine Farbe und gibt einen Bezeichner zurück. imagecreatefromjpeg/png/webp lädt vorhandene Dateien. Überprüfen Sie immer den Rückgabewert (false bei Fehlschlag). Verwenden Sie imagesx/imagesy, um Abmessungen zu ermitteln. Geben Sie Speicher mit imagedestroy frei, wenn Sie fertig sind.

php
<?php
// Create a blank image
$img = imagecreatetruecolor(400, 300);

// Allocate colors
$white = imagecolorallocate($img, 255, 255, 255);
$red = imagecolorallocate($img, 255, 0, 0);

// Fill background
imagefill($img, 0, 0, $white);

// Load existing images
$photo = imagecreatefromjpeg('photo.jpg');
$png = imagecreatefrompng('logo.png');
$webp = imagecreatefromwebp('image.webp');

// Get dimensions
$width = imagesx($photo);
$height = imagesy($photo);

Formen & Text zeichnen

GD bietet Zeichenprimitive: Rechtecke, Ellipsen, Linien, Polygone und Bögen. Gefüllte Varianten (imagefilled*) zeichnen ausgefüllte Formen. imagettftext rendert TrueType-Schriftarten mit Winkel- und Größenkontrolle. Senden Sie immer einen Content-Type-Header, bevor Sie Bilddaten ausgeben. Rufen Sie imagedestroy auf, um Speicher freizugeben.

php
<?php
$img = imagecreatetruecolor(400, 300);
$white = imagecolorallocate($img, 255, 255, 255);
$red = imagecolorallocate($img, 255, 0, 0);
$blue = imagecolorallocate($img, 0, 0, 255);
imagefill($img, 0, 0, $white);

// Shapes
imagerectangle($img, 50, 50, 150, 120, $red);
imagefilledrectangle($img, 200, 50, 300, 120, $blue);
imageellipse($img, 100, 200, 80, 80, $red);
imageline($img, 0, 0, 400, 300, $red);

// Text with TrueType font
imagettftext($img, 20, 0, 50, 270, $red, 'arial.ttf', 'Hello GD!');

header('Content-Type: image/png');
imagepng($img);
imagedestroy($img);

Größenänderung & Zuschneiden

imagecopyresampled erzeugt qualitativ bessere Ergebnisse als imagecopyresized (verwendet Interpolation). Behalten Sie das Seitenverhältnis bei, indem Sie die Abmessungen aus dem Original berechnen. Für Miniaturansichten zentrieren Sie den Zuschnitt auf ein Quadrat für ein einheitliches Layout. Zerstören Sie Quellbilder nach dem Kopieren immer, um Speicherlecks bei der Stapelverarbeitung zu vermeiden.

php
<?php
function resizeImage($src, $maxW, $maxH) {
    list($w, $h) = getimagesize($src);
    $ratio = min($maxW / $w, $maxH / $h);
    $newW = (int)($w * $ratio);
    $newH = (int)($h * $ratio);

    $srcImg = imagecreatefromjpeg($src);
    $dstImg = imagecreatetruecolor($newW, $newH);

    // High-quality resampling
    imagecopyresampled($dstImg, $srcImg, 0, 0, 0, 0,
        $newW, $newH, $w, $h);

    imagedestroy($srcImg);
    return $dstImg;
}

// Center crop to square
function cropSquare($src, $size) {
    list($w, $h) = getimagesize($src);
    $min = min($w, $h);
    $x = (int)(($w - $min) / 2);
    $y = (int)(($h - $min) / 2);
    $dst = imagecreatetruecolor($size, $size);
    $img = imagecreatefromjpeg($src);
    imagecopyresampled($dst, $img, 0, 0, $x, $y, $size, $size, $min, $min);
    return $dst;
}

Filter & Effekte

imagefilter wendet integrierte Effekte an: Graustufen, Helligkeit (Bereich -255 bis 255), Kontrast (negativ erhöht), Unschärfe, Kantenerkennung, Invertierung und Einfärben (RGB + Alpha). Pixelate erzeugt einen Mosaik-Effekt. Diese sind schnell, aber grundlegend; für fortgeschrittene Effekte verwenden Sie ImageMagick (Imagick-Erweiterung), das Faltungsmatrizen und benutzerdefinierte Filter unterstützt.

php
<?php
$img = imagecreatefromjpeg('photo.jpg');

// Built-in filters
imagefilter($img, IMG_FILTER_GRAYSCALE);      // B&W
imagefilter($img, IMG_FILTER_BRIGHTNESS, 30);  // Brighten
imagefilter($img, IMG_FILTER_CONTRAST, -20);   // More contrast
imagefilter($img, IMG_FILTER_GAUSSIAN_BLUR);   // Blur
imagefilter($img, IMG_FILTER_EDGEDETECT);      // Edge detection
imagefilter($img, IMG_FILTER_NEGATE);          // Invert colors

// Colorize (tint)
imagefilter($img, IMG_FILTER_COLORIZE, 0, 0, 100, 0);  // Blue tint

// Pixelate
imagefilter($img, IMG_FILTER_PIXELATE, 10, true);

imagepng($img, 'filtered.png');
imagedestroy($img);

Wasserzeichen & Compositing

imagecopymerge legt ein Bild mit einstellbarer Deckkraft (0-100) über ein anderes. PNG-Wasserzeichen mit Alpha-Kanälen verschmelzen natürlich. Für Textwasserzeichen verwenden Sie imagecolorallocatealpha für halbtransparenten Text. imagejpeg-Qualität reicht von 0 (schlechteste) bis 100 (beste); 75-90 ist ein guter Kompromiss für das Web. Zerstören Sie immer beide Bilder.

php
<?php
$photo = imagecreatefromjpeg('photo.jpg');
$watermark = imagecreatefrompng('logo.png');

$pw = imagesx($photo); $ph = imagesy($photo);
$ww = imagesx($watermark); $wh = imagesy($watermark);

// Position: bottom-right with 20px padding
$destX = $pw - $ww - 20;
$destY = $ph - $wh - 20;

// Merge with 50% opacity
imagecopymerge($photo, $watermark, $destX, $destY, 0, 0, $ww, $wh, 50);

// Text watermark
$color = imagecolorallocatealpha($photo, 255, 255, 255, 60);
imagettftext($photo, 30, 0, 20, $ph - 20, $color, 'arial.ttf', '© 2025');

imagejpeg($photo, 'watermarked.jpg', 90);  // 90% quality
imagedestroy($photo);
imagedestroy($watermark);
20

Sessions & Cookies Vertieft

Session-Sicherheit

Sichere Sessions erfordern: HttpOnly-Cookies (kein JavaScript-Zugriff), Secure-Flag (nur HTTPS), SameSite=Strict (CSRF-Schutz) und Strict-Modus (nicht initialisierte Session-IDs ablehnen). Regenerieren Sie die Session-ID immer nach Berechtigungsänderungen (Login, Admin-Zugriff), um Session-Fixation zu verhindern. Verwenden Sie einen benutzerdefinierten Session-Namen, um PHP nicht preiszugeben.

php
<?php
// php.ini or runtime configuration
ini_set('session.cookie_httponly', 1);    // No JS access
ini_set('session.cookie_secure', 1);       // HTTPS only
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);     // Reject uninitialized IDs
ini_set('session.gc_maxlifetime', 3600);   // 1 hour

session_name('APP_SID');  // Custom name
session_start();

// Regenerate ID after login (prevent fixation)
session_regenerate_id(true);

$_SESSION['user_id'] = 123;
$_SESSION['login_time'] = time();

Benutzerdefinierter Session-Handler

Benutzerdefinierte Session-Handler speichern Session-Daten in Datenbanken, Redis oder Memcached statt in Dateien. Implementieren Sie SessionHandlerInterface mit den Methoden open, close, read, write, destroy und gc. Datenbankspeicherung ermöglicht Session-Sharing über mehrere Server (Lastausgleich). Verwenden Sie immer parametrisierte Abfragen, um SQL-Injection in Session-IDs zu verhindern.

php
<?php
class DbSessionHandler implements SessionHandlerInterface
{
    private PDO $pdo;
    public function __construct(PDO $pdo) { $this->pdo = $pdo; }

    public function open($path, $name): bool { return true; }
    public function close(): bool { return true; }

    public function read($id): string|false {
        $stmt = $this->pdo->prepare(
            'SELECT data FROM sessions WHERE id = ? AND expires > ?'
        );
        $stmt->execute([$id, time()]);
        return $stmt->fetchColumn() ?: '';
    }

    public function write($id, $data): bool {
        $exp = time() + (int)ini_get('session.gc_maxlifetime');
        return $this->pdo->prepare(
            'REPLACE INTO sessions (id, data, expires) VALUES (?, ?, ?)'
        )->execute([$id, $data, $exp]);
    }

    public function destroy($id): bool {
        return $this->pdo->prepare('DELETE FROM sessions WHERE id = ?')
            ->execute([$id]);
    }

    public function gc($max): int|false {
        return $this->pdo->prepare('DELETE FROM sessions WHERE expires < ?')
            ->execute([time()]);
    }
}

session_set_save_handler(new DbSessionHandler($pdo), true);
session_start();

Cookie-Verwaltung

Verwenden Sie die Options-Array-Form von setcookie (PHP 7.3+) für mehr Klarheit und zum Setzen von SameSite. Sichere Cookies erfordern HTTPS. HttpOnly verhindert XSS-basierten Cookie-Diebstahl. SameSite=Lax blockiert Cross-Site-POST (ausreichend für den meisten CSRF-Schutz); Strict blockiert alle Cross-Site-Anfragen. Löschen Sie Cookies, indem Sie die Ablaufzeit in die Vergangenheit setzen mit demselben Pfad/Domain.

php
<?php
// Set a cookie with all security options
setcookie('preferences', json_encode(['theme' => 'dark']), [
    'expires' => time() + 86400 * 30,  // 30 days
    'path' => '/',
    'domain' => '.example.com',
    'secure' => true,                   // HTTPS only
    'httponly' => true,                 // No JavaScript access
    'samesite' => 'Lax'                 // CSRF protection
]);

// Read cookies
$theme = $_COOKIE['preferences'] ?? 'default';

// Delete a cookie (set expiration in the past)
setcookie('preferences', '', [
    'expires' => time() - 3600,
    'path' => '/',
]);

Flash-Nachrichten

Flash-Nachrichten speichern einmalige Benachrichtigungen in der Session, die nach einer Weiterleitung angezeigt werden (Post/Redirect/Get-Muster). Die Nachricht wird vor der Weiterleitung gesetzt und nach der Anzeige gelöscht. Dies verhindert Wiederholungswarnungen und hält die UI sauber. Speichern Sie als Array für mehrere Nachrichten. Löschen Sie sofort nach dem Lesen, um veraltete Anzeigen zu verhindern.

php
<?php
// Set a flash message (one-time notification)
function flash(string $key, string $message): void {
    $_SESSION['_flash'][$key] = $message;
}

// Get and clear flash message
function getFlash(string $key): ?string {
    $msg = $_SESSION['_flash'][$key] ?? null;
    unset($_SESSION['_flash'][$key]);
    return $msg;
}

// Usage in controller
flash('success', 'Item saved!');
header('Location: /items');
exit;

// In view after redirect
if ($msg = getFlash('success')) {
    echo "<div class='alert'>$msg</div>";
}

JWT-Authentifizierung

JWT ermöglicht zustandslose Authentifizierung: Der Server speichert keine Session-Daten, was es ideal für APIs und Microservices macht. Das Token enthält Claims (Benutzer-ID, Rolle, Ablaufzeit), die mit einem Secret signiert sind. Kompromisse: Tokens können nicht einfach widerrufen werden (verwenden Sie kurze Ablaufzeit + Refresh-Tokens), und sie erhöhen die Anforderungsgröße. Verwenden Sie HttpOnly-Cookies, um XSS-Token-Diebstahl zu verhindern.

php
<?php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

// Generate token on login
$payload = [
    'user_id' => 123,
    'role' => 'admin',
    'iat' => time(),           // Issued at
    'exp' => time() + 3600,    // Expires in 1 hour
];
$token = JWT::encode($payload, $secretKey, 'HS256');

// Send to client (cookie or Authorization header)
setcookie('auth_token', $token, [
    'expires' => time() + 3600,
    'httponly' => true,
    'secure' => true,
    'samesite' => 'Lax',
]);

// Verify on each request
try {
    $decoded = JWT::decode(
        $_COOKIE['auth_token'],
        new Key($secretKey, 'HS256')
    );
    $userId = $decoded->user_id;
} catch (Exception $e) {
    http_response_code(401);
    exit('Unauthorized');
}
21

REST API Vertieft

Routing & Anfrageverarbeitung

REST-APIs ordnen HTTP-Methoden CRUD-Operationen zu: GET (lesen), POST (erstellen), PUT/PATCH (aktualisieren), DELETE (löschen). Parsen Sie den URL-Pfad zur Ressourcenidentifikation. Lesen Sie den Anfragekörper aus php://input für POST/PUT. Geben Sie immer angemessene HTTP-Statuscodes (200, 201, 400, 404, 500) und JSON-Antworten mit Content-Type-Header zurück.

php
<?php
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segments = explode('/', trim($path, '/'));

switch (true) {
    case $method === 'GET' && $segments[0] === 'users':
        if (isset($segments[1])) getUser((int)$segments[1]);
        else listUsers();
        break;

    case $method === 'POST' && $segments[0] === 'users':
        $data = json_decode(file_get_contents('php://input'), true);
        createUser($data);
        break;

    case $method === 'PUT' && $segments[0] === 'users' && isset($segments[1]):
        $data = json_decode(file_get_contents('php://input'), true);
        updateUser((int)$segments[1], $data);
        break;

    default:
        http_response_code(404);
        echo json_encode(['error' => 'Not Found']);
}

Antwort & Statuscodes

Setzen Sie immer Content-Type: application/json für API-Antworten. Verwenden Sie korrekte Statuscodes: 200 (OK), 201 (Created), 204 (No Content), 400 (Bad Request), 401 (Unauthorized), 403 (Forbidden), 404 (Not Found), 422 (Unprocessable Entity), 429 (Too Many Requests), 500 (Server Error). Schließen Sie Fehlerdetails zum Debuggen ein, aber exponieren Sie niemals Stack-Traces in der Produktion.

php
<?php
function jsonResponse($data, int $status = 200): void
{
    http_response_code($status);
    header('Content-Type: application/json');
    echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    exit;
}

// Success responses
jsonResponse(['data' => $users], 200);          // OK
jsonResponse(['data' => $user], 201);           // Created

// Error responses
jsonResponse(['error' => 'Validation failed'], 400);
jsonResponse(['error' => 'Unauthorized'], 401);
jsonResponse(['error' => 'Forbidden'], 403);
jsonResponse(['error' => 'Not Found'], 404);
jsonResponse(['error' => 'Server error'], 500);

Paginierung & Filterung

Implementieren Sie Paginierung mit LIMIT/OFFSET und geben Sie Metadaten zurück (Gesamt, aktuelle Seite, Gesamtseiten). Validieren und bereinigen Sie Sortierspalten gegen eine Whitelist, um SQL-Injection zu verhindern. Begrenzen Sie per_page, um übermäßige Abfragen zu verhindern. Verwenden Sie LIKE für die Suche mit Wildcards. Geben Sie Paginierungsmetadaten in einem separaten Meta-Objekt zurück.

php
<?php
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = min(100, max(1, (int)($_GET['per_page'] ?? 20)));
$offset = ($page - 1) * $perPage;

// Whitelist sort columns (prevent SQL injection)
$sort = in_array($_GET['sort'] ?? '', ['name', 'email'])
    ? $_GET['sort'] : 'id';
$order = strtolower($_GET['order'] ?? 'asc') === 'desc' ? 'DESC' : 'ASC';

$sql = "SELECT * FROM users ORDER BY $sort $order LIMIT ? OFFSET ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$perPage, $offset]);
$users = $stmt->fetchAll();

// Total count for pagination metadata
$total = (int)$pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();

jsonResponse([
    'data' => $users,
    'meta' => [
        'page' => $page,
        'per_page' => $perPage,
        'total' => $total,
        'total_pages' => ceil($total / $perPage),
    ],
]);

Ratenlimitierung

Ratenlimitierung verhindert API-Missbrauch. Verwenden Sie Fixed-Window (einfach) oder Sliding-Window (genauer) Algorithmen. Speichern Sie Zähler in Redis für verteilte Systeme. Geben Sie X-RateLimit-Header (Limit, Remaining, Reset) zurück, damit Clients sich selbst regulieren können. HTTP 429 mit Retry-After teilt Clients mit, wann sie es erneut versuchen sollen. Für die Produktion verwenden Sie Redis oder einen dedizierten Ratenlimiter.

php
<?php
function checkRateLimit(int $userId, int $max = 100, int $window = 3600): bool
{
    $file = sys_get_temp_dir() . "/rate_{$userId}_" . floor(time() / $window);
    $count = file_exists($file) ? (int)file_get_contents($file) : 0;

    if ($count >= $max) {
        $reset = (floor(time() / $window) + 1) * $window;
        header("X-RateLimit-Limit: $max");
        header("X-RateLimit-Remaining: 0");
        header("Retry-After: " . ($reset - time()));
        http_response_code(429);
        echo json_encode(['error' => 'Rate limit exceeded']);
        return false;
    }

    file_put_contents($file, $count + 1);
    header("X-RateLimit-Remaining: " . ($max - $count - 1));
    return true;
}

if (!checkRateLimit($userId)) exit;

API-Versionierung

API-Versionierungsstrategien: URL-Präfix (/v1/) ist am explizitesten und cache-freundlich; Accept-Header ist RESTful, aber schwerer zu testen. Dokumentieren Sie APIs mit OpenAPI (Swagger)-Annotationen. Generieren Sie interaktive Dokumentation mit Tools wie swagger-php. Versionieren Sie von Anfang an; Breaking Changes erfordern eine neue Version. Deprekieren Sie alte Versionen mit dem Sunset-Header.

php
<?php
// Versioning via URL prefix: /v1/users, /v2/users
$version = $segments[0] ?? 'v1';

// Versioning via Accept header
// Accept: application/vnd.myapp.v2+json
preg_match('/vnd\.myapp\.(v\d+)\+json/',
    $_SERVER['HTTP_ACCEPT'] ?? '', $m);
$version = $m[1] ?? 'v1';

// OpenAPI/Swagger documentation
/**
 * @OA\Get(path="/api/users",
 *   @OA\Response(response=200, description="List users")
 * )
 */
22

Sicherheit Vertieft (XSS/CSRF)

XSS-Prävention

XSS (Cross-Site Scripting) injiziert bösartige Skripte in Webseiten. Verhindern Sie dies durch kontextbasierte Ausgabecodierung: htmlspecialchars für HTML, json_encode für JavaScript, urlencode für URLs. ENT_QUOTES maskiert sowohl einfache als auch doppelte Anführungszeichen. Content-Security-Policy (CSP) fügt Defense-in-Depth hinzu, indem Skriptquellen eingeschränkt werden. Vertrauen Sie niemals Benutzereingaben.

php
<?php
// Output encoding (prevents XSS)
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

// JavaScript string (use json_encode)
echo '<script>var name = ' . json_encode($name) . ';</script>';

// URL parameter
echo 'redirect=' . urlencode($url);

// Content-Security-Policy header
header("Content-Security-Policy: default-src 'self'; script-src 'self'");

// Disable inline scripts
header("X-XSS-Protection: 1; mode=block");

CSRF-Schutz

CSRF (Cross-Site Request Forgery) täuscht Benutzer dazu, ungewollte Aktionen auszuführen. Verhindern Sie dies mit Anti-CSRF-Tokens: Generieren Sie ein zufälliges Token pro Session, fügen Sie es in Formularen als verstecktes Feld ein und verifizieren Sie es bei POST/PUT/DELETE. Verwenden Sie hash_equals für zeit sicheren Vergleich. Für AJAX senden Sie das Token in einem benutzerdefinierten Header. SameSite=Strict-Cookies bieten zusätzlichen Schutz.

php
<?php
// Generate CSRF token
function csrfToken(): string {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

// Verify CSRF token (timing-safe)
function verifyCsrf(string $token): bool {
    return !empty($_SESSION['csrf_token'])
        && hash_equals($_SESSION['csrf_token'], $token);
}

// In the form
echo '<input type="hidden" name="csrf_token" value="' . csrfToken() . '">';

// On POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!verifyCsrf($_POST['csrf_token'] ?? '')) {
        http_response_code(403);
        die('CSRF token validation failed');
    }
}

SQL-Injection-Prävention

SQL-Injection erlaubt Angreifern, beliebige SQL auszuführen. Verwenden Sie immer Prepared Statements mit parametrisierten Abfragen: Die Datenbank trennt SQL-Logik von Daten, was Injection unmöglich macht. Verketten Sie niemals Benutzereingaben in SQL-Strings. Für dynamische Abfragen (IN-Klauseln, ORDER BY) erstellen Sie die SQL-Struktur mit Platzhaltern und übergeben Sie Werte als Parameter.

php
<?php
// BAD: string concatenation (SQL injection vulnerable)
$sql = "SELECT * FROM users WHERE name = '" . $_GET['name'] . "'";
// Attack: ?name=' OR '1'='1

// GOOD: prepared statements
$stmt = $pdo->prepare('SELECT * FROM users WHERE name = ? AND status = ?');
$stmt->execute([$_GET['name'], 'active']);
$users = $stmt->fetchAll();

// Named parameters
$stmt = $pdo->prepare(
    'INSERT INTO users (name, email) VALUES (:name, :email)'
);
$stmt->execute([':name' => $name, ':email' => $email]);

// Dynamic IN clause (still safe)
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);

Passwort-Hashing

Speichern Sie niemals Klartextpasswörter. password_hash verwendet bcrypt (oder Argon2, falls verfügbar) mit automatischer Salt-Generierung. Der Hash enthält den Algorithmus, Cost und Salt, sodass password_verify jedes Format prüfen kann. Verwenden Sie password_needs_rehash, um Hashes zu aktualisieren, wenn Sie den Cost-Faktor erhöhen oder Algorithmen wechseln. Argon2 wird für neue Anwendungen empfohlen.

php
<?php
// Hash a password (uses bcrypt by default, auto-salts)
$hash = password_hash('myPassword123', PASSWORD_DEFAULT);
// $2y$10$... (bcrypt with cost 10)

// Verify a password
if (password_verify($inputPassword, $storedHash)) {
    // Check if rehash is needed (algorithm upgrade)
    if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
        $newHash = password_hash($inputPassword, PASSWORD_DEFAULT);
        // Update stored hash in database
    }
    // Log user in
}

// Argon2 (PHP 7.2+, requires libsodium)
$hash = password_hash('password', PASSWORD_ARGON2ID, [
    'memory_cost' => 65536,  // 64 MB
    'time_cost'   => 4,      // iterations
    'threads'     => 2,
]);

Eingabevalidierung

Validieren Sie Eingaben immer serverseitig (clientseitige Validierung dient nur der UX). Verwenden Sie filter_input mit FILTER_VALIDATE_* für Typprüfung und FILTER_SANITIZE_* zum Bereinigen. Für benutzerdefinierte Regeln verwenden Sie Regex oder dedizierte Validierungsbibliotheken (Respect/Validation, Symfony Validator). Verwenden Sie einen Whitelist-Ansatz: Akzeptieren Sie nur bekannte Felder, lehnen Sie alles andere ab.

php
<?php
// Validate email
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email === false) {
    $errors[] = 'Invalid email';
}

// Validate integer with range
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [
    'options' => ['min_range' => 1, 'max_range' => 150]
]);

// Validate URL
$url = filter_input(INPUT_POST, 'website', FILTER_VALIDATE_URL);

// Custom validation with regex
function validateUsername(string $u): ?string {
    if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $u)) {
        return 'Username must be 3-20 alphanumeric chars';
    }
    return null;
}

// Whitelist approach for arrays
$allowed = ['name', 'email', 'age'];
$input = array_intersect_key($_POST, array_flip($allowed));
23

Namespaces & Autoloading Vertieft

Namespace-Deklaration

Namespaces verhindern Klassennamenskollisionen und organisieren Code hierarchisch. Die Namespace-Deklaration muss die erste Anweisung sein. use importiert Klassen, Funktionen und Konstanten. Aliase (as) lösen Konflikte. PHP-Namespacing verwendet Backslashes. Der PSR-4-Standard ordnet Namespace-Separators Verzeichnis-Separators zu: MyApp\Services\UserService -> src/Services/UserService.php.

php
<?php
// File: src/Services/UserService.php
namespace MyApp\Services;

use MyApp\Models\User;
use MyApp\Repositories\UserRepository;
use MyApp\Exceptions\NotFoundException;

class UserService
{
    public function __construct(
        private UserRepository $repo
    ) {}

    public function find(int $id): User
    {
        $user = $this->repo->findById($id);
        if (!$user) {
            throw new NotFoundException("User {$id} not found");
        }
        return $user;
    }
}

PSR-4-Autoloading

PSR-4 ist die Standard-Autoloading-Spezifikation: Ein Namespace-Präfix wird einem Basisverzeichnis zugeordnet, und jeder Namespace-Separator wird zu einem Verzeichnis-Separator. Composer generiert den Autoloader, der Klassennamen automatisch zu Dateipfaden auflöst. Führen Sie composer dump-autoload aus, nachdem Sie neue Klassen hinzugefügt haben. Der Autoloader lädt Klassen nur, wenn sie zum ersten Mal referenziert werden (Lazy Loading).

php
// composer.json
{
    "autoload": {
        "psr-4": {
            "MyApp\\": "src/",
            "Tests\\": "tests/"
        }
    }
}

// After composer install:
require __DIR__ . '/vendor/autoload.php';

// Now all classes auto-load:
use MyApp\Services\UserService;
use MyApp\Controllers\UserController;

// PSR-4 rules:
// MyApp\User          -> src/User.php
// MyApp\Services\Auth -> src/Services/Auth.php
// MyApp\Models\User\Profile -> src/Models/User/Profile.php

Benutzerdefinierter Autoloader

spl_autoload_register fügt eine Funktion zum Autoloader-Stack hinzu. Wenn eine Klasse referenziert, aber nicht geladen ist, ruft PHP jeden registrierten Autoloader der Reihe nach auf, bis einer die Klasse lädt. Mehrere Autoloader können koexistieren (z.B. einer für PSR-4, einer für Legacy-Klassen). Überprüfen Sie immer, ob die Datei existiert, bevor Sie sie einbinden, um Fehler zu vermeiden. Composer verwendet diesen Mechanismus intern.

php
<?php
spl_autoload_register(function (string $class): void {
    $prefix = 'MyApp\\';
    $baseDir = __DIR__ . '/src/';

    // Check if class uses our namespace prefix
    $len = strlen($prefix);
    if (strncmp($prefix, $class, $len) !== 0) {
        return;  // Not our class
    }

    // Get relative class name
    $relativeClass = substr($class, $len);

    // Replace namespace separators with directory separators
    $file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';

    if (file_exists($file)) {
        require $file;
    }
});

// Multiple autoloaders can be registered (stack-based)
spl_autoload_register(function ($class) {
    $path = __DIR__ . '/legacy/' . $class . '.php';
    if (file_exists($path)) require $path;
});

Classmap & Files-Autoloading

Classmap-Autoloading scannt Verzeichnisse zur dump-autoload-Zeit und erstellt ein Array, das Klassennamen Dateipfaden zuordnet. Dies ist schneller als PSR-4 (ein Array-Lookup vs. Dateisystemprüfungen) und wird für die Produktion empfohlen. files lädt bei jeder Anfrage bestimmte Dateien, nützlich für Hilfsfunktionen und Konstanten, die nicht als Klassen geladen werden können. Verwenden Sie --optimize für die Produktions-Classmap.

php
{
    "autoload": {
        "psr-4": { "MyApp\\": "src/" },
        "classmap": ["src/Legacy/", "lib/OldClasses.php"],
        "files": ["src/helpers.php", "src/constants.php"]
    }
}

// classmap: scans directories and builds class-to-file map
// Faster than PSR-4 for production (no filesystem checks)

// files: always-loaded files (for functions and constants)
// src/helpers.php:
<?php
function dd($var) { var_dump($var); die; }
function env(string $key, $default = null) { /* ... */ }

Namespace-Auflösung

In Code mit Namespaces werden unqualifizierte Klassennamen zuerst über Imports aufgelöst, dann über den aktuellen Namespace. Eingebaute Klassen (DateTime, PDO, Exception) leben im globalen Namespace; referenzieren Sie sie mit einem führenden Backslash oder importieren Sie sie. Funktionen und Konstanten fallen auf den globalen Namespace zurück, wenn sie lokal nicht gefunden werden. Verwenden Sie FQCN (führender Backslash) für absolute Referenzen.

php
<?php
namespace MyApp\Services;

use MyApp\Models\User;

class UserService
{
    public function create(): User
    {
        // User resolves to MyApp\Models\User (imported)
        return new User();
    }

    public function find(): \MyApp\Models\User
    {
        // Fully Qualified Class Name (leading backslash)
        return new \MyApp\Models\User();
    }

    public function date(): \DateTime
    {
        // Global classes need backslash or import
        return new \DateTime();
    }
}
24

Generatoren & Yield

Basis-Generator

Generatoren produzieren Werte träge mit yield, einen nach dem anderen, ohne die gesamte Sammlung im Speicher aufzubauen. Dies ist speichereffizient für große oder unendliche Sequenzen. Die Funktion gibt ein Generator-Objekt zurück, das Iterator implementiert. Jedes yield pausiert die Ausführung, die bei der nächsten Iteration fortgesetzt wird. Verwenden Sie Generatoren zum Lesen großer Dateien, Datenbankcursor und berechneter Sequenzen.

php
<?php
function rangeGen(int $start, int $end, int $step = 1): Generator
{
    for ($i = $start; $i <= $end; $i += $step) {
        yield $i;
    }
}

// Iterate without loading all values into memory
foreach (rangeGen(1, 5) as $value) {
    echo $value . ' ';  // 1 2 3 4 5
}

// Memory efficient for large sequences
foreach (rangeGen(1, 1000000) as $value) {
    if ($value % 100000 === 0) echo "Reached {$value}\n";
}

Yield-Schlüssel-Wert-Paare

Generatoren können Schlüssel-Wert-Paare mit der Syntax yield key => value ausgeben, genau wie assoziative Arrays. Dies erhält Schlüssel durch Transformationen. Um Werte zu filtern, geben Sie sie einfach nicht aus. Der Generator behält seine Position in der Iteration, sodass Sie Pipeline-Verarbeitung aufbauen können, bei der jeder Generator den Strom transformiert oder filtert.

php
<?php
function mapGen(array $data): Generator
{
    foreach ($data as $key => $value) {
        yield $key => strtoupper($value);
    }
}

foreach (mapGen(['a' => 'hello', 'b' => 'world']) as $key => $value) {
    echo "{$key} => {$value}\n";
}
// a => HELLO
// b => WORLD

// Filter by not yielding unwanted values
function filterGen(array $data): Generator
{
    foreach ($data as $value) {
        if ($value > 0) yield $value;
    }
}

Werte an Generatoren senden

Die send()-Methode übergibt einen Wert an den Generator, der das Ergebnis des yield-Ausdrucks wird. Dies ermöglicht bidirektionale Kommunikation, nützlich für Koroutinen und Zustandsautomaten. current() startet den Generator. getReturn() ruft den Rückgabewert ab, nachdem der Generator abgeschlossen ist. Der finally-Block wird ausgeführt, wenn der Generator zerstört wird, was Ressourcenbereinigung ermöglicht.

php
<?php
function accumulator(): Generator
{
    $total = 0;
    while (true) {
        $value = yield $total;
        if ($value === null) break;
        $total += $value;
    }
    return $total;
}

$gen = accumulator();
$gen->current();  // Start: 0
$gen->send(10);   // Returns 10
$gen->send(20);   // Returns 30
$gen->send(5);    // Returns 35
$gen->send(null); // Triggers return
echo $gen->getReturn();  // 35

Yield from (Delegation)

yield from delegiert an einen anderen Generator, ein Array oder ein Traversable und flacht dessen Werte in den äußeren Generator ab. Der Rückgabewert des inneren Generators ist dem äußeren Generator verfügbar. Dies ermöglicht Komposition: Bauen Sie komplexe Pipelines aus einfachen Generatoren. yield from ist auch effizienter als manuelles Iterieren und erneutes Yielden.

php
<?php
function innerGen(): Generator
{
    yield 1;
    yield 2;
    return 'inner done';
}

function outerGen(): Generator
{
    yield 0;
    $result = yield from innerGen();
    echo "Inner returned: {$result}\n";
    yield 3;
}

foreach (outerGen() as $value) {
    echo $value . ' ';  // 0 1 2 3
}
// Inner returned: inner done

// Yield from arrays
function mixedGen(): Generator {
    yield from [10, 20, 30];
    yield from new ArrayObject([40, 50]);
}

Praktische Anwendungsfälle

Generatoren glänzen bei der Verarbeitung großer oder unendlicher Datenströme: zeilenweises Lesen von Dateien, Datenbankcursor-Iteration, paginiertes API-Abrufen und mathematische Sequenzen. Das take()-Muster begrenzt einen unendlichen Generator. Generatoren komponieren gut: Leiten Sie Daten durch mehrere Generatoren zum Filtern, Mappen und Reduzieren. Der Speicherverbrauch bleibt konstant, unabhängig von der Datengröße.

php
<?php
// 1. Read large files line by line (memory efficient)
function readLines(string $file): Generator {
    $handle = fopen($file, 'r');
    while (!feof($handle)) {
        $line = fgets($handle);
        if ($line !== false) yield rtrim($line);
    }
    fclose($handle);
}

foreach (readLines('large.log') as $line) {
    if (str_contains($line, 'ERROR')) {
        echo "{$line}\n";
    }
}

// 2. Infinite sequence with take()
function naturals(): Generator {
    $n = 1;
    while (true) yield $n++;
}

function take(Generator $gen, int $n): Generator {
    for ($i = 0; $i < $n; $i++) {
        yield $gen->current();
        $gen->next();
    }
}

foreach (take(naturals(), 5) as $num) {
    echo $num . ' ';  // 1 2 3 4 5
}
25

Sicherheit

SQL-Injection-Prävention

SQL-Injection tritt auf, wenn Benutzereingaben in SQL verkettet werden. Verwenden Sie immer Prepared Statements mit parametrisierten Abfragen. PDO und MySQLi unterstützen beide. Vertrauen Sie niemals Benutzereingaben. Validieren und bereinigen Sie alle externen Daten.

php
// BAD: vulnerable
$sql = "SELECT * FROM users WHERE name = '" . $_POST['name'] . "'";
// GOOD: prepared statements
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = :name");
$stmt->execute([':name' => $_POST['name']]);
$users = $stmt->fetchAll();

XSS-Prävention

XSS (Cross-Site Scripting) injiziert bösartige Skripte. htmlspecialchars wandelt Sonderzeichen in HTML-Entities um. ENT_QUOTES maskiert sowohl einfache als auch doppelte Anführungszeichen. Maskieren Sie immer, wenn Sie Benutzerdaten in HTML ausgeben. Verwenden Sie Content-Security-Policy-Header für Defense-in-Depth.

php
// BAD: output without escaping
echo $_GET['name'];
// GOOD: escape output
echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
// For HTML attributes
echo 'value="' . htmlspecialchars($value, ENT_QUOTES) . '"';

Passwort-Hashing

password_hash verwendet bcrypt (oder argon2) mit automatischer Salt-Generierung. Verwenden Sie niemals md5 oder sha1 für Passwörter. password_verify prüft ein Passwort gegen einen Hash. password_needs_rehash ermöglicht das Aktualisieren von Hash-Algorithmen. Das Salt ist in den Hash-String eingebettet.

php
// Hash a password
$hash = password_hash('mypassword', PASSWORD_DEFAULT);
// Verify
if (password_verify('mypassword', $hash)) {
    echo 'Valid password';
}
// Check if needs rehash
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $newHash = password_hash('mypassword', PASSWORD_DEFAULT);
}

CSRF-Schutz

CSRF (Cross-Site Request Forgery) täuscht Benutzer zu ungewollten Aktionen. Generieren Sie ein zufälliges Token pro Session. Fügen Sie es in Formularen als verstecktes Feld ein. Verifizieren Sie bei POST mit hash_equals (zeit sicherer Vergleich). SameSite-Cookies bieten zusätzlichen Schutz.

php
session_start();
if (empty($_SESSION['token'])) {
    $_SESSION['token'] = bin2hex(random_bytes(32));
}
// In form
echo '<input type="hidden" name="token" value="' . $_SESSION['token'] . '">';
// Verify
if (!hash_equals($_SESSION['token'], $_POST['token'] ?? '')) {
    die('CSRF token mismatch');
}

Session-Sicherheit

cookie_httponly verhindert JavaScript-Zugriff. cookie_secure stellt nur HTTPS sicher. samesite=Strict verhindert CSRF. use_strict_mode lehnt nicht initialisierte Session-IDs ab. session_regenerate_id verhindert Session-Fixation. Regenerieren Sie immer nach Berechtigungsänderungen.

php
// Secure session settings
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);  // HTTPS only
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);
session_start();
// Regenerate ID after login
session_regenerate_id(true);

Was this helpful?