```
Von der Installation bis zur ersten Composer-Anwendung – modern und idiomatisch.
Setup mit Docker, DDEV oder lokal, erstes Script
$-Variablen, strict_types, Typumwandlung
Interpolation, Heredoc, String-Funktionen
Indiziert, assoziativ, gemischt
if, match, foreach, while
Parameter, Rückgabewerte, Named Arguments
Promoted Constructor, readonly, Methoden
PSR-4, use-Statements, Datei-Organisation
Exceptions, try/catch, eigene Fehler
Pakete installieren, kleines CLI-Tool bauen
php -S für schnelle Tests? Die richtige Wahl spart dir später Stunden.
Für lokale Entwicklung gibt es drei sinnvolle Optionen, jede mit ihrem Anwendungsfall:
| Methode | Wann |
|---|---|
| Lokal (brew, apt) | Kleine Scripts, schneller Test |
| DDEV | Mehrere Projekte mit DB |
| Docker direkt | Custom-Setup, CI/CD |
Für den Einstieg reicht eine lokale Installation. Auf macOS mit Homebrew: brew install php. Auf Ubuntu: apt install php8.4-cli. Auf Windows: am einfachsten WSL2 mit Ubuntu darin.
Lege eine Datei hello.php an mit folgendem Inhalt:
<?php declare(strict_types=1); echo "Hallo Welt!\n"; echo "PHP-Version: " . PHP_VERSION . "\n";
Im Terminal: php hello.php. Du siehst die Ausgabe. Glückwunsch – das ist alles, was PHP zum Start braucht.
Die Zeile declare(strict_types=1) gehört in jede moderne PHP-Datei. Sie aktiviert strenge Typprüfung und verhindert, dass PHP heimlich Strings in Zahlen umwandelt. Verlässlicher Code.
Für Web-Entwicklung braucht PHP normalerweise einen Webserver (Apache, nginx). Für lokales Testen gibt es einen eingebauten Server:
# Im Verzeichnis mit deinen .php-Dateien
php -S localhost:8000
Öffne http://localhost:8000/hello.php im Browser. Der Server ist nicht für Production gedacht, aber für lokale Entwicklung perfekt.
ddev config in deinem Projektordner reicht meist.
declare(strict_types=1) gut?php -S, wann DDEV?$. Warum eigentlich? Und wie kann eine Variable mal eine Zahl, mal ein String sein, ohne dass es explodiert? Die Antwort zeigt, wie PHPs Typsystem funktioniert.
Jede Variable in PHP beginnt mit dem Dollar-Zeichen. Das macht sie im Code sofort erkennbar – du musst nie raten, ob ein Name eine Variable oder eine Funktion ist:
$name = 'Marek'; $age = 34; $height = 1.82; $isActive = true; $email = null;
PHP ist dynamisch typisiert: derselbe Variable darf erst eine Zahl, dann ein String zugewiesen werden. Das ist flexibel, kann aber zu Bugs führen – deshalb sind Type-Hints in Funktionen so wichtig (Kapitel 6).
| Typ | Beispiel | Wofür |
|---|---|---|
int | 42 | Ganze Zahlen |
float | 3.14 | Kommazahlen |
string | 'Hi' | Text |
bool | true | Wahrheitswert |
array | [1, 2] | Liste oder Map |
null | null | Kein Wert |
Den Typ einer Variable findest du mit gettype($x) oder mit Funktionen wie is_int($x), is_string($x).
PHP wandelt Typen oft automatisch um – das ist bequem, aber tückisch. Mit explizitem Cast bist du auf der sicheren Seite:
$input = '42'; // string $number = (int) $input; // 42 als int $float = (float) '3.14'; // 3.14 $text = (string) 42; // "42" // Boolean-Cast: 0, "", "0", null, [] sind false if ($value) { // implizit zu bool echo 'truthy'; }
'0' == false ist true. 'abc' == 0 war in alten PHP-Versionen auch true. Nutze immer === für strikten Vergleich, der auch den Typ prüft.
== und ===?false?Beide sind Strings, aber doppelte Anführungszeichen interpolieren Variablen, einfache nicht:
$name = 'Marek'; echo "Hallo $name"; // Hallo Marek echo 'Hallo $name'; // Hallo $name (wörtlich) // Komplexere Ausdrücke: geschweifte Klammern $user = ['name' => 'Marek']; echo "Hallo {$user['name']}"; // Verkettung mit . echo 'Hallo ' . $name . '!';
Faustregel: Doppelte Anführungszeichen wenn du Variablen einsetzen willst, einfache sonst. Einfache sind minimal schneller, aber das ist heute praktisch egal.
Für längeren Text mit Variablen oder ohne gibt es Heredoc und Nowdoc:
$name = 'Marek'; $mail = <<<TEXT Hallo $name, vielen Dank für deine Bestellung. Grüße TEXT; // Nowdoc (kein Interpolation, wörtlich) $tpl = <<<'TPL' Verwende {{ name }} für den Namen. TPL;
PHP hat eine riesige Auswahl an String-Funktionen. Diese kennst du nach einer Woche auswendig:
| Funktion | Effekt |
|---|---|
strlen($s) | Länge des Strings |
strtoupper / strtolower | Groß-/Kleinschreibung |
trim($s) | Whitespace vorne/hinten entfernen |
str_replace($a, $b, $s) | a durch b ersetzen |
str_contains($s, $needle) | Prüft, ob enthalten |
explode(',', $s) | String zu Array |
implode(',', $arr) | Array zu String |
sprintf('%05d', 42) | Formatiert "00042" |
trim(strtolower($s)) ist die Schreibweise – von innen nach außen lesen.
"", wann ''?array – das macht vieles flexibel, aber auch leicht verwirrend. Wie funktioniert das?
Eine Liste ohne explizite Keys – die Indizes werden automatisch 0, 1, 2, ...:
$fruits = ['Apfel', 'Birne', 'Kirsche']; $fruits[0]; // 'Apfel' $fruits[2]; // 'Kirsche' count($fruits); // 3 // Hinten anhängen $fruits[] = 'Banane'; // jetzt 4 Elemente array_push($fruits, 'Mango'); // alternative Schreibweise
Mit String-Keys wird das Array zu einer Map – die häufigste Form in PHP-Code, weil viele APIs (JSON, Datenbank-Zeilen) so aussehen:
$user = [ 'name' => 'Marek', 'age' => 34, 'roles' => ['admin', 'editor'], ]; $user['name']; // 'Marek' $user['email'] = 'm@e.de'; // neuer Key // Sicher: gibt null wenn nicht da, kein Fehler $phone = $user['phone'] ?? 'unbekannt';
Der Null-Coalescing-Operator ?? ist eine der nützlichsten PHP-Features. Er gibt den linken Wert zurück, wenn er existiert und nicht null ist, sonst den rechten.
foreach ist das Werkzeug der Wahl. Es funktioniert für beide Array-Typen:
// Nur Werte foreach ($fruits as $fruit) { echo $fruit; } // Key und Wert foreach ($user as $key => $value) { echo "$key: $value\n"; }
| Funktion | Wofür |
|---|---|
array_map($fn, $arr) | Jedes Element transformieren |
array_filter($arr, $fn) | Elemente mit Bedingung behalten |
array_reduce($arr, $fn, $init) | Aggregieren zu einem Wert |
array_keys / array_values | Keys oder Werte extrahieren |
in_array($x, $arr) | Enthält Wert? |
sort($arr) | Sortiert in-place |
?? in PHP?match ein modernes Werkzeug ergänzt, das vieles eleganter macht.
if ($score >= 90) { $grade = 'A'; } elseif ($score >= 75) { $grade = 'B'; } else { $grade = 'C oder schlechter'; } // Ternary für einfache Fälle $status = $age >= 18 ? 'erwachsen' : 'minderjährig';
Seit PHP 8 gibt es match – ähnlich wie switch, aber strikt im Vergleich, ein Ausdruck (also mit Rückgabewert), kein fallthrough:
$status = match($code) { 200, 201, 204 => 'success', 301, 302 => 'redirect', 404 => 'not found', 500 => 'server error', default => 'unknown', };
Beachte die Kommas zwischen Werten – sie bedeuten "oder". Und das default ist Pflicht (sonst Exception), das schützt vor vergessenen Fällen.
// for: bekannte Anzahl for ($i = 0; $i < 10; $i++) { echo $i; } // foreach: über Arrays foreach ($items as $item) { echo $item; } // while: unbestimmte Anzahl while ($line = fgets($file)) { processLine($line); } // break und continue foreach ($items as $item) { if ($item->skip) continue; if ($item->stop) break; process($item); }
match. Strikter Vergleich (===), kein fallthrough-Fehler, Ergebnis als Wert. switch nur noch in Legacy-Code oder wenn du komplexe Blöcke pro Fall brauchst.
foreach, wann for?match gegenüber switch?break und continue?function add(int $a, int $b): int { return $a + $b; } function greet(string $name, string $greeting = 'Hallo'): string { return "$greeting, $name!"; } echo add(3, 4); // 7 echo greet('Marek'); // Hallo, Marek!
Die Syntax: function name(typ $param): rückgabetyp. Beides ist optional, aber heute Standard.
Seit PHP 8 kannst du Argumente beim Aufruf mit Namen übergeben. Praktisch bei Funktionen mit vielen Defaults:
function createUser( string $name, int $age = 0, bool $isAdmin = false, ?string $email = null, ): User { /* ... */ } // Positional (alt) createUser('Marek', 34, false, 'm@e.de'); // Named (modern, lesbarer) createUser( name: 'Marek', email: 'm@e.de', isAdmin: true, );
Mit ? wird ein Typ nullable, mit | sind mehrere Typen erlaubt:
// Darf string oder null sein function find(int $id): ?User { return $id > 0 ? new User($id) : null; } // Union: int ODER string function parse(int|string $input): int { return (int) $input; } // void: gibt nichts zurück function log(string $msg): void { file_put_contents('app.log', $msg); }
use ($var) Variablen importieren – aber Globals mit global $x sind tabu.
string zurückgibt??string als Typ?Das wichtigste PHP-8-Feature für Klassen: Konstruktor-Parameter werden automatisch zu Properties, wenn du sie mit einem Sichtbarkeits-Modifier markierst:
class User { public function __construct( public readonly string $name, public readonly int $age, private ?string $email = null, ) {} public function isAdult(): bool { return $this->age >= 18; } public function getEmail(): ?string { return $this->email; } } $user = new User(name: 'Marek', age: 34); echo $user->name; // 'Marek' echo $user->isAdult() ? 'ja' : 'nein';
readonly verhindert nachträgliche Änderungen. public, private, protected steuern die Sichtbarkeit von außen.
interface Greetable { public function greet(): string; } class User implements Greetable { public function __construct(public readonly string $name) {} public function greet(): string { return "Hallo, $this->name"; } } class Admin extends User { public function greet(): string { return "Hallo Admin $this->name"; } }
class Math { public const PI = 3.14159; public static function square(int $x): int { return $x * $x; } } echo Math::PI; // 3.14159 echo Math::square(5); // 25
public readonly string $name im Konstruktor?extends und implements?User – eine für Admins, eine für Kunden – kracht es. Wie verhindert PHP solche Konflikte? Namespaces sind die Antwort, und sie hängen eng mit Composers Autoloading zusammen.
Ein Namespace gibt deinen Klassen einen "Pfad". Konvention: ein Namespace pro Datei, am Anfang deklariert:
<?php declare(strict_types=1); namespace App\Domain\User; class User { public function __construct(public readonly string $name) {} }
Die volle Bezeichnung dieser Klasse ist jetzt App\Domain\User\User. Eine andere Klasse mit demselben Namen in einem anderen Namespace kollidiert nicht.
<?php namespace App\Controller; use App\Domain\User\User; use App\Domain\Order\Order; class UserController { public function show(int $id): User { return new User('Marek'); // kein voller Pfad nötig } }
Mit Aliassen kannst du Namen umbenennen, wenn es Konflikte gibt:
use App\Domain\User\User as DomainUser; use App\Http\User as HttpUser;
PSR-4 ist die Konvention, wie Namespaces auf Dateipfade abgebildet werden. Composer nutzt das für Autoloading. Eine typische Struktur:
my-project/ ├── composer.json ├── vendor/ # installierte Pakete └── src/ ├── Controller/ │ └── UserController.php # App\Controller\UserController └── Domain/ └── User/ └── User.php # App\Domain\User\User
In composer.json definierst du das Mapping:
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
Nach composer dump-autoload findet PHP jede Klasse anhand ihres Namespaces automatisch – kein manuelles require mehr.
User.php enthält class User. Die meisten Frameworks und Tools verlassen sich darauf.
false-Rückgaben und moderne Exceptions. Wie navigierst du beide?
Eine Exception ist PHPs moderne Art, Fehler zu signalisieren. Wird sie nicht gefangen, bricht das Script ab. Mit try/catch reagierst du gezielt:
try { $data = json_decode($json, flags: JSON_THROW_ON_ERROR); $user = processData($data); } catch (\JsonException $e) { echo "Ungültiges JSON: " . $e->getMessage(); } catch (\Exception $e) { echo "Anderer Fehler: " . $e->getMessage(); } finally { cleanup(); }
Der finally-Block läuft immer – auch wenn die Exception nicht gefangen wurde. Praktisch für Cleanup wie Datei schließen oder Locks freigeben.
PHP kennt eine ganze Hierarchie von Exception-Typen. Du kannst gezielt darauf reagieren:
| Exception | Wann |
|---|---|
InvalidArgumentException | Falsche Funktionsargumente |
RuntimeException | Fehler zur Laufzeit |
TypeError | Type-Hint verletzt |
ValueError | Wert außerhalb erlaubtem Bereich |
JsonException | JSON-Parsing fehlgeschlagen |
PDOException | Datenbankfehler |
Für domänenspezifische Fehler definierst du eigene Exception-Klassen. Sie erben von \Exception:
class InsufficientFundsException extends \Exception {} function withdraw(int $amount): void { if ($amount > $this->balance) { throw new InsufficientFundsException( "Brauche $amount, habe nur $this->balance" ); } $this->balance -= $amount; }
@functionCall() kannst du Warnings unterdrücken. Tu's nicht – du versteckst nur Probleme, die später schwer zu finden sind. Wenn du weißt, dass etwas schiefgehen kann, behandle es explizit mit try/catch.
finally-Block?Composer ist der Standard-Paketmanager für PHP. Installation einmal global, dann pro Projekt:
# Neues Projekt mkdir hello-cli cd hello-cli composer init # interaktiver Wizard # Paket hinzufügen composer require symfony/console # Projektstruktur anlegen mkdir src mkdir bin
Composer erzeugt composer.json mit deinen Abhängigkeiten und den Ordner vendor/ mit den installierten Paketen.
{
"name": "marek/hello-cli",
"type": "project",
"require": {
"php": "^8.4",
"symfony/console": "^7.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
Nach Änderungen am Autoload: composer dump-autoload ausführen, damit PHP die neuen Pfade kennt.
Mit Symfony Console schreibst du in wenigen Zeilen ein professionelles CLI-Programm:
// src/GreetCommand.php <?php namespace App; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; class GreetCommand extends Command { protected static $defaultName = 'greet'; protected function configure(): void { $this->addArgument('name', InputArgument::REQUIRED); } protected function execute(InputInterface $in, OutputInterface $out): int { $name = $in->getArgument('name'); $out->writeln("<info>Hallo, $name!</info>"); return Command::SUCCESS; } }
// bin/app.php <?php require __DIR__ . '/../vendor/autoload.php'; use Symfony\Component\Console\Application; use App\GreetCommand; $app = new Application(); $app->add(new GreetCommand()); $app->run();
Im Terminal: php bin/app.php greet Marek. Du siehst "Hallo, Marek!" in Grün. Das ist ein vollständiges CLI-Tool mit Argument-Parsing, Help-Output und Exit-Codes – mit weniger als 50 Zeilen Code.
composer dump-autoload?Du hast PHP jetzt in 10 Kapiteln vom ersten Script bis zur Composer-Anwendung durchlaufen. Aber Wissen verblasst ohne Wiederholung. Plane aktive Wiederholung ein – effektiver als jedes Re-Reading.
Guide gelesen, Recall-Fragen aus jedem Kapitel beantwortet.
OnePager überfliegen, alle Recall-Fragen aus dem Kopf beantworten.
Mini-Projekt: ein eigenes CLI-Tool mit Composer und Symfony Console.
Cheatsheet als Referenz – ein neues Paket aus Packagist einbinden.
Mit diesen Grundlagen kannst du in jede Spezialisierung einsteigen. Empfehlungen je nach Interesse:
Dieser Guide ist Teil eines Sets: