```
Von Interfaces und Generators bis Tests – Production-PHP, das hält.
Kontrakte definieren, Implementierungen trennen
Code-Reuse jenseits klassischer Vererbung
Backed Enums, Methods, Pattern-Matching
Metadaten typensicher statt Annotations
First-Class Callables, use-Capture
array_map, array_filter, array_reduce in der Praxis
Lazy Evaluation mit yield
Templates ohne native Generics
Prepared Statements, Transaktionen
APIs konsumieren, async Requests
Bugs vor der Ausführung finden
Unit-Tests, Mocks, Data Providers
Ein Interface definiert nur, was Methoden tun, nicht wie. Klassen, die das Interface implementieren, müssen alle Methoden bereitstellen:
interface LoggerInterface { public function info(string $message): void; public function error(string $message, array $context = []): void; } class FileLogger implements LoggerInterface { public function __construct(private readonly string $path) {} public function info(string $message): void { file_put_contents($this->path, "[INFO] $message\n", FILE_APPEND); } public function error(string $message, array $context = []): void { file_put_contents($this->path, "[ERROR] $message\n", FILE_APPEND); } }
Der Gewinn: jede Funktion kann den Typ LoggerInterface erwarten und akzeptiert dadurch jede Implementierung – ohne deren Klasse zu kennen.
class OrderService { public function __construct( private readonly LoggerInterface $logger ) {} public function place(Order $order): void { $this->logger->info("Order $order->id placed"); // ... } } // In Tests: Fake-Logger reinreichen // In Production: FileLogger oder SentryLogger $service = new OrderService(new FileLogger('/var/log/app.log'));
Wenn du gemeinsamen Code teilen willst, aber bestimmte Methoden offen lassen, nimmst du eine abstrakte Klasse. Sie kann Properties, fertige Methoden und abstrakte Methoden mischen:
abstract class Notification { public function __construct(protected readonly string $recipient) {} // Fertige Methode public function send(string $message): void { $formatted = $this->format($message); $this->deliver($formatted); } // Muss von Subklassen implementiert werden abstract protected function format(string $msg): string; abstract protected function deliver(string $msg): void; }
| Use Case | Wähle |
|---|---|
| Reiner Kontrakt | Interface |
| Mehrere "Verträge" erfüllen | Interface (mehrfach implementierbar) |
| Gemeinsame Code-Basis teilen | Abstrakte Klasse |
| Template Method Pattern | Abstrakte Klasse |
Psr\Log\LoggerInterface, Psr\Container\ContainerInterface. Wenn du deine Klassen daran ausrichtest, passen sie zu jedem Framework.
User und Order kopieren? Traits lösen das eleganter.
Ein Trait ist wie eine Klasse, aber du erstellst keine Instanz davon. Stattdessen wirst du in andere Klassen "einkopiert". Mehrere Traits pro Klasse sind erlaubt:
trait Timestampable { private ?\DateTimeImmutable $createdAt = null; private ?\DateTimeImmutable $updatedAt = null; public function touch(): void { $this->updatedAt = new \DateTimeImmutable(); $this->createdAt ??= $this->updatedAt; } public function getCreatedAt(): ?\DateTimeImmutable { return $this->createdAt; } } class Article { use Timestampable; public function __construct(public string $title) { $this->touch(); } } class Comment { use Timestampable; // gleiche Methoden, ohne Doppelung }
trait SoftDeletable { private ?\DateTimeImmutable $deletedAt = null; public function delete(): void { $this->deletedAt = new \DateTimeImmutable(); } public function isDeleted(): bool { return $this->deletedAt !== null; } } class Article { use Timestampable, SoftDeletable; // beide Sets von Methoden }
Wenn zwei Traits Methoden mit gleichem Namen haben, gibt es einen Konflikt. PHP zwingt dich, explizit zu wählen:
trait A { public function hello(): string { return 'A'; } } trait B { public function hello(): string { return 'B'; } } class X { use A, B { A::hello insteadof B; // nimm A's hello, ignoriere B's B::hello as helloFromB; // B's hello als 'helloFromB' verfügbar } }
Traits sind verlockend, aber sie binden Code statisch in Klassen ein. Eine moderne Alternative ist Komposition: ein Service-Objekt als Property, das die Logik kapselt. Faustregel:
enum Status { case Draft; case Published; case Archived; } $status = Status::Draft; if ($status === Status::Draft) { echo 'noch nicht veröffentlicht'; }
Die Cases sind Singletons – jede Verwendung von Status::Draft ist dasselbe Objekt. Vergleich mit === funktioniert wie erwartet.
Für Serialisierung – etwa in Datenbank oder JSON – brauchst du Backed Enums mit konkretem String oder Int als Backing-Value:
enum Status: string { case Draft = 'draft'; case Published = 'published'; case Archived = 'archived'; } // Wert auslesen Status::Draft->value; // 'draft' // Aus Wert zurückbauen Status::from('draft'); // Status::Draft Status::tryFrom('unknown'); // null (statt Exception)
Enums dürfen Methoden haben. Damit packst du Verhalten direkt zum Wert, statt es in Helper-Funktionen zu verstecken:
enum Status: string { case Draft = 'draft'; case Published = 'published'; case Archived = 'archived'; public function label(): string { return match($this) { self::Draft => 'Entwurf', self::Published => 'Veröffentlicht', self::Archived => 'Archiv', }; } public function isPublic(): bool { return $this === self::Published; } } Status::Draft->label(); // 'Entwurf' Status::Draft->isPublic(); // false
foreach (Status::cases() as $status) { echo $status->label() . "\n"; } // Entwurf // Veröffentlicht // Archiv
Praktisch für Dropdowns, Validierung oder Migration zwischen Status.
const STATUS_DRAFT = 'draft' schreibst, ist es fast immer besser, ein Enum zu nehmen. Typ-Sicherheit, Methoden direkt am Wert, IDE-Vervollständigung, exhaustive match-Checks.
Status::cases() zurück?/** @Route("/users") */. Kommentare, die geparst werden – brüchig, ohne Typ-Check. PHP 8 brachte native Attribute. Wie schreibst und nutzt du sie?
Ein Attribut ist eine Klasse, die mit #[Attribute] markiert ist. Sie kann Konstruktor-Parameter haben wie jede andere Klasse:
#[\Attribute(\Attribute::TARGET_METHOD)] class Route { public function __construct( public readonly string $path, public readonly string $method = 'GET', ) {} }
Das TARGET_METHOD sagt: dieses Attribut darf nur auf Methoden. Andere Optionen: TARGET_CLASS, TARGET_PROPERTY, TARGET_PARAMETER.
class UserController { #[Route('/users')] public function list(): Response { /* ... */ } #[Route('/users/{id}', method: 'GET')] public function show(int $id): Response { /* ... */ } #[Route('/users', method: 'POST')] public function create(): Response { /* ... */ } }
Mit der Reflection-API liest du Attribute aus Klassen, Methoden oder Properties aus – das ist die Grundlage für Frameworks wie Symfony, Doctrine oder ORM-Libraries:
$reflection = new \ReflectionClass(UserController::class); foreach ($reflection->getMethods() as $method) { foreach ($method->getAttributes(Route::class) as $attr) { $route = $attr->newInstance(); // Route-Instanz echo "$route->method $route->path → " . $method->name . "\n"; } } // Output: // GET /users → list // GET /users/{id} → show // POST /users → create
| Framework | Beispiel-Attribute |
|---|---|
| Symfony | #[Route], #[AsCommand] |
| Doctrine | #[ORM\Entity], #[ORM\Column] |
| PHPUnit | #[DataProvider], #[Test] |
| Validator | #[Assert\NotBlank] |
/** @Route("/users") */ funktioniert noch, aber das Doctrine/Symfony-Ökosystem migriert auf native Attribute. Bei Neuentwicklungen: immer Attribute, nie Annotations.
Eine Closure ist eine Funktion ohne Namen, die in einer Variable lebt. Du übergibst sie als Argument oder gibst sie zurück:
$double = function(int $x): int { return $x * 2; }; echo $double(5); // 10 // Als Argument an array_map $numbers = [1, 2, 3]; $doubled = array_map($double, $numbers); // [2, 4, 6]
Im Gegensatz zu normalen Funktionen haben Closures Zugriff auf den umgebenden Scope – aber nur, wenn du es explizit mit use erlaubst:
$prefix = 'Mr. '; $greet = function(string $name) use ($prefix): string { return $prefix . $name; }; echo $greet('Marek'); // 'Mr. Marek' // use kopiert standardmäßig: spätere Änderung wirkt nicht $prefix = 'Dr. '; echo $greet('Marek'); // immer noch 'Mr. Marek' // use mit Referenz: spätere Änderung wirkt $counter = 0; $increment = function() use (&$counter): void { $counter++; }; $increment(); $increment(); echo $counter; // 2
Für einzeilige Closures gibt es seit PHP 7.4 die kürzere Arrow Function Syntax. Sie capturet Variablen automatisch (kein use nötig):
$multiplier = 3; $multiply = fn(int $x): int => $x * $multiplier; echo $multiply(5); // 15 // Praktisch in array_map / array_filter $names = ['marek', 'anna', 'tom']; $upper = array_map(fn($n) => strtoupper($n), $names);
Seit PHP 8.1 kannst du jede Funktion oder Methode als Closure referenzieren – ohne sie aufzurufen:
// Globale Funktion $upper = strtoupper(...); $upper('hallo'); // 'HALLO' // Methode $user = new User('Marek'); $greet = $user->greet(...); $greet(); // 'Hallo, Marek' // Static Methode $square = Math::square(...); $square(5); // 25 // Direkt an array_map übergeben $upper = array_map(strtoupper(...), $names);
fn) wenn der Body ein einzelner Ausdruck ist. Volle Closure (function) wenn du mehrere Statements brauchst oder explizites Variablen-Capturing willst.
use ($x) und use (&$x)?strtoupper(...)?foreach, Zwischenvariable, Bedingung – fünf Zeilen. Mit Higher-Order Functions wird daraus ein lesbarer Einzeiler. Wie?
array_map wendet eine Funktion auf jedes Element an und gibt ein neues Array zurück:
$prices = [10, 25, 99]; $withTax = array_map(fn($p) => $p * 1.19, $prices); // [11.9, 29.75, 117.81] // Objekte transformieren $users = [new User('Marek'), new User('Anna')]; $names = array_map(fn(User $u) => $u->name, $users); // ['Marek', 'Anna']
array_filter behält nur Elemente, für die die Funktion true zurückgibt:
$numbers = [1, 2, 3, 4, 5, 6]; $even = array_filter($numbers, fn($n) => $n % 2 === 0); // [2, 4, 6] // Achtung: Keys werden beibehalten! // $even ist [1 => 2, 3 => 4, 5 => 6] // Re-indexen mit array_values $even = array_values(array_filter($numbers, fn($n) => $n % 2 === 0)); // [2, 4, 6] mit Keys 0, 1, 2
array_reduce reduziert ein Array zu einem einzelnen Wert. Der Akkumulator startet mit einem Initialwert und wird mit jedem Element aktualisiert:
$prices = [10, 25, 99]; $total = array_reduce( $prices, fn(float $sum, int $price) => $sum + $price, 0.0 // Initialwert ); // 134.0
Das volle Potenzial entfaltet sich, wenn du mehrere Funktionen kombinierst – z.B. Beträge der bezahlten Bestellungen aufsummieren:
$paidTotal = array_reduce( array_filter($orders, fn($o) => $o->status === Status::Paid), fn($sum, $o) => $sum + $o->amount, 0.0 ); // Mit Zwischen-Variablen leichter zu lesen $paidOrders = array_filter($orders, fn($o) => $o->isPaid()); $amounts = array_map(fn($o) => $o->amount, $paidOrders); $total = array_sum($amounts);
Letzteres ist oft lesbarer als verschachtelte Aufrufe – nicht immer ist die kompakteste Version die beste.
collect($users)->filter(...)->map(...)->sum(). Lesbarer als verschachtelte PHP-Standardfunktionen.
array_map vs. array_filter?array_filter überrascht oft?Ein Generator ist eine Funktion, die Werte mit yield ausgibt statt mit return. Sie pausiert zwischen den Werten – Speicher bleibt frei:
function readLines(string $path): \Generator { $handle = fopen($path, 'r'); while (($line = fgets($handle)) !== false) { yield rtrim($line); } fclose($handle); } // Aufruf liefert sofort einen Generator – Datei wird noch nicht gelesen $lines = readLines('/var/log/huge.log'); // Erst die Iteration liest tatsächlich foreach ($lines as $line) { if (str_contains($line, 'ERROR')) { echo $line; } }
Der Speicher-Vorteil: zu jedem Zeitpunkt ist nur eine Zeile geladen, egal wie groß die Datei ist.
function readCsv(string $path): \Generator { $handle = fopen($path, 'r'); $headers = fgetcsv($handle); $rowNumber = 0; while (($row = fgetcsv($handle)) !== false) { yield $rowNumber++ => array_combine($headers, $row); } fclose($handle); } foreach (readCsv('users.csv') as $num => $row) { echo "Zeile $num: " . $row['name'] . "\n"; }
Da Generators lazy sind, kannst du theoretisch unendliche Sequenzen erzeugen – solange du sie nicht komplett auswertest:
function fibonacci(): \Generator { $a = 0; $b = 1; while (true) { yield $a; [$a, $b] = [$b, $a + $b]; } } // Erste 10 Fibonacci-Zahlen $count = 0; foreach (fibonacci() as $n) { echo $n . ' '; if (++$count >= 10) break; } // 0 1 1 2 3 5 8 13 21 34
Mit yield from delegierst du an einen anderen Generator – nützlich, um Logik zu kapseln:
function range1to3(): \Generator { yield 1; yield 2; yield 3; } function range1to6(): \Generator { yield from range1to3(); // gibt 1, 2, 3 yield 4; yield 5; yield 6; }
foreach durchläufst, ist der zweite Durchlauf leer. Brauchst du Mehrfach-Zugriff, konvertiere mit iterator_to_array($gen) zu einem Array.
foreach durchläufst?yield from da?mixed. Die Lösung: PHPDoc-Generics mit PHPStan oder Psalm.
class Collection { private array $items = []; public function add(mixed $item): void { $this->items[] = $item; } public function first(): mixed { return $this->items[0] ?? null; } } $users = new Collection(); $users->add(new User('Marek')); $user = $users->first(); // IDE: mixed, keine Autocompletion $user->name; // keine Hilfe von IDE oder PHPStan
Mit PHPDoc-Annotationen kannst du Templates definieren. PHPStan und Psalm verstehen sie und prüfen Typen statisch:
/** * @template T */ class Collection { /** @var array<T> */ private array $items = []; /** * @param T $item */ public function add(mixed $item): void { $this->items[] = $item; } /** * @return T|null */ public function first(): mixed { return $this->items[0] ?? null; } }
Bei der Verwendung gibst du den konkreten Typ als PHPDoc-Annotation an. PHPStan ersetzt dann T intern:
/** @var Collection<User> $users */ $users = new Collection(); $users->add(new User('Marek')); $user = $users->first(); // PHPStan weiß: User|null $user->name; // OK, IDE und PHPStan kennen User $users->add('string'); // PHPStan-Fehler: erwartet User, nicht string
/** * @template T of \Throwable */ class ErrorCollection { /** @var array<T> */ private array $errors = []; /** * @param T $error */ public function add(\Throwable $error): void { $this->errors[] = $error; } } // Erlaubt nur Throwables und Subklassen /** @var ErrorCollection<\RuntimeException> $errors */ $errors = new ErrorCollection();
PHP-Arrays sind oft "halb-Objekte" mit festen Keys. PHPStan kennt Array-Shapes:
/** * @return array{name: string, age: int, email: string|null} */ function getUser(int $id): array { return ['name' => 'Marek', 'age' => 34, 'email' => null]; } $user = getUser(42); echo $user['name']; // PHPStan weiß: string echo $user['phone']; // PHPStan-Fehler: Key existiert nicht
array{name: string, age: int}?PDO unterstützt MySQL, PostgreSQL, SQLite und mehr über dieselbe API. Der DSN-String unterscheidet sich, der Rest ist gleich:
$dsn = 'mysql:host=localhost;dbname=myapp;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, ]);
Die drei Options sind Pflicht in jedem Projekt: Exceptions bei Fehlern, assoziative Arrays beim Fetch, echte Prepared Statements (statt emulierter Client-Side).
Niemals User-Input direkt in SQL-Strings konkatenieren. Stattdessen: Placeholder mit ? oder Named Parameters:
// Falsch: SQL-Injection-Lücke $id = $_GET['id']; $result = $pdo->query("SELECT * FROM users WHERE id = $id"); // $_GET['id'] = "1; DROP TABLE users" → katastrophal // Richtig: Prepared Statement mit Positional Parameters $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?'); $stmt->execute([$id]); $user = $stmt->fetch(); // Oder mit Named Parameters $stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id AND active = :active'); $stmt->execute([':id' => $id, ':active' => true]);
// Einzelne Zeile $user = $stmt->fetch(); // assoc array oder false // Alle Zeilen $users = $stmt->fetchAll(); // Array von Zeilen // Iterativ für große Result-Sets while ($row = $stmt->fetch()) { processRow($row); } // Direkt in Objekte mappen (PDO::FETCH_CLASS) $stmt->setFetchMode(PDO::FETCH_CLASS, User::class); $users = $stmt->fetchAll();
Wenn mehrere Operationen atomar sein müssen (alle oder keine), nutzt du Transaktionen:
$pdo->beginTransaction(); try { $pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?') ->execute([100, $fromId]); $pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?') ->execute([100, $toId]); $pdo->commit(); } catch (\PDOException $e) { $pdo->rollBack(); throw $e; }
mysql_*-Funktionen sind seit PHP 7 entfernt. mysqli existiert noch, aber nur mit Prepared Statements nutzen. Standard heute: PDO für direkten DB-Zugriff, Doctrine DBAL für Query-Builder, Doctrine ORM für komplexe Domänen.
file_get_contents oder cURL geht das, aber wird schnell hässlich. Guzzle ist Standard-Library für HTTP in PHP.
composer require guzzlehttp/guzzle
use GuzzleHttp\Client; $client = new Client([ 'base_uri' => 'https://api.example.com', 'timeout' => 5, 'headers' => [ 'Accept' => 'application/json', 'Authorization' => 'Bearer ' . $token, ], ]);
// GET $response = $client->get('/users/42'); $data = json_decode($response->getBody()->getContents(), true); // GET mit Query-Parametern $response = $client->get('/users', [ 'query' => ['page' => 2, 'limit' => 50], ]); // POST mit JSON-Body $response = $client->post('/users', [ 'json' => ['name' => 'Marek', 'email' => 'm@e.de'], ]); // Status und Header $response->getStatusCode(); // 201 $response->getHeader('Content-Type');
Guzzle wirft bei HTTP-Status 4xx/5xx Exceptions. Die hierarchische Struktur erlaubt gezieltes Catchen:
use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ServerException; use GuzzleHttp\Exception\ConnectException; try { $response = $client->get('/users/42'); } catch (ClientException $e) { // 4xx Fehler – z.B. 404 Not Found, 401 Unauthorized $status = $e->getResponse()->getStatusCode(); } catch (ServerException $e) { // 5xx Fehler – z.B. 500, 503 } catch (ConnectException $e) { // Netzwerk-Fehler, Timeout, DNS }
Mehrere Requests parallel statt seriell? Guzzle unterstützt Promises:
use GuzzleHttp\Promise; $promises = [ 'users' => $client->getAsync('/users'), 'orders' => $client->getAsync('/orders'), 'stats' => $client->getAsync('/stats'), ]; // Warte bis alle fertig sind $results = Promise\Utils::settle($promises)->wait(); foreach ($results as $key => $result) { if ($result['state'] === 'fulfilled') { $body = (string) $result['value']->getBody(); // ... } }
Drei API-Calls in der Zeit eines einzelnen – wenn die API es zulässt, ist das ein riesiger Performance-Gewinn.
Psr\Http\Client\ClientInterface. Guzzle implementiert das, aber auch andere Clients (z.B. Symfony HttpClient). Du tauschst sie dann ohne Code-Änderung aus.
base_uri in der Client-Konfiguration?composer require --dev phpstan/phpstan
# phpstan.neon parameters: level: 8 # strikteste Stufe paths: - src excludePaths: - vendor
# Analyse ausführen
vendor/bin/phpstan analyse
PHPStan kennt Stufen von 0 (loose) bis 9 (sehr strikt). Beim Einstieg in ein Projekt fängst du niedrig an und arbeitest dich hoch:
| Level | Was geprüft wird |
|---|---|
| 0 | Existenz von Klassen und Funktionen |
| 2 | Falsche Typen in Operatoren |
| 5 | Typen der Funktionsargumente |
| 7 | Möglicherweise null-Werte |
| 8 | Calls auf nullable-Typen |
| 9 | mixed-Werte korrekt einschränken |
// 1. Tippfehler in Property class User { public function __construct(public readonly string $name) {} } $user = new User('Marek'); echo $user->naem; // PHPStan: Access to undefined property // 2. null-Aufruf function findUser(int $id): ?User { /* ... */ } $user = findUser(42); echo $user->name; // PHPStan: $user might be null // 3. Falsche Argument-Typen function double(int $x): int { return $x * 2; } double('5'); // PHPStan: expects int, gets string
Je präziser deine PHPDoc-Annotationen, desto mehr findet PHPStan. Besonders mächtig bei Arrays:
/** * @param array<User> $users * @return array<string> */ function getNames(array $users): array { return array_map(fn($u) => $u->name, $users); } // Ohne PHPDoc würde PHPStan nicht wissen, was $users enthält // Mit PHPDoc kann es fehlerhafte Aufrufe finden: getNames([new User('M'), 'string']); // PHPStan: erwartet User, nicht string
Wenn du PHPStan zu einem bestehenden Projekt hinzufügst, sind oft hunderte Fehler in altem Code. Eine Baseline ignoriert bestehende Fehler – neue müssen gefixt werden:
vendor/bin/phpstan analyse --generate-baseline
Das erzeugt phpstan-baseline.neon. Bestehende Fehler werden eingefroren, neuer Code wird streng geprüft. Über die Zeit räumst du die Baseline ab.
composer require --dev phpunit/phpunit
// phpunit.xml <?xml version="1.0"?> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <testsuites> <testsuite name="default"> <directory>tests</directory> </testsuite> </testsuites> </phpunit>
// src/Calculator.php namespace App; class Calculator { public function add(int $a, int $b): int { return $a + $b; } public function divide(int $a, int $b): float { if ($b === 0) throw new \DivisionByZeroError(); return $a / $b; } } // tests/CalculatorTest.php namespace App\Tests; use App\Calculator; use PHPUnit\Framework\TestCase; class CalculatorTest extends TestCase { public function testAdd(): void { $calc = new Calculator(); $this->assertSame(5, $calc->add(2, 3)); } public function testDivideByZero(): void { $this->expectException(\DivisionByZeroError::class); (new Calculator())->divide(10, 0); } }
Ausführen: vendor/bin/phpunit. Du siehst grüne Punkte für jeden bestandenen Test.
Wenn du eine Methode mit vielen Input-Output-Paaren testen willst, ist ein Data Provider effizienter als einzelne Testmethoden:
use PHPUnit\Framework\Attributes\DataProvider; class CalculatorTest extends TestCase { #[DataProvider('addCases')] public function testAdd(int $a, int $b, int $expected): void { $this->assertSame($expected, (new Calculator())->add($a, $b)); } public static function addCases(): array { return [ 'positives' => [2, 3, 5], 'negatives' => [-1, -2, -3], 'mixed' => [-5, 10, 5], 'zero' => [0, 0, 0], ]; } }
PHPUnit ruft testAdd einmal pro Datensatz auf. Bei Fehler zeigt es den Key (z.B. 'negatives'), du findest den problematischen Fall sofort.
Wenn deine Klasse externe Services nutzt (DB, API), willst du im Test nicht wirklich zugreifen. Mit Mocks simulierst du das Verhalten:
public function testOrderServiceLogsCreation(): void { // Mock erstellen $logger = $this->createMock(LoggerInterface::class); // Erwarte: info wird genau 1x mit 'Order created' aufgerufen $logger->expects($this->once()) ->method('info') ->with($this->stringContains('Order created')); $service = new OrderService($logger); $service->create(new Order(42)); }
Faustregel für Anfang:
Was du nicht testen musst: Getter/Setter, fremde Library-Funktionen, Framework-Code.
assertSame im Gegensatz zu assertEquals?Du hast PHP jetzt von OOP-Patterns über funktionale Werkzeuge bis zu Production-Tools durchlaufen. Damit baust du wartbare, getestete Applikationen. Aber Praxis schlägt Theorie – setze diese Patterns in echtem Code ein.
Guide gelesen, Recall-Fragen aus jedem Kapitel beantwortet.
Drei beliebige Kapitel überfliegen, Recall-Fragen aus dem Kopf.
Mini-Projekt: REST-API mit PDO, Tests und PHPStan Level 8.
Bestehendes Projekt um Tests und Static Analysis erweitern.
Mit diesen Werkzeugen kannst du in jede Spezialisierung tiefer einsteigen:
Dieser Guide ist Teil eines Sets: