From e964c807d9e02603b63f1c73486120ec753cb174 Mon Sep 17 00:00:00 2001 From: Team3 Date: Mon, 25 May 2026 19:33:48 +0200 Subject: [PATCH] update --- backend/config.py | 2 +- backend/generator.py | 8 +- templates/Format/BeginnerGuide.md | 147 ++ templates/Format/Cheatsheet.md | 104 + templates/Format/ExtendedGuide.md | 171 ++ templates/Format/IntermediateGuide.md | 166 ++ templates/Format/MiniGuide.md | 148 ++ templates/Format/OnePager.md | 96 + templates/Referenz/BeginnerGuide.md | 1438 +++++++++++++ templates/Referenz/Cheatsheet.md | 537 +++++ templates/Referenz/ExtendedGuide.md | 2542 +++++++++++++++++++++++ templates/Referenz/IntermediateGuide.md | 1874 +++++++++++++++++ templates/Referenz/MiniGuide.md | 383 ++++ templates/Referenz/OnePager.md | 526 +++++ 14 files changed, 8137 insertions(+), 5 deletions(-) create mode 100644 templates/Format/BeginnerGuide.md create mode 100644 templates/Format/Cheatsheet.md create mode 100644 templates/Format/ExtendedGuide.md create mode 100644 templates/Format/IntermediateGuide.md create mode 100644 templates/Format/MiniGuide.md create mode 100644 templates/Format/OnePager.md create mode 100644 templates/Referenz/BeginnerGuide.md create mode 100644 templates/Referenz/Cheatsheet.md create mode 100644 templates/Referenz/ExtendedGuide.md create mode 100644 templates/Referenz/IntermediateGuide.md create mode 100644 templates/Referenz/MiniGuide.md create mode 100644 templates/Referenz/OnePager.md diff --git a/backend/config.py b/backend/config.py index b29a9c9..38aec00 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,7 +1,7 @@ from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parent.parent -DOC_DIR = PROJECT_ROOT / "doc" +TEMPLATES_DIR = PROJECT_ROOT / "templates" STORAGE_DIR = PROJECT_ROOT / "storage" DB_PATH = PROJECT_ROOT / "guides.db" diff --git a/backend/generator.py b/backend/generator.py index 7c3e9e6..afced1b 100644 --- a/backend/generator.py +++ b/backend/generator.py @@ -6,7 +6,7 @@ from pathlib import Path from config import ( CLAUDE_CLI, - DOC_DIR, + TEMPLATES_DIR, GENERATION_TIMEOUTS, MAX_CONCURRENT_GENERATIONS, MAX_ITERATIONS, @@ -80,8 +80,8 @@ async def _render_pngs(pdf_path: Path, preview_dir: Path) -> list[Path]: def _build_generator_prompt(topic: str, format_name: str, html_path: Path) -> str: - spec = (DOC_DIR / "Format" / f"{format_name}.md").read_text(encoding="utf-8") - reference = (DOC_DIR / "Referenz" / f"{format_name}.md").read_text(encoding="utf-8") + spec = (TEMPLATES_DIR / "Format" / f"{format_name}.md").read_text(encoding="utf-8") + reference = (TEMPLATES_DIR / "Referenz" / f"{format_name}.md").read_text(encoding="utf-8") return f"""Erstelle einen Lern-Guide zum Thema "{topic}" im Format "{format_name}". @@ -111,7 +111,7 @@ Führe KEIN weasyprint aus, erzeuge KEINE PDF. def _build_review_prompt(format_name: str, png_paths: list[Path], page_count: int) -> str: - spec = (DOC_DIR / "Format" / f"{format_name}.md").read_text(encoding="utf-8") + spec = (TEMPLATES_DIR / "Format" / f"{format_name}.md").read_text(encoding="utf-8") png_list = "\n".join(str(p) for p in png_paths) diff --git a/templates/Format/BeginnerGuide.md b/templates/Format/BeginnerGuide.md new file mode 100644 index 0000000..6622d78 --- /dev/null +++ b/templates/Format/BeginnerGuide.md @@ -0,0 +1,147 @@ +``` +ANFÄNGER-GUIDE-STIL (HTML/CSS → PDF via WeasyPrint) + +FORMAT +- A4 Hochformat, mehrseitig +- @page { size: A4; margin: 22mm 20mm 20mm 20mm; } +- @page :first { margin: 0; } für Cover +- Footer: Seitenzahl Mitte, Guide-Titel rechts (außer Cover) + +UMFANG +- 8-12 Kapitel à ~15 Min Lesezeit +- ~1500 Wörter Fließtext pro Kapitel +- 2-3 Code-Beispiele pro Kapitel (kurz, 5-15 Zeilen) +- 15-25 Seiten gesamt +- ~2-3h Lesezeit gesamt + +EINSTIEGSNIVEAU +- Setzt nur voraus, dass der Leser weiß, WAS das Thema ist +- Kapitel 1 erklärt Setup und erste Schritte von Null +- Keine Vorkenntnisse im Thema selbst +- Erklärt Begriffe beim ersten Auftreten + +KAPITEL-PROGRESSION +- Aufgeteilt in 3 Teile mit eigenen TOC-Sektionen: + - Teil 1: Grundlagen — Was ist es, wie startet man, erste Konzepte + - Teil 2: Strukturen/Bausteine — Die wichtigsten Mechanismen + - Teil 3: Echte Anwendungen — Integration, Tooling, kleine Projekte +- Jedes Kapitel baut auf vorherigen auf +- Tiefere Konzepte erst nach den Grundlagen +- Letztes Kapitel idealerweise mit kleiner echter Anwendung + +STRUKTUR +1. Cover: vollflächiger Hintergrund, Hero-Aussage, Outcome-Versprechen +2. Inhaltsverzeichnis: 3 Teile, nummeriert, mit Zeit-Markern (15 Min) +3. Kapitel 1-N +4. Ending: Spaced-Repetition-Plan, nächste Schritte, Begleitmaterial + +KAPITEL-AUFBAU +1. Kapitel-Head: große Nummer + Titel + Subtitle, Trennlinie +2. Gap-Opener: kursiv eingerahmt, Information-Gap explizit +3. 2-4 H2-Sektionen +4. Pro Sektion: Erklärtext + Code-Beispiel + ggf. Callout +5. Recall-Box am Ende (3 Fragen) + +ELEMENTE +- Fließtext: justify mit Silbentrennung +- Codeblöcke: dunkler Hintergrund, syntax highlighting +- Inline-Code: heller Hintergrund, Hauptfarbe +- Tabellen: Header farbig (Hauptfarbe-Dunkel), Zeilen mit dünner Trennlinie +- Callouts in 3 Varianten: tip (grün), warn (rot), note (Hauptfarbe) +- Recall-Box: dunkler Hintergrund mit Akzentfarbe + +TYPOGRAFIE +- Body: 10.5pt Serif (Charter), line-height 1.55 +- H1 Kapitel: 22pt Sans-Serif bold, Hauptfarbe-Dunkel +- H2 Sektion: 14pt Sans-Serif bold +- H3 Subsektion: 11pt Sans-Serif bold +- Code: 8.5pt Monospace, line-height 1.5 +- Inline-Code: 9pt Monospace +- Recall/Callout-Labels: 8pt uppercase, letter-spacing 1pt +- Cover-H1: 56pt Sans-Serif bold, letter-spacing -2pt + +FARBEN (max 3 + Neutrals) +- Hauptfarbe: kräftig, an offizielle Farbe des Themas anlehnen +- Hauptfarbe-Dunkel: dunklere Variante für Headings und Akzente +- Hauptfarbe-Darker: noch dunkler für Cover-Verlauf und Recall-Box +- Hintergrund-Soft: helle Variante der Hauptfarbe +- Code-Hintergrund: #1e2a3a (dunkel) +- Text: #1a1a1a / muted #5a6470 / Linie #d8dde3 +- Callout-Farben: grün/rot/Hauptfarbe + +INFORMATION-GAP-OPENER (PFLICHT pro Kapitel) +- Kursiv, eingerahmt mit Hauptfarbe-Border +- Stellt konkrete Frage, die das Kapitel beantwortet +- Erzeugt Spannung (Information-Gap) +- Niveau passend zum Kapitel: + - Kapitel 1: setzt nur Grundverständnis voraus + - Letztes Kapitel: darf auf alle vorherigen Kapitel aufbauen + +RECALL-BOX (PFLICHT pro Kapitel) +- Am Kapitel-Ende +- Dunkler Hintergrund mit Akzentfarbe +- 3 nummerierte Fragen +- Direkt auf Kapitel-Inhalt bezogen +- Code-Snippets in Fragen mit Akzentfarbe hervorgehoben + +ENDING (PFLICHT) +- Spaced-Repetition-Plan: 4 Karten (Heute, +1 Tag, +7 Tage, +30 Tage) +- "Was als nächstes lernen" mit Spezialisierungs-Vorschlägen +- Verweis auf Begleitmaterial (OnePager, Cheatsheet, Mini-Guide) + +CALLOUT-NUTZUNG +- tip (grün): Best Practice, idiomatische Lösung +- warn (rot): Fallen, häufige Bugs, Anti-Patterns +- note (Hauptfarbe): Hintergrund-Info, Querverweis + +CALLOUT-CSS WICHTIG +- .callout-body > b:first-child mit display:block für Label +- NICHT .callout-body b global mit display:block (zerstört Inline-Bold) +- Mehrzeiliger Body-Text in

wrappen wenn Inline-Bolds drin sind + +THEMENSPEZIFISCHE ANPASSUNGEN (vor Generierung wählen) +- Hauptfarbe: offizielle Farbe des Themas +- Logo-Buchstabe(n): erstes Zeichen oder Kürzel +- Version + Stand-Datum +- 8-12 Kapitel-Titel mit progressivem Aufbau + +PFLICHT-ELEMENTE PRO KAPITEL +- 1 Gap-Opener am Anfang +- 2-3 Code-Beispiele (kurz, 5-15 Zeilen) +- Mindestens 1 Callout +- 1 Recall-Box am Ende + +VERMEIDEN +- Einleitungs-Floskeln ("In diesem Kapitel lernen wir...") +- Wiederholungen aus vorherigem Kapitel +- Übersichts-Inhalt (steht im OnePager) +- Reine Referenz-Tabellen (stehen im Cheatsheet) +- Konzepte vorwegnehmen, die später dran sind +- Vorausgesetztes Wissen über das Thema +- page-break mitten in Codeblock oder Callout (page-break-inside: avoid) +- Mehr als 3 Schriftgrößen pro Sektion +- Floats oder absolute positioning (bricht in WeasyPrint) +- Subscript/Superscript via Unicode (nicht alle Fonts unterstützen das) + +GENERIERUNG MIT FEEDBACK-LOOP (max 3 Iterationen) +1. HTML schreiben +2. weasyprint file.html file.pdf (Timeout 240s) +3. PDF zu PNGs: alle Seiten konvertieren +4. Mehrere Seiten ansehen (Cover, TOC, Kapitel 1, mittlere Seite, Ending) +5. Prüfen: + - Cover randlos und ohne Footer? + - TOC zeigt alle 3 Teile? + - Kapitel 1 fängt wirklich bei Null an? + - Kapitel beginnen auf neuer Seite? + - Code-Blöcke nicht über Seitenumbruch zerrissen? + - Recall-Boxen vollständig sichtbar? + - Footer mit Seitenzahl korrekt? + - Inline-Bolds in Callouts korrekt (nicht als Blöcke)? + - Steigt der Schwierigkeitsgrad spürbar von Kapitel zu Kapitel? +6. Bei Problemen: fixen, ab Schritt 2 wiederholen +7. Nach max 3 Iterationen ausgeben + +INSTALLATION +- pip install weasyprint pdf2image +- apt install poppler-utils +``` \ No newline at end of file diff --git a/templates/Format/Cheatsheet.md b/templates/Format/Cheatsheet.md new file mode 100644 index 0000000..ed39a65 --- /dev/null +++ b/templates/Format/Cheatsheet.md @@ -0,0 +1,104 @@ +``` +CHEATSHEET-STIL QUERFORMAT (HTML/CSS → PDF via WeasyPrint) + +FORMAT +- A4 Querformat (297mm × 210mm) +- @page { size: A4 landscape; margin: 0; } +- Padding: 7mm 9mm 18mm 9mm (unten Platz für Footer) +- Position relative für absoluten Footer + +LAYOUT +- Grid: hero (auto) / main (4 Spalten) / footer (absolute) +- Main: 4 Spalten gleich breit, gap 3mm +- Spalten intern: flex-column, gap 3mm +- 8-12 thematische Blöcke verteilt (2-3 pro Spalte) + +STRUKTUR (in dieser Reihenfolge) +1. Hero: Logo links (15mm), Titel + Untertitel mittig, Version + Stand rechts +2. Main-Grid: 8-12 Blöcke in 4 Spalten +3. Footer: farbige Box mit Quick-Commands + Tag (absolute, unten) + +UNTERSCHIEDE ZU HOCHFORMAT-CHEATSHEET +- 4 Spalten statt 3 (mehr horizontaler Platz) +- 9-12 Blöcke statt 8-10 +- Footer mit 4 Quick-Commands statt 3 +- Kleinere Schriftgrößen (8pt Body statt 8.5pt) +- Kompaktere Code-Blöcke (6.5pt statt 6.8pt) + +BLOCK-AUFBAU +- Block-Head: Hauptfarbe-Hintergrund, weißer Text, Icon links (3.5mm) +- Block-Body: weißer Hintergrund, dünner Rand, abgerundet 2mm +- Inhalt: dichte Referenz, nicht Erklärung +- Varianten: Referenz-Tabelle, Code-Block, Kachel-Grid, Plus/Minus + +FARBEN (max 3 + Neutrals) +- Hauptfarbe: an offizielle Farbe des Themas anlehnen +- Hauptfarbe-Dunkel: dunklere Variante für Headings +- Hauptfarbe-Darker: noch dunkler für Footer +- Akzentfarbe für Plus/Minus: grün/rot +- Hintergrund-Soft: helle Variante der Hauptfarbe +- Code-Hintergrund: #1e2a3a (dunkel) +- Text: #1a1a1a / muted #5a6470 / Linie #d8dde3 + +TYPOGRAFIE +- Body: 8pt, line-height 1.35 +- Hero h1: 18pt bold +- Block-Head: 8pt bold uppercase, letter-spacing 0.5pt +- Tabellen: 7.5pt, Keys 7pt monospace +- Code: 6.5pt monospace, line-height 1.4 +- Inline-Code: 7pt monospace, Hauptfarbe +- Max 3 Schriftgrößen pro Block + +ICONS +- SVG inline, stroke statt fill +- 3.5mm in Block-Heads +- 2mm in Tile-Icons (kleiner als Hochformat) +- currentColor für automatische Anpassung + +BLOCK-TYPEN (für Variation nutzen) +- Referenz-Tabelle: 2-spaltig (Befehl/Methode → Bedeutung) +- Code-Block: vollständiges Beispiel mit Syntax-Highlighting +- Kachel-Grid: 2x4 mit Icons (z.B. Ökosystem) +- Direktiven-Grid: 2-spaltig kompakte Begriffe + Kurzbeschreibung +- Plus/Minus-Split: 2-spaltig (idiomatisch vs vermeiden) + +THEMENSPEZIFISCHE ANPASSUNGEN (vor Generierung wählen) +- Hauptfarbe: offizielle Farbe des Themas +- Logo-Buchstabe(n) oder Kürzel +- Version + Stand-Datum +- Block-Auswahl: 8-12 wichtigste Referenz-Themen +- Quick-Commands im Footer: 4 wichtigste Kommandos + +VISUELLE ELEMENTE PFLICHT +- Mindestens 1 Code-Block (oft mehrere im Querformat) +- Mindestens 1 Kachel-Grid mit Icons +- Mindestens 1 Plus/Minus-Split +- Footer mit Quick-Commands +- Versionsbadge in Hero + +VERMEIDEN +- Reine Bullet-Listen ohne Struktur +- Erklärtext (gehört in Guide, nicht Cheatsheet) +- Mehr als 12 Blöcke (überfüllt) +- Mehr als 3 Schriftgrößen +- Vertikal sehr lange Blöcke (Spalten unbalanciert) +- Floats oder absolute positioning (außer für Footer) + +GENERIERUNG MIT FEEDBACK-LOOP (max 3 Iterationen) +1. HTML schreiben +2. weasyprint file.html file.pdf +3. PDF zu PNG: python -c "from pdf2image import convert_from_path; convert_from_path('file.pdf', dpi=120)[0].save('preview.png')" +4. Preview ansehen mit Read-Tool +5. Prüfen: + - Footer überlappt nicht mit Inhalt? + - 4 Spalten balanciert (ähnliche Höhe)? + - Alle Blöcke vollständig sichtbar? + - Code-Blöcke nicht abgeschnitten? + - Icons rendern? +6. Bei Problemen: Inhalt straffen oder padding-bottom erhöhen, ab Schritt 2 wiederholen +7. Nach max 3 Iterationen ausgeben + +INSTALLATION +- pip install weasyprint pdf2image +- apt install poppler-utils +``` \ No newline at end of file diff --git a/templates/Format/ExtendedGuide.md b/templates/Format/ExtendedGuide.md new file mode 100644 index 0000000..e226c78 --- /dev/null +++ b/templates/Format/ExtendedGuide.md @@ -0,0 +1,171 @@ +``` +EXTENDED-GUIDE-STIL (HTML/CSS → PDF via WeasyPrint) + +FORMAT +- A4 Hochformat, mehrseitig +- @page { size: A4; margin: 22mm 20mm 20mm 20mm; } +- @page :first { margin: 0; } für Cover +- Footer: Seitenzahl Mitte, Guide-Titel rechts (außer Cover) + +UMFANG +- 15-20 Kapitel à ~15 Min Lesezeit +- ~1500 Wörter Fließtext pro Kapitel (gleich wie alle anderen Stufen) +- 2-3 Code-Beispiele pro Kapitel (gleich wie alle anderen, 5-15 Zeilen) +- 50-70 Seiten gesamt +- ~4-5h Lesezeit gesamt + +EINSTIEGSNIVEAU +- Setzt Anfänger- UND Fortgeschritten-Guide voraus +- Grundbegriffe und fortgeschrittene Patterns werden NICHT mehr erklärt +- Verweist bei Bedarf auf vorherige Guides +- Geht direkt in Internals, Spezial-Themen und Edge-Cases + +UNTERSCHIED ZU FORTGESCHRITTEN-GUIDE +- Kapitel-Größe IDENTISCH (1500 Wörter, 2-3 Code-Beispiele, 15 Min) +- Unterschied liegt nur in: + - THEMEN (Experten/Nischen, Internals, Sprach-Mechanismen) + - VORAUSGESETZTEM WISSEN (alle Patterns aus Fortgeschritten) + - KAPITEL-ANZAHL (15-20 statt 12-15) + - "WARUM" über "WIE" (mehr Trade-Offs, mehr Design-Entscheidungen) + +KAPITEL-PROGRESSION +- Aufgeteilt in 3 Teile mit eigenen TOC-Sektionen +- Beispiele für PHP: + - Teil 1: Sprach-Internals — Reflection, SPL, Stream-Wrapper, GC, FFI + - Teil 2: Performance & Async — OpCache, Fibers, ReactPHP, Profiling, Caching + - Teil 3: Architektur & Patterns — DI-Container, Event-Dispatcher, CQRS, DDD, Hexagonal, Event Sourcing +- Jedes Kapitel geht in Bereiche, wo die meisten Entwickler nicht hingehen +- Bewusst dort, wo Mainstream-Tutorials aufhören + +STRUKTUR +1. Cover: vollflächiger Hintergrund, Hero-Aussage mit "an der Grenze" oder ähnlichem Tone +2. Inhaltsverzeichnis: 3 Teile, nummeriert, Zeit-Marker (15 Min) - kann auf zwei Seiten brechen +3. Kapitel 1-N +4. Ending: Spaced-Repetition-Plan, nächste Schritte (außerhalb des offiziellen Lernpfads), Begleitmaterial + +KAPITEL-AUFBAU +1. Kapitel-Head: große Nummer + Titel + Subtitle, Trennlinie +2. Gap-Opener: kursiv eingerahmt, anspruchsvolles Problem oder Tief-Frage +3. 3-5 H2-Sektionen (mehr als im Fortgeschritten-Guide) +4. Pro Sektion: Erklärtext + Code-Beispiel + ggf. Callout +5. Recall-Box am Ende (3 Fragen, anspruchsvoller als alle Stufen davor) + +CODE-BEISPIELE +- Production-Code-Niveau, keine Demos +- Echte Library-Namen (Symfony, Doctrine, ReactPHP, EventSauce) +- Internals-Code zur Erklärung von PHP-Mechanismen +- Vereinfachte Implementierungen echter Frameworks +- Länge bleibt 5-15 Zeilen (wie alle anderen Stufen) + +ELEMENTE +- Fließtext: justify mit Silbentrennung +- Codeblöcke: dunkler Hintergrund, syntax highlighting +- Inline-Code: heller Hintergrund, Hauptfarbe +- Tabellen: Header farbig (Hauptfarbe-Dunkel), Vergleichstabellen mit Trade-Offs +- Callouts in 3 Varianten: tip (grün), warn (rot), note (Hauptfarbe) +- Recall-Box: dunkler Hintergrund mit Akzentfarbe + +TYPOGRAFIE +- Body: 10.5pt Serif (Charter), line-height 1.55 +- H1 Kapitel: 22pt Sans-Serif bold, Hauptfarbe-Dunkel +- H2 Sektion: 14pt Sans-Serif bold +- H3 Subsektion: 11pt Sans-Serif bold +- Code: 8.5pt Monospace, line-height 1.5 +- Inline-Code: 9pt Monospace +- Recall/Callout-Labels: 8pt uppercase, letter-spacing 1pt +- Cover-H1: 56pt Sans-Serif bold, letter-spacing -2pt + +FARBEN (max 3 + Neutrals) +- Hauptfarbe: kräftig, an offizielle Farbe des Themas anlehnen +- Hauptfarbe-Dunkel: dunklere Variante für Headings und Akzente +- Hauptfarbe-Darker: noch dunkler für Cover-Verlauf und Recall-Box +- Hintergrund-Soft: helle Variante der Hauptfarbe +- Code-Hintergrund: #1e2a3a (dunkel) +- Text: #1a1a1a / muted #5a6470 / Linie #d8dde3 +- Callout-Farben: grün/rot/Hauptfarbe + +INFORMATION-GAP-OPENER (PFLICHT pro Kapitel) +- Kursiv, eingerahmt mit Hauptfarbe-Border +- Anspruchsvolles Problem oder Internals-Frage als Aufhänger +- Niveau: setzt fortgeschrittene Praxis voraus +- Beispiele: + - "Wie weiß Symfony zur Laufzeit, welche Routen in deinem Controller stecken?" + - "PHP räumt Speicher automatisch auf – meistens. Aber in Long-Running-Prozessen..." + - "Anämische Entities sind in PHP weit verbreitet..." + +RECALL-BOX (PFLICHT pro Kapitel) +- Am Kapitel-Ende +- 3 Fragen, anspruchsvoller als Fortgeschritten +- Fragen nach Warum/Wofür/Wann genau statt Was/Wie +- Code-Snippets in Fragen mit Akzentfarbe hervorgehoben + +ENDING (PFLICHT) +- Spaced-Repetition-Plan: 4 Karten (Heute, +7 Tage, +30 Tage, +90 Tage) + - Sehr langfristige Spacing-Abstände + - Aufgaben anspruchsvoll (eigenes Spezialprojekt, Source-Code lesen) +- "Was als nächstes lernen" — bewusst außerhalb offizieller Lernpfade + (Source-Code, eigene Extensions, Sprach-Design, RFCs) +- Verweis auf ALLE Begleitmaterialien (komplette Reihe) + +CALLOUT-NUTZUNG +- tip (grün): Best Practice für Spezial-Cases, Library-Empfehlung +- warn (rot): subtile Fallen, Architektur-Anti-Patterns, Komplexitäts-Warnungen +- note (Hauptfarbe): Hintergrund-Info, alternative Lösung, "wann lohnt sich das" + +CALLOUT-CSS WICHTIG +- .callout-body > b:first-child mit display:block für Label +- NICHT .callout-body b global mit display:block (zerstört Inline-Bold) +- Mehrzeiliger Body-Text in

wrappen wenn Inline-Bolds drin sind + +GAP-CSS WICHTIG +- .gap > b:first-child mit display:block für "FRAGE ZUM EINSTIEG"-Label +- NICHT .gap b global mit display:block (zerstört Inline-Bold im Frage-Text) +- Bei Inline-Bolds im Gap-Text wird sonst jedes zum Block + +THEMENSPEZIFISCHE ANPASSUNGEN (vor Generierung wählen) +- Hauptfarbe: offizielle Farbe des Themas +- Logo-Buchstabe(n) oder Kürzel +- Version + Stand-Datum +- 15-20 Kapitel-Titel: Internals, Spezial-Themen, Production-Edge +- Themen, die der Mainstream-Entwickler nicht täglich braucht + +PFLICHT-ELEMENTE PRO KAPITEL +- 1 Gap-Opener am Anfang +- 2-3 Code-Beispiele (5-15 Zeilen, gleich wie alle anderen Stufen) +- Mindestens 1 Callout +- 1 Recall-Box am Ende + +VERMEIDEN +- Wiederholung von Anfänger- und Fortgeschritten-Themen +- Einleitungs-Floskeln ("In diesem Kapitel lernen wir...") +- Übersichts-Inhalt (steht im OnePager) +- Reine Referenz-Tabellen (stehen im Cheatsheet) +- Toy-Beispiele (Production-Niveau zeigen) +- Themen, die der Mainstream-Entwickler täglich braucht + (gehören in Anfänger oder Fortgeschritten) +- page-break mitten in Codeblock oder Callout (page-break-inside: avoid) +- Mehr als 3 Schriftgrößen pro Sektion +- Floats oder absolute positioning (bricht in WeasyPrint) + +GENERIERUNG MIT FEEDBACK-LOOP (max 3 Iterationen) +1. HTML schreiben (sehr langes Dokument, ~3500-5000 Zeilen typisch) +2. weasyprint file.html file.pdf (Timeout 300s, große Datei) +3. PDF zu PNGs: alle Seiten konvertieren (dpi=90 für Memory-Effizienz) +4. Schlüsselseiten ansehen: Cover, TOC, Kapitel 1, mittlere Seite, Ending +5. Prüfen: + - Cover randlos und ohne Footer? + - TOC zeigt alle 3 Teile? (kann auf 2 Seiten brechen bei 15+ Kapiteln, OK) + - Kapitel beginnen auf neuer Seite? + - Code-Blöcke nicht über Seitenumbruch zerrissen? + - Recall-Boxen vollständig sichtbar? + - Footer mit Seitenzahl korrekt? + - Setzt der Guide spürbar Anfänger+Fortgeschritten-Wissen voraus? + - Sind Themen wirklich Experten-/Nischen-Niveau? + - Inline-Bolds in Gap-Openers und Callouts korrekt (nicht als Blöcke)? +6. Bei Problemen: fixen, ab Schritt 2 wiederholen +7. Nach max 3 Iterationen ausgeben + +INSTALLATION +- pip install weasyprint pdf2image +- apt install poppler-utils +``` \ No newline at end of file diff --git a/templates/Format/IntermediateGuide.md b/templates/Format/IntermediateGuide.md new file mode 100644 index 0000000..f8f28f1 --- /dev/null +++ b/templates/Format/IntermediateGuide.md @@ -0,0 +1,166 @@ +``` +FORTGESCHRITTEN-GUIDE-STIL (HTML/CSS → PDF via WeasyPrint) + +FORMAT +- A4 Hochformat, mehrseitig +- @page { size: A4; margin: 22mm 20mm 20mm 20mm; } +- @page :first { margin: 0; } für Cover +- Footer: Seitenzahl Mitte, Guide-Titel rechts (außer Cover) + +UMFANG +- 12-15 Kapitel à ~15 Min Lesezeit +- ~1500 Wörter Fließtext pro Kapitel (gleich wie Anfänger) +- 2-3 Code-Beispiele pro Kapitel (gleich wie Anfänger, 5-15 Zeilen) +- 30-50 Seiten gesamt +- ~3-4h Lesezeit gesamt + +EINSTIEGSNIVEAU +- Setzt Anfänger-Guide oder gleichwertiges Vorwissen voraus +- Grundbegriffe werden NICHT mehr erklärt +- Verweist bei Bedarf auf Anfänger-Guide +- Geht direkt in fortgeschrittene Patterns und Production-Tools + +UNTERSCHIED ZU ANFÄNGER-GUIDE +- Kapitel-Größe IDENTISCH (1500 Wörter, 2-3 Code-Beispiele, 15 Min) +- Unterschied liegt nur in: + - THEMEN (anspruchsvoller, weniger bekannt) + - VORAUSGESETZTEM WISSEN (Grundlagen werden nicht wiederholt) + - REIFE der Code-Beispiele (Production-nah statt Demo) + - KAPITEL-ANZAHL (12-15 statt 8-12) + +KAPITEL-PROGRESSION +- Aufgeteilt in 3 Teile mit eigenen TOC-Sektionen +- Beispiele für PHP: + - Teil 1: OOP-Patterns — Interfaces, Traits, Enums, Attribute + - Teil 2: Funktional & Generators — Closures, Higher-Order, Generators, Generics + - Teil 3: Production-Tools — PDO, HTTP-Clients, Static Analysis, Tests +- Jedes Kapitel ist tiefer als ein Anfänger-Kapitel +- Mehr Edge Cases, mehr "warum genau so" +- Production-Code-Niveau + +STRUKTUR +1. Cover: vollflächiger Hintergrund, Hero-Aussage mit "tiefer" oder ähnlichem Tone +2. Inhaltsverzeichnis: 3 Teile, nummeriert, mit Zeit-Markern (15 Min) +3. Kapitel 1-12 +4. Ending: Spaced-Repetition-Plan, nächste Schritte, Begleitmaterial + +KAPITEL-AUFBAU +1. Kapitel-Head: große Nummer + Titel + Subtitle, Trennlinie +2. Gap-Opener: kursiv eingerahmt, konkretes Praxis-Problem +3. 2-4 H2-Sektionen +4. Pro Sektion: Erklärtext + Code-Beispiel + ggf. Callout +5. Recall-Box am Ende (3 Fragen, anspruchsvoller als im Anfänger) + +CODE-BEISPIELE +- Realistischer als im Anfänger-Guide (kein "foo/bar") +- Production-nahe Patterns +- TypeScript/Type-Hints idiomatisch +- Echte Library-Namen (Guzzle, Doctrine, PHPStan, Symfony) +- Mehr Edge Cases zeigen +- Länge bleibt 5-15 Zeilen (wie Anfänger) + +ELEMENTE +- Fließtext: justify mit Silbentrennung +- Codeblöcke: dunkler Hintergrund, syntax highlighting +- Inline-Code: heller Hintergrund, Hauptfarbe +- Tabellen: Header farbig (Hauptfarbe-Dunkel) +- Callouts in 3 Varianten: tip (grün), warn (rot), note (Hauptfarbe) +- Recall-Box: dunkler Hintergrund mit Akzentfarbe + +TYPOGRAFIE +- Body: 10.5pt Serif (Charter), line-height 1.55 +- H1 Kapitel: 22pt Sans-Serif bold, Hauptfarbe-Dunkel +- H2 Sektion: 14pt Sans-Serif bold +- H3 Subsektion: 11pt Sans-Serif bold +- Code: 8.5pt Monospace, line-height 1.5 +- Inline-Code: 9pt Monospace +- Recall/Callout-Labels: 8pt uppercase, letter-spacing 1pt +- Cover-H1: 56pt Sans-Serif bold, letter-spacing -2pt + +FARBEN (max 3 + Neutrals) +- Hauptfarbe: kräftig, an offizielle Farbe des Themas anlehnen +- Hauptfarbe-Dunkel: dunklere Variante für Headings und Akzente +- Hauptfarbe-Darker: noch dunkler für Cover-Verlauf und Recall-Box +- Hintergrund-Soft: helle Variante der Hauptfarbe +- Code-Hintergrund: #1e2a3a (dunkel) +- Text: #1a1a1a / muted #5a6470 / Linie #d8dde3 +- Callout-Farben: grün/rot/Hauptfarbe + +INFORMATION-GAP-OPENER (PFLICHT pro Kapitel) +- Kursiv, eingerahmt mit Hauptfarbe-Border +- Konkretes Praxis-Problem als Aufhänger +- Niveau: setzt Anfänger-Wissen voraus +- Beispiele: + - "Du hast drei verschiedene Logger – Datei, Datenbank, Sentry..." + - "Du sollst die Zeilen einer 5-GB-Logdatei verarbeiten..." + - "SQL-Injection ist seit 20 Jahren die häufigste Web-Sicherheitslücke..." + +RECALL-BOX (PFLICHT pro Kapitel) +- Am Kapitel-Ende +- 3 Fragen, anspruchsvoller als im Anfänger-Guide +- Fragen nach Wann/Warum/Wofür statt Was/Wie +- Code-Snippets in Fragen mit Akzentfarbe hervorgehoben + +ENDING (PFLICHT) +- Spaced-Repetition-Plan: 4 Karten (Heute, +3 Tage, +14 Tage, +60 Tage) + - Anspruchsvollere Spacing-Abstände als im Anfänger-Guide + - Aufgaben anspruchsvoller (echtes Projekt aufsetzen) +- "Was als nächstes lernen" mit Spezialisierungs-Vorschlägen Richtung Extended +- Verweis auf alle Begleitmaterialien (OnePager, Cheatsheet, Mini, Anfänger) + +CALLOUT-NUTZUNG +- tip (grün): Best Practice, idiomatische Lösung, Library-Empfehlung +- warn (rot): Fallen, häufige Anti-Patterns, Sicherheits-Risiken +- note (Hauptfarbe): Hintergrund-Info, alternative Lösung, Querverweis + +CALLOUT-CSS WICHTIG +- .callout-body > b:first-child mit display:block für Label +- NICHT .callout-body b global mit display:block (zerstört Inline-Bold) +- Mehrzeiliger Body-Text in

wrappen wenn Inline-Bolds drin sind + +THEMENSPEZIFISCHE ANPASSUNGEN (vor Generierung wählen) +- Hauptfarbe: offizielle Farbe des Themas +- Logo-Buchstabe(n) oder Kürzel +- Version + Stand-Datum +- 12-15 Kapitel-Titel: fortgeschrittene Patterns, Tools, Production-Aspekte +- Keine Wiederholung der Anfänger-Grundlagen + +PFLICHT-ELEMENTE PRO KAPITEL +- 1 Gap-Opener am Anfang +- 2-3 Code-Beispiele (5-15 Zeilen, gleich wie Anfänger) +- Mindestens 1 Callout +- 1 Recall-Box am Ende + +VERMEIDEN +- Wiederholung von Grundlagen aus dem Anfänger-Guide +- Einleitungs-Floskeln ("In diesem Kapitel lernen wir...") +- Übersichts-Inhalt (steht im OnePager) +- Reine Referenz-Tabellen (stehen im Cheatsheet) +- Toy-Beispiele wie foo/bar (Production-Code zeigen) +- Themen, die in Anfänger oder Extended besser passen +- page-break mitten in Codeblock oder Callout (page-break-inside: avoid) +- Mehr als 3 Schriftgrößen pro Sektion +- Floats oder absolute positioning (bricht in WeasyPrint) + +GENERIERUNG MIT FEEDBACK-LOOP (max 3 Iterationen) +1. HTML schreiben +2. weasyprint file.html file.pdf (Timeout 300s) +3. PDF zu PNGs: alle Seiten konvertieren +4. Schlüsselseiten ansehen: Cover, TOC, Kapitel 1, mittlere Seite, Ending +5. Prüfen: + - Cover randlos und ohne Footer? + - TOC zeigt alle 3 Teile? + - Kapitel beginnen auf neuer Seite? + - Code-Blöcke nicht über Seitenumbruch zerrissen? + - Recall-Boxen vollständig sichtbar? + - Footer mit Seitenzahl korrekt? + - Setzt der Guide spürbar Anfänger-Wissen voraus? + - Sind Code-Beispiele realistisch (kein foo/bar)? + - Inline-Bolds in Callouts korrekt (nicht als Blöcke)? +6. Bei Problemen: fixen, ab Schritt 2 wiederholen +7. Nach max 3 Iterationen ausgeben + +INSTALLATION +- pip install weasyprint pdf2image +- apt install poppler-utils +``` \ No newline at end of file diff --git a/templates/Format/MiniGuide.md b/templates/Format/MiniGuide.md new file mode 100644 index 0000000..b3c878b --- /dev/null +++ b/templates/Format/MiniGuide.md @@ -0,0 +1,148 @@ +``` +MINI-GUIDE-STIL (HTML/CSS → PDF via WeasyPrint) + +FORMAT +- A4 Hochformat, 3-4 Seiten +- @page { size: A4; margin: 18mm 18mm 16mm 18mm; } +- Footer: Seitenzahl Mitte, Guide-Titel rechts + +UMFANG (einheitlich mit allen Guide-Stufen) +- 1 Kapitel (oder besser: 4-6 Sektionen ohne Kapitel-Struktur) +- ~1500 Wörter Fließtext +- 5-7 Code-Beispiele (sehr kurz, 2-7 Zeilen) +- ~15 Min Lesezeit +- 3-4 Seiten + +ZIELGRUPPE — KOMPAKTER ANFÄNGER-EINSTIEG +- Echte Anfänger ohne Programmier-Vorwissen im Thema +- Setzt nur allgemeines Verständnis voraus ("was ist Programmieren") +- Begriffe werden bei erstem Auftreten erklärt +- KEIN Sprach-Charakter-Überblick für erfahrene Entwickler +- KEINE fortgeschrittenen Features (auch wenn cool und kurz) + +INHALTLICHE PRINZIPIEN +- Nur absolute Basics zeigen +- Themen, die jemand nach 15 Min selbst nachmachen kann +- Keine Tooling-Komplexität (Paketmanager, Build-Systeme, Compiler) +- Keine Sprach-Spezialitäten (Type-Systeme, Decorators, Generics) +- Keine OOP, wenn möglich (oder nur trivialste Form) +- Erklär-Tiefe vor Feature-Breite +- Lieber 5 Konzepte gründlich als 15 oberflächlich + +TYPISCHE 5-SEKTIONEN-STRUKTUR +1. Sprache starten — Installation, erste Datei, erstes Programm +2. Variablen — Konzept + 2-3 Basis-Typen +3. Kontrollfluss — if/else mit einfachem Beispiel +4. Listen + Iteration — Array + Schleife +5. Funktionen — Deklaration + Aufruf + Rückgabe + +(Diese Reihenfolge baut aufeinander auf und endet mit etwas Sinnvollem.) + +STRUKTUR +1. Kompakter Head: Logo links (16mm), Titel + Subtitle mittig, Badge + Zeit rechts +2. Gap-Opener: Frage zum Einstieg, kursiv eingerahmt, niedrigschwellig +3. 4-6 H2-Sektionen mit Erklärtext + Code-Beispiel + ggf. Callout + +ELEMENTE +- Fließtext: justify mit Silbentrennung +- Codeblöcke: dunkler Hintergrund, syntax highlighting, sehr kurz (2-7 Zeilen) +- Inline-Code: heller Hintergrund, Hauptfarbe +- Tabellen: nur wenn wirklich nötig (Vergleichs-Operatoren o.ä.) +- Callouts in 3 Varianten: tip (grün), warn (rot), note (Hauptfarbe) + +TYPOGRAFIE +- Body: 10.5pt Serif (Charter), line-height 1.55 +- Head h1: 20pt Sans-Serif bold +- H2 Sektion: 13pt Sans-Serif bold +- Code: 8.5pt Monospace, line-height 1.5 +- Inline-Code: 9pt Monospace +- Callout-Labels: 8pt uppercase, letter-spacing 1pt + +FARBEN (max 3 + Neutrals) +- Hauptfarbe: kräftig, an offizielle Farbe des Themas anlehnen +- Hauptfarbe-Dunkel: dunklere Variante für Akzente +- Hintergrund-Soft: helle Variante der Hauptfarbe +- Code-Hintergrund: #1e2a3a +- Text: #1a1a1a / muted #5a6470 / Linie #d8dde3 +- Callout-Farben: grün/rot/Hauptfarbe + +GAP-OPENER (PFLICHT) +- Kursiv, eingerahmt mit Hauptfarbe-Border +- Niedrigschwellige Frage, die der Guide beantwortet +- Begeistert mit relevanter Statistik oder Praxis-Bezug +- KEINE Fachbegriffe, die noch nicht erklärt sind +- Beispiele: + - "PHP läuft hinter rund drei Viertel aller Webseiten..." + - "JavaScript ist die Sprache des Webs – aber wie schreibt man das eigentliche Code..." + - "Python ist die beliebteste Anfänger-Sprache..." + +ERKLÄR-TIEFE PRO KONZEPT +- Konzept benennen (z.B. "Variable") +- In einem Satz erklären, was es ist +- Code-Beispiel mit Kommentaren +- Sprach-Eigenheiten erwähnen (z.B. "in PHP beginnen Variablen mit $") +- KEIN Verweis auf andere Konzepte, die noch kommen + +CALLOUT-NUTZUNG +- tip (grün): Übungs-Anregung am Ende, ermutigend +- warn (rot): Anfänger-Stolperfallen ("= vs ==", "vergessenes Semikolon") +- note (Hauptfarbe): Hintergrund-Info, Erklärung einer Sprach-Eigenheit + +CALLOUT-CSS WICHTIG +- .callout-body > b:first-child mit display:block für Label +- NICHT .callout-body b global mit display:block (zerstört Inline-Bold) + +GAP-CSS WICHTIG +- .gap > b:first-child mit display:block für "FRAGE ZUM EINSTIEG"-Label +- NICHT .gap b global mit display:block (zerstört Inline-Bold im Frage-Text) + +THEMENSPEZIFISCHE ANPASSUNGEN (vor Generierung wählen) +- Hauptfarbe: offizielle Farbe des Themas +- Logo-Buchstabe(n) oder Kürzel +- Begrüßungs-Statistik im Gap-Opener +- 4-6 Anfänger-Themen wählen (siehe Standard-Struktur) + +PFLICHT-ELEMENTE +- 1 Gap-Opener am Anfang +- 5-7 Code-Beispiele (kurz, 2-7 Zeilen, anfänger-tauglich) +- Mindestens 1 Callout (oft: warn für Stolperfalle, tip für Übung am Ende) +- Inline-Codes für Fachbegriffe + +VERMEIDEN +- TOC oder Cover (überdimensioniert für 15 Min) +- Einleitungs-Floskeln ("In diesem Mini-Guide lernen wir...") +- Vollständigkeitsanspruch (gehört in größeren Guide) +- Referenz-Tabellen ohne Erklärtext (gehört in Cheatsheet) +- Recall oder Next-Step am Ende (Mini-Guide endet mit Inhalt) +- Themen, die fortgeschritten sind (auch wenn cool): + - Type-Systems, Type-Hints, Generics + - OOP-Features (außer trivialster Form) + - Tooling (Paketmanager, Build, Linting) + - Sprach-Spezialitäten (PHP: strict_types, readonly, Composer, PSR-4) +- page-break mitten in Codeblock oder Callout (page-break-inside: avoid) +- Mehr als 3 Schriftgrößen pro Sektion +- Floats oder absolute positioning (bricht in WeasyPrint) +- Fachbegriffe ohne Erklärung +- Verweise auf andere Konzepte, die noch kommen +- Edge Cases und "aber"-Sätze + +GENERIERUNG MIT FEEDBACK-LOOP (max 3 Iterationen) +1. HTML schreiben +2. weasyprint file.html file.pdf +3. PDF zu PNGs: alle Seiten konvertieren +4. Alle Seiten ansehen +5. Prüfen: + - Head sauber (Logo überlappt nicht mit Titel)? + - Code-Blöcke nicht über Seitenumbruch zerrissen? + - Callouts vollständig sichtbar? + - Inline-Bolds in Callouts/Gap korrekt (nicht als Blöcke)? + - Footer mit Seitenzahl korrekt? + - Würde ein echter Anfänger das verstehen? + - Wurden alle Fachbegriffe beim ersten Auftreten erklärt? +6. Bei Problemen: fixen, ab Schritt 2 wiederholen +7. Nach max 3 Iterationen ausgeben + +INSTALLATION +- pip install weasyprint pdf2image +- apt install poppler-utils +``` \ No newline at end of file diff --git a/templates/Format/OnePager.md b/templates/Format/OnePager.md new file mode 100644 index 0000000..3c6a2b1 --- /dev/null +++ b/templates/Format/OnePager.md @@ -0,0 +1,96 @@ +``` +ONEPAGER-STIL QUERFORMAT (HTML/CSS → PDF via WeasyPrint) + +FORMAT +- A4 Querformat (297mm × 210mm) +- @page { size: A4 landscape; margin: 0; } +- Padding: 9mm 11mm 9mm 11mm + +LAYOUT +- Grid: hero+stats (auto) / divider / main (1fr, 3 Spalten) / footer (absolute) +- Stats-Bar in Hero integriert (rechts), spart vertikalen Platz +- Main: 3 Spalten gleich breit, gap 4mm +- Spalten intern: flex-column, gap 4mm +- 6 thematische Blöcke verteilt (2 pro Spalte) + +STRUKTUR (in dieser Reihenfolge) +1. Hero: Logo links (22mm), Titel + Untertitel mittig, 4 Stats rechts +2. Divider: 1.5pt schwarze Linie +3. Main-Grid: 6 Blöcke in 3 Spalten +4. Footer: farbige Box mit Kernaussage + Tag (absolute, unten) + +UNTERSCHIEDE ZU HOCHFORMAT +- 3 Spalten statt 2 (mehr horizontaler Platz) +- 6 Blöcke statt 4-5 +- Stats integriert in Hero statt eigene Zeile +- Kleinere Schriftgrößen (9.5pt Body statt 10pt) +- Kompaktere Code-Blöcke (7pt statt 7.5pt) + +BLOCK-AUFBAU +- Titel: 9.5pt bold uppercase, Icon links, Hauptfarbe-Unterstreichung 2pt +- Inhalt: visuell, nicht reine Textbullets +- Varianten: Icon-Liste, Code-Block, Kachel-Grid, Plus/Minus-Spalten, Type-Grid + +FARBEN (max 3 + Neutrals) +- Hauptfarbe: an offizielle Farbe des Themas anlehnen +- Hauptfarbe-Dunkel: für Headings +- Hauptfarbe-Darker: für Footer +- Akzentfarbe: kontrastierend +- Hintergrund-Soft: helle Variante der Hauptfarbe +- Code-Hintergrund: #1e2a3a +- Text: #1a1a1a / muted #5a6470 / Linie #e5e5e5 + +TYPOGRAFIE +- Body: 9.5pt, line-height 1.4 +- Hero h1: 20pt bold +- Block-Titel: 9.5pt bold uppercase, letter-spacing 0.5pt +- Stats-Zahl: 14pt bold, Label 6.5pt uppercase +- Code: 7pt monospace, dunkler Hintergrund +- Feature-Text: 8.5pt +- Max 3 Schriftgrößen pro Block + +ICONS +- SVG inline, stroke statt fill +- 4mm in Block-Titeln (kleiner als Hochformat wegen kompakter Layout) +- 2.8mm in Kachel-Icons +- currentColor für automatische Anpassung + +THEMENSPEZIFISCHE ANPASSUNGEN (vor Generierung wählen) +- Hauptfarbe: offizielle Farbe des Themas +- Logo-Buchstabe(n) oder Kürzel +- 4 Stats: themen-relevante Zahlen +- Block-Auswahl: 6 wichtigste Aspekte für Erstübersicht + +VISUELLE ELEMENTE PFLICHT +- Mindestens 1 Code-Block +- Mindestens 1 Kachel-Grid mit Icons (Ökosystem) +- Mindestens 1 Plus/Minus-Split (Modern vs Legacy o.ä.) +- Footer als farbige Box (visueller Anker) +- Stats-Bar im Hero + +VERMEIDEN +- Reine Bullet-Listen in jedem Block +- Mehr als 6 Hauptblöcke (Querformat hat eh schon mehr Platz) +- Mehr als 3 Schriftgrößen +- Marketing-Floskeln in Hero +- Floats oder absolute positioning (außer für Footer) +- Vertikal sehr lange Blöcke (würden Spalten unbalanciert machen) + +GENERIERUNG MIT FEEDBACK-LOOP (max 3 Iterationen) +1. HTML schreiben +2. weasyprint file.html file.pdf +3. PDF zu PNG: python -c "from pdf2image import convert_from_path; convert_from_path('file.pdf', dpi=120)[0].save('preview.png')" +4. Preview ansehen mit Read-Tool +5. Prüfen: + - Stats-Bar überlappt nicht mit Titel? + - 3 Spalten balanciert (ähnliche Höhe)? + - Footer nicht abgeschnitten? + - Code-Block lesbar? + - Alle Icons rendern? +6. Bei Problemen: fixen, ab Schritt 2 wiederholen +7. Nach max 3 Iterationen ausgeben + +INSTALLATION +- pip install weasyprint pdf2image +- apt install poppler-utils +``` \ No newline at end of file diff --git a/templates/Referenz/BeginnerGuide.md b/templates/Referenz/BeginnerGuide.md new file mode 100644 index 0000000..a4662b0 --- /dev/null +++ b/templates/Referenz/BeginnerGuide.md @@ -0,0 +1,1438 @@ +``` + + + + +PHP Anfänger-Guide + + + + + +

+
+ +
Anfänger-Guide · 2,5h · Stand 2026
+
+ +
+

PHP
in 10 Kapiteln.

+

Von der Installation bis zur ersten Composer-Anwendung – modern und idiomatisch.

+
+ +
+
+ Was du danach kannst + PHP-Code mit Type-Hints schreiben · Arrays und Klassen modern nutzen · Funktionen sauber strukturieren · Fehler richtig behandeln · Composer-Pakete installieren und in eigenen Projekten verwenden. +
+
PHP 8.4
+
+
+ + +
+

Inhalt

+ +
Teil 1 · Grundlagen
+ +
+
1
+
+

PHP installieren und starten

+

Setup mit Docker, DDEV oder lokal, erstes Script

+
+
15 Min
+
+ +
+
2
+
+

Variablen und Typen

+

$-Variablen, strict_types, Typumwandlung

+
+
15 Min
+
+ +
+
3
+
+

Strings und Formatierung

+

Interpolation, Heredoc, String-Funktionen

+
+
15 Min
+
+ +
Teil 2 · Strukturen
+ +
+
4
+
+

Arrays: Liste und Map

+

Indiziert, assoziativ, gemischt

+
+
15 Min
+
+ +
+
5
+
+

Control Flow

+

if, match, foreach, while

+
+
15 Min
+
+ +
+
6
+
+

Funktionen mit Type-Hints

+

Parameter, Rückgabewerte, Named Arguments

+
+
15 Min
+
+ +
Teil 3 · Objekte und echte Anwendungen
+ +
+
7
+
+

Klassen und Objekte

+

Promoted Constructor, readonly, Methoden

+
+
15 Min
+
+ +
+
8
+
+

Namespaces und Autoloading

+

PSR-4, use-Statements, Datei-Organisation

+
+
15 Min
+
+ +
+
9
+
+

Fehlerbehandlung

+

Exceptions, try/catch, eigene Fehler

+
+
15 Min
+
+ +
+
10
+
+

Composer und erste Anwendung

+

Pakete installieren, kleines CLI-Tool bauen

+
+
15 Min
+
+
+ + +
+
+
01
+
+

PHP installieren und starten

+
Setup mit Docker, DDEV oder lokal
+
+
+ +
+ Frage zum Einstieg + Du willst PHP-Code schreiben und ausführen. Aber wie kommt PHP auf deinen Rechner? Klassische XAMPP-Installation, modernes Docker, oder einfach php -S für schnelle Tests? Die richtige Wahl spart dir später Stunden. +
+ +

Drei Wege, PHP zu starten

+

Für lokale Entwicklung gibt es drei sinnvolle Optionen, jede mit ihrem Anwendungsfall:

+ + + + + + +
MethodeWann
Lokal (brew, apt)Kleine Scripts, schneller Test
DDEVMehrere Projekte mit DB
Docker direktCustom-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.

+ +

Erstes Script

+

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.

+ +

Eingebauter Web-Server

+

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 für echte Projekte + Sobald du eine Datenbank brauchst, mehrere Projekte gleichzeitig, oder ein konkretes PHP-Framework wie Shopware oder Laravel, lohnt sich DDEV – wraps Docker mit sinnvollen Defaults. ddev config in deinem Projektordner reicht meist. +
+
+ +
+ Recall +
    +
  1. Wie führst du eine PHP-Datei im Terminal aus?
  2. +
  3. Wozu ist declare(strict_types=1) gut?
  4. +
  5. Wann nutzt du php -S, wann DDEV?
  6. +
+
+
+ + +
+
+
02
+
+

Variablen und Typen

+
$-Variablen, strict_types, Typumwandlung
+
+
+ +
+ Frage zum Einstieg + PHP-Variablen beginnen mit einem $. 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. +
+ +

Variablen mit $

+

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).

+ +

Die wichtigsten Typen

+ + + + + + + + + +
TypBeispielWofür
int42Ganze Zahlen
float3.14Kommazahlen
string'Hi'Text
booltrueWahrheitswert
array[1, 2]Liste oder Map
nullnullKein Wert
+ +

Den Typ einer Variable findest du mit gettype($x) oder mit Funktionen wie is_int($x), is_string($x).

+ +

Typumwandlung

+

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';
+}
+ +
+
!
+
+ Vergleich mit == ist tückisch + '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. +
+
+ +
+ Recall +
    +
  1. Wie erkennst du in PHP-Code, ob etwas eine Variable ist?
  2. +
  3. Was ist der Unterschied zwischen == und ===?
  4. +
  5. Welche Werte werden bei Boolean-Cast zu false?
  6. +
+
+
+ + +
+
+
03
+
+

Strings und Formatierung

+
Interpolation, Heredoc, String-Funktionen
+
+
+ +
+ Frage zum Einstieg + In PHP gibt es einfache und doppelte Anführungszeichen für Strings. Beide funktionieren – aber sie verhalten sich unterschiedlich. Warum gibt es überhaupt zwei, und welche solltest du wann nutzen? +
+ +

Strings: '' vs ""

+

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.

+ +

Heredoc und Nowdoc für Mehrzeiler

+

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;
+ +

Wichtige String-Funktionen

+

PHP hat eine riesige Auswahl an String-Funktionen. Diese kennst du nach einer Woche auswendig:

+ + + + + + + + + + + +
FunktionEffekt
strlen($s)Länge des Strings
strtoupper / strtolowerGroß-/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"
+ +
+
+
+ Methoden-Verkettung mit Pipes nicht möglich + Anders als bei Objektmethoden gibt es bei String-Funktionen keine Verkettung. trim(strtolower($s)) ist die Schreibweise – von innen nach außen lesen. +
+
+ +
+ Recall +
    +
  1. Wann nutzt du "", wann ''?
  2. +
  3. Was ist der Unterschied zwischen Heredoc und Nowdoc?
  4. +
  5. Wie prüfst du, ob ein String einen anderen enthält?
  6. +
+
+
+ + +
+
+
04
+
+

Arrays: Liste und Map

+
Indiziert, assoziativ, gemischt
+
+
+ +
+ Frage zum Einstieg + In den meisten Sprachen sind Listen und Dictionaries zwei verschiedene Datenstrukturen. In PHP ist beides ein array – das macht vieles flexibel, aber auch leicht verwirrend. Wie funktioniert das? +
+ +

Indizierte Arrays (Listen)

+

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
+ +

Assoziative Arrays (Maps)

+

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.

+ +

Über Arrays iterieren

+

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";
+}
+ +

Array-Funktionen

+ + + + + + + + + +
FunktionWofü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_valuesKeys oder Werte extrahieren
in_array($x, $arr)Enthält Wert?
sort($arr)Sortiert in-place
+ +
+ Recall +
    +
  1. Was ist der Unterschied zwischen indizierten und assoziativen Arrays?
  2. +
  3. Was macht ?? in PHP?
  4. +
  5. Wie iterierst du über ein assoziatives Array mit Keys und Werten?
  6. +
+
+
+ + +
+
+
05
+
+

Control Flow

+
if, match, foreach, while
+
+
+ +
+ Frage zum Einstieg + Wie sagst du PHP "tu das nur, wenn X" oder "wiederhole das, bis Y"? Die klassischen Konstrukte kennst du aus jeder Sprache – aber PHP 8 hat mit match ein modernes Werkzeug ergänzt, das vieles eleganter macht. +
+ +

if, elseif, else

+ +
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';
+ +

match: das moderne switch

+

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.

+ +

Schleifen

+ +
// 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 statt switch + In neuem Code: nimm match. Strikter Vergleich (===), kein fallthrough-Fehler, Ergebnis als Wert. switch nur noch in Legacy-Code oder wenn du komplexe Blöcke pro Fall brauchst. +
+
+ +
+ Recall +
    +
  1. Wann nutzt du foreach, wann for?
  2. +
  3. Was sind die wichtigsten Vorteile von match gegenüber switch?
  4. +
  5. Was ist der Unterschied zwischen break und continue?
  6. +
+
+
+ + +
+
+
06
+
+

Funktionen mit Type-Hints

+
Parameter, Rückgabewerte, Named Arguments
+
+
+ +
+ Frage zum Einstieg + Eine Funktion ohne Type-Hints ist wie ein Brief ohne Adresse – sie kommt vielleicht an, aber niemand weiß, was sie erwartet. Modernes PHP-Code hat Types überall. Wie schreibst du sie, und was kannst du damit? +
+ +

Funktion deklarieren

+ +
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.

+ +

Named Arguments

+

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,
+);
+ +

Nullable und Union Types

+

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);
+}
+ +
+
!
+
+ Closures vs. globale Variablen + Funktionen sehen keine Variablen aus dem umgebenden Code. Du musst sie explizit übergeben. Closures (anonyme Funktionen) können mit use ($var) Variablen importieren – aber Globals mit global $x sind tabu. +
+
+ +
+ Recall +
    +
  1. Wie deklarierst du eine Funktion, die string zurückgibt?
  2. +
  3. Was ist der Vorteil von Named Arguments?
  4. +
  5. Was bedeutet ?string als Typ?
  6. +
+
+
+ + +
+
+
07
+
+

Klassen und Objekte

+
Promoted Constructor, readonly, Methoden
+
+
+ +
+ Frage zum Einstieg + In PHP 5 brauchten Klassen viel Boilerplate: Properties, Konstruktor, Getter, Setter – seitenweise Code für simple Daten. PHP 8 hat das drastisch reduziert. Was sieht eine moderne Klasse heute aus? +
+ +

Klasse mit Promoted Constructor

+

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.

+ +

Vererbung und Interfaces

+ +
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";
+  }
+}
+ +

Static Methods und Konstanten

+ +
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
+ +
+
i
+
+ Konstruktor-Promotion sparsam einsetzen + Bei mehr als 5-6 Parametern wird die Konstruktor-Liste unübersichtlich. Dann lieber DTOs oder Builder-Pattern. Aber für die meisten Cases mit 2-4 Properties ist Promotion ein riesiger Gewinn an Lesbarkeit. +
+
+ +
+ Recall +
    +
  1. Was generiert public readonly string $name im Konstruktor?
  2. +
  3. Was ist der Unterschied zwischen extends und implements?
  4. +
  5. Wie greifst du auf eine static-Konstante zu?
  6. +
+
+
+ + +
+
+
08
+
+

Namespaces und Autoloading

+
PSR-4, use-Statements, Datei-Organisation
+
+
+ +
+ Frage zum Einstieg + Bei zwei Klassen mit dem Namen 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. +
+ +

Namespace deklarieren

+

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.

+ +

Klassen importieren mit use

+ +
<?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: Datei = Namespace

+

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.

+ +
+
+
+ Eine Klasse, eine Datei + Konvention: pro Datei genau eine Klasse, Datei-Name gleich Klassen-Name (case-sensitive!). User.php enthält class User. Die meisten Frameworks und Tools verlassen sich darauf. +
+
+ +
+ Recall +
    +
  1. Wozu sind Namespaces da?
  2. +
  3. Was bedeutet PSR-4 in einem Satz?
  4. +
  5. Wie importierst du eine Klasse aus einem anderen Namespace?
  6. +
+
+
+ + +
+
+
09
+
+

Fehlerbehandlung

+
Exceptions, try/catch, eigene Fehler
+
+
+ +
+ Frage zum Einstieg + Was passiert, wenn eine Datei nicht existiert, eine Datenbankverbindung scheitert oder eine API einen unerwarteten Wert liefert? In PHP gibt es zwei Welten: alte false-Rückgaben und moderne Exceptions. Wie navigierst du beide? +
+ +

Exceptions verstehen

+

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.

+ +

Mehrere Exception-Typen

+

PHP kennt eine ganze Hierarchie von Exception-Typen. Du kannst gezielt darauf reagieren:

+ + + + + + + + + +
ExceptionWann
InvalidArgumentExceptionFalsche Funktionsargumente
RuntimeExceptionFehler zur Laufzeit
TypeErrorType-Hint verletzt
ValueErrorWert außerhalb erlaubtem Bereich
JsonExceptionJSON-Parsing fehlgeschlagen
PDOExceptionDatenbankfehler
+ +

Eigene Exceptions

+

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;
+}
+ +
+
!
+
+ Niemals @-Suppression + Mit @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. +
+
+ +
+ Recall +
    +
  1. Was passiert, wenn eine Exception nicht gefangen wird?
  2. +
  3. Wann läuft der finally-Block?
  4. +
  5. Warum solltest du eigene Exception-Klassen schreiben?
  6. +
+
+
+ + +
+
+
10
+
+

Composer und erste Anwendung

+
Pakete installieren, kleines CLI-Tool bauen
+
+
+ +
+ Frage zum Einstieg + Du hast PHP gelernt – aber wie kommst du jetzt zu einem echten Projekt? Composer ist die Brücke: er installiert Pakete aus dem riesigen PHP-Ökosystem, lädt Klassen automatisch und gibt deinem Projekt Struktur. Lass uns ein kleines Tool bauen. +
+ +

Projekt aufsetzen

+

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.

+ +

composer.json mit Autoload

+ +
{
+  "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.

+ +

Kleines CLI-Tool bauen

+

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.

+ +
+
+
+ Erst Tools, dann Framework + Mit Composer und einzelnen Symfony-Komponenten kommst du bei kleinen Projekten weit. Erst wenn du Routing, Templates, ORM, Auth zusammen brauchst, lohnt sich der Sprung zu einem vollen Framework wie Laravel oder Symfony. +
+
+ +
+ Recall +
    +
  1. Welche Datei steuert Composers Abhängigkeiten und Autoload?
  2. +
  3. Wozu ist composer dump-autoload?
  4. +
  5. Welche Library nimmst du für CLI-Tools in PHP?
  6. +
+
+
+ + +
+

Wie es weitergeht

+ +

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.

+ +

Spaced-Repetition-Plan

+
+
+ Heute +

Guide gelesen, Recall-Fragen aus jedem Kapitel beantwortet.

+
+
+ +1 Tag +

OnePager überfliegen, alle Recall-Fragen aus dem Kopf beantworten.

+
+
+ +7 Tage +

Mini-Projekt: ein eigenes CLI-Tool mit Composer und Symfony Console.

+
+
+ +30 Tage +

Cheatsheet als Referenz – ein neues Paket aus Packagist einbinden.

+
+
+ +

Was als nächstes lernen

+

Mit diesen Grundlagen kannst du in jede Spezialisierung einsteigen. Empfehlungen je nach Interesse:

+ +
    +
  • Web-Frameworks – Laravel für schnellen Einstieg, Symfony für modulare Apps
  • +
  • Datenbanken – PDO für SQL direkt, Doctrine als ORM für komplexe Domänen
  • +
  • Testing – PHPUnit für Unit-Tests, Mockery für Mocks
  • +
  • Static Analysis – PHPStan und Psalm finden Bugs ohne Ausführung
  • +
  • E-Commerce – Shopware oder Magento für Online-Shops
  • +
  • CMS – WordPress, Drupal oder TYPO3 für Content-Sites
  • +
+ +

Begleitmaterial

+

Dieser Guide ist Teil eines Sets:

+
    +
  • PHP OnePager – die visuelle Übersicht für den Schreibtisch
  • +
  • PHP Cheatsheet – die dichte Referenz beim Programmieren
  • +
  • PHP Mini-Guide – der 15-Min-Schnelleinstieg
  • +
  • PHP Anfänger-Guide (dieses Dokument) – die Grundlagen tief
  • +
+
+ + + +``` \ No newline at end of file diff --git a/templates/Referenz/Cheatsheet.md b/templates/Referenz/Cheatsheet.md new file mode 100644 index 0000000..d976242 --- /dev/null +++ b/templates/Referenz/Cheatsheet.md @@ -0,0 +1,537 @@ +``` + + + + +PHP Cheatsheet + + + +
+ + +
+ +
+

PHP Cheatsheet

+
Syntax, OOP, Type-System, Standard-Funktionen & Ökosystem auf einen Blick
+
+
+ 8.4 + Stand 2026 +
+
+ + +
+ + +
+ +
+
+ + + + Variablen & Typen +
+
+ + + + + + + + + + + +
$x = 42int
$x = 3.14float
$x = "Hi"string
$x = truebool
$x = nullnull
$x = [1,2,3]array
$x = ['k'=>1]assoc array
gettype($x)Typ-Name
is_int($x)Typ-Check
(int)$xCast
+
+
+ +
+
+ + + + String-Funktionen +
+
+ + + + + + + + + + + +
strlen($s)Länge
strtolowerklein
strtouppergroß
trim($s)Whitespace weg
explode(',', $s)→ array
implode(',', $a)→ string
str_replacea → b
str_containsenthält?
sprintf("%d", 1)format
"$name"Interpolation
+
+
+ +
+ + +
+ +
+
+ + + + Moderne Klasse (PHP 8) +
+
+
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; + } +} + +$u = new User(name: 'Marek', age: 34);
+
+
+ +
+
+ + + + Array-Funktionen +
+
+ + + + + + + + + + + +
count($a)Anzahl
array_map(fn, $a)transform
array_filterfiltern
array_reduceaggregieren
array_keys→ Keys
array_values→ Values
in_array($x, $a)enthält?
array_mergea + b
sort / usortsortieren
array_uniquededupe
+
+
+ +
+ + +
+ +
+
+ + + + Control Flow +
+
+
if ($x > 0) { ... } +elseif ($x < 0) { ... } +else { ... } + +match($status) { + 'ok', 'good' => 'positiv', + 'err' => 'negativ', + default => '?', +}; + +foreach ($items as $k => $v) { ... } +for ($i=0; $i<10; $i++) { ... } +while ($cond) { ... }
+
+
+ +
+
+ + + + Enums & Match +
+
+
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', + }; + } +}
+
+
+ +
+
+ + + + Fehler & Try/Catch +
+
+
try { + $result = risky(); +} catch (\ValueError $e) { + log($e->getMessage()); +} catch (\Exception $e) { + throw new \AppError(...); +} finally { + cleanup(); +}
+
+
+ +
+ + +
+ +
+
+ + + + Ökosystem +
+
+
+
+
+
ComposerPaket-Manager
+
+
+
+
LaravelFull-Stack
+
+
+
+
SymfonyEnterprise
+
+
+
+
PHPUnitTests
+
+
+
+
PHPStanStatic-Analyse
+
+
+
+
ShopwareE-Commerce
+
+
+
+
WordPressCMS
+
+
+
+
DoctrineORM & DBAL
+
+
+
+
+ +
+
+ + + + Idiome vs. Anti-Patterns +
+
+
+
+

Idiomatisch

+
    +
  • strict_types=1
  • +
  • Type-Hints überall
  • +
  • readonly Properties
  • +
  • Composer Autoload
  • +
  • PSR-12 Coding-Style
  • +
  • match statt switch
  • +
+
+
+

Vermeiden

+
    +
  • mysql_* Funktionen
  • +
  • extract() von User
  • +
  • eval() jeglicher Art
  • +
  • Globals ($GLOBALS)
  • +
  • @-Suppression
  • +
  • include statt require_once
  • +
+
+
+
+
+ +
+
+ + +
+
+ Projekt starten + composer init +
+
+ Server starten + php -S localhost:8000 +
+
+ Tests + vendor/bin/phpunit +
+
+ Static Analysis + vendor/bin/phpstan analyse +
+
php.net
+
+ +
+ + +``` \ No newline at end of file diff --git a/templates/Referenz/ExtendedGuide.md b/templates/Referenz/ExtendedGuide.md new file mode 100644 index 0000000..d121029 --- /dev/null +++ b/templates/Referenz/ExtendedGuide.md @@ -0,0 +1,2542 @@ +``` + + + + +PHP Extended-Guide + + + + + +
+
+ +
Extended-Guide · 4h · Stand 2026
+
+ +
+

PHP
an der Grenze.

+

Internals, Fibers, FFI, DDD & Event Sourcing – PHP über das Übliche hinaus.

+
+ +
+
+ Was du danach kannst + Reflection und Stream-Wrapper produktiv einsetzen · mit Fibers asynchron programmieren · eigenen DI-Container schreiben · Performance gezielt profilen und optimieren · komplexe Domänen mit DDD, Hexagonal Architecture und Event Sourcing strukturieren. +
+
PHP 8.4
+
+
+ + +
+

Inhalt

+ +
Teil 1 · Sprach-Internals
+ +
+
1
+
+

Reflection-API tief

+

Klassen, Methoden, Attribute zur Laufzeit erkunden

+
+
15 Min
+
+ +
+
2
+
+

SPL Datenstrukturen

+

SplStack, SplQueue, SplPriorityQueue, SplObjectStorage

+
+
15 Min
+
+ +
+
3
+
+

Stream-Wrapper und Filter

+

Eigene Protokolle wie file:// schreiben

+
+
15 Min
+
+ +
+
4
+
+

Garbage Collection

+

Zirkuläre Referenzen, Memory-Profiling

+
+
15 Min
+
+ +
+
5
+
+

FFI - C-Code direkt aufrufen

+

Foreign Function Interface für Native-Libraries

+
+
15 Min
+
+ +
Teil 2 · Performance & Async
+ +
+
6
+
+

OpCache und Preloading

+

Code-Compilation einmal, nicht pro Request

+
+
15 Min
+
+ +
+
7
+
+

Fibers - Cooperative Concurrency

+

Async ohne Threading (PHP 8.1+)

+
+
15 Min
+
+ +
+
8
+
+

ReactPHP & Event-Loops

+

Long-Running Server, non-blocking I/O

+
+
15 Min
+
+ +
+
9
+
+

Profiling mit Blackfire

+

CPU- und Memory-Hotspots finden

+
+
15 Min
+
+ +
+
10
+
+

Caching-Strategien

+

APCu, Redis, HTTP-Cache, ESI

+
+
15 Min
+
+ +
Teil 3 · Architektur & Patterns
+ +
+
11
+
+

Dependency Injection Container

+

Eigener Container in 100 Zeilen

+
+
15 Min
+
+ +
+
12
+
+

Event-Dispatcher Pattern

+

Lose Kopplung zwischen Bounded Contexts

+
+
15 Min
+
+ +
+
13
+
+

CQRS und Command Bus

+

Lese- und Schreibmodelle trennen

+
+
15 Min
+
+ +
+
14
+
+

Domain-Driven Design in PHP

+

Value Objects, Entities, Aggregates

+
+
15 Min
+
+ +
+
15
+
+

Hexagonal Architecture

+

Ports und Adapter, Domain im Zentrum

+
+
15 Min
+
+ +
+
16
+
+

Event Sourcing

+

State als Sequenz von Events

+
+
15 Min
+
+
+ + +
+
+
01
+
+

Reflection-API tief

+
Klassen zur Laufzeit erkunden
+
+
+ +
+ Frage zum Einstieg + Wie weiß Symfony zur Laufzeit, welche Routen in deinem Controller stecken? Wie generiert Doctrine SQL-Schemas aus deinen Entity-Klassen? Beide nutzen Reflection – PHPs eingebauter Mechanismus, um Code-Struktur zu introspecten. +
+ +

ReflectionClass: alles über eine Klasse

+

Mit ReflectionClass bekommst du alle Metadaten einer Klasse: Properties, Methoden, Attribute, Parent, Interfaces. Das ist die Basis fast aller Framework-Magie:

+ +
$reflection = new \ReflectionClass(User::class);
+
+$reflection->getName();                // 'App\User'
+$reflection->getShortName();           // 'User'
+$reflection->getMethods();              // ReflectionMethod[]
+$reflection->getProperties();           // ReflectionProperty[]
+$reflection->getParentClass();          // ReflectionClass|false
+$reflection->getInterfaceNames();       // string[]
+$reflection->getAttributes();           // ReflectionAttribute[]
+ +

Instanzen ohne Konstruktor

+

Manchmal willst du eine Klasse instanziieren, ohne den Konstruktor aufzurufen – z.B. zum Deserialisieren von Datenbank-Rows oder JSON. Reflection erlaubt das:

+ +
$reflection = new \ReflectionClass(User::class);
+$user = $reflection->newInstanceWithoutConstructor();
+
+// Jetzt Properties direkt setzen
+$nameProperty = $reflection->getProperty('name');
+$nameProperty->setValue($user, 'Marek');
+ +

Doctrine ORM tut genau das beim Laden: Entity ohne Konstruktor instanziieren, Properties aus DB-Row setzen. Sonst müsste jede Entity einen "leeren" Konstruktor haben.

+ +

Private Properties und Methoden

+

Reflection ignoriert Sichtbarkeitsregeln – essentiell für Testing-Frameworks und Serializer:

+ +
class Order {
+  private string $internalRef;
+
+  private function calculateTax(): float { /* ... */ }
+}
+
+$order = new Order();
+$reflection = new \ReflectionClass($order);
+
+// Private property setzen (für Tests)
+$prop = $reflection->getProperty('internalRef');
+$prop->setValue($order, 'TEST-123');
+
+// Private Methode aufrufen
+$method = $reflection->getMethod('calculateTax');
+$tax = $method->invoke($order);
+ +

Performance-Implikationen

+

Reflection ist nicht gratis. Pro ReflectionClass-Instanz parst PHP intern viele Metadaten. Frameworks wie Symfony cachen Reflection-Ergebnisse aggressiv. Faustregel:

+ + + + + + + +
Use CaseOK
Boot-Zeit, einmal pro Requestja
Dependency-Injection-Container Buildja, aber cachen
In jedem Method-Callnein, das ist langsam
Hot-Path-Loopsnein, niemals
+ +
+
i
+
+ Attribute via Reflection + Wie im Fortgeschritten-Guide gezeigt: $reflection->getAttributes() liest PHP 8 Attribute aus. Das ist die Brücke zwischen deinem deklarativen Markup (#[Route('/users')]) und dem Framework-Code, der es interpretiert. +
+
+ +
+ Recall +
    +
  1. Welche zwei großen Frameworks nutzen Reflection als Kern-Mechanismus?
  2. +
  3. Wie umgehst du den Konstruktor beim Instanziieren?
  4. +
  5. Warum ist Reflection in Hot-Paths problematisch?
  6. +
+
+
+ + +
+
+
02
+
+

SPL Datenstrukturen

+
Stack, Queue, Heap, ObjectStorage
+
+
+ +
+ Frage zum Einstieg + PHP-Arrays sind extrem flexibel – aber für spezielle Use Cases nicht optimal. Eine Priority Queue mit Array nachbauen kostet O(n log n) pro Insert. Die Standard PHP Library (SPL) liefert spezialisierte Datenstrukturen. Wann lohnt sich der Wechsel? +
+ +

SplStack und SplQueue

+

Stack (LIFO) und Queue (FIFO) als typisierte Strukturen. Bei großen Mengen schneller als Array-Operationen mit array_push/array_shift:

+ +
$stack = new \SplStack();
+$stack->push('a');
+$stack->push('b');
+$stack->push('c');
+
+$stack->pop();                       // 'c'
+$stack->top();                       // 'b' (peek ohne entfernen)
+
+$queue = new \SplQueue();
+$queue->enqueue('first');
+$queue->enqueue('second');
+$queue->dequeue();                   // 'first'
+ +

Wichtiger Vorteil gegenüber Array: O(1) für alle Operationen, auch dequeue. array_shift ist O(n) wegen Re-Indexing.

+ +

SplPriorityQueue

+

Bei Aufgaben mit Prioritäten – Job-Scheduler, A*-Pathfinding, Event-Loops – ist eine Priority Queue Standard:

+ +
$pq = new \SplPriorityQueue();
+$pq->insert('low-priority-task', 1);
+$pq->insert('urgent-task', 10);
+$pq->insert('normal-task', 5);
+
+while (!$pq->isEmpty()) {
+  echo $pq->extract() . "\n";
+}
+// urgent-task
+// normal-task
+// low-priority-task
+ +

SplObjectStorage: Map mit Objekten als Keys

+

Reguläre PHP-Arrays erlauben nur Strings/Ints als Keys. Mit SplObjectStorage kannst du Objekte selbst als Keys nutzen:

+ +
$permissions = new \SplObjectStorage();
+
+$alice = new User('Alice');
+$bob = new User('Bob');
+
+$permissions[$alice] = ['read', 'write'];
+$permissions[$bob] = ['read'];
+
+$permissions[$alice];               // ['read', 'write']
+$permissions->contains($bob);          // true
+
+// Iteration
+foreach ($permissions as $user) {
+  $perms = $permissions[$user];
+  echo $user->name . ': ' . implode(',', $perms) . "\n";
+}
+ +

Praktisch für Identity-Maps in ORMs, Permission-Systems, Graphen mit Objekt-Knoten.

+ +

SplFixedArray für Memory-Optimierung

+

Bei sehr großen, indizierten Arrays mit fester Größe ist SplFixedArray deutlich speichereffizienter:

+ +
// Reguläres Array: ~100MB für 1M Elemente
+$arr = [];
+for ($i = 0; $i < 1_000_000; $i++) {
+  $arr[$i] = $i * 2;
+}
+
+// SplFixedArray: ~40MB für dasselbe
+$arr = new \SplFixedArray(1_000_000);
+for ($i = 0; $i < 1_000_000; $i++) {
+  $arr[$i] = $i * 2;
+}
+ +
+
!
+
+ SPL nur wenn nötig + Für die meisten Web-Apps reichen normale Arrays völlig. SPL lohnt sich, wenn du Performance-kritische Pfade mit großen Mengen hast oder spezielle Semantik brauchst (Priority, Object-Keys). Sonst macht es Code unnötig komplex. +
+
+ +
+ Recall +
    +
  1. Was ist der Performance-Vorteil von SplQueue gegenüber array_shift?
  2. +
  3. Wofür nutzt du SplObjectStorage?
  4. +
  5. Wann lohnt sich SplFixedArray?
  6. +
+
+
+ + +
+
+
03
+
+

Stream-Wrapper und Filter

+
Eigene Protokolle wie file:// schreiben
+
+
+ +
+ Frage zum Einstieg + Du kennst fopen('file:///pfad'), file_get_contents('http://...'), vielleicht fopen('php://memory'). All das funktioniert über Stream-Wrapper. Du kannst eigene schreiben – z.B. s3://bucket/key oder db://users/42. Wie geht das? +
+ +

Was sind Stream-Wrapper?

+

Ein Stream-Wrapper definiert ein Pseudo-Protokoll, das wie eine Datei aussieht. Alle PHP-Funktionen, die mit Streams arbeiten (fopen, fread, file_get_contents, file_put_contents), funktionieren transparent damit.

+ +

PHP bringt mehrere eingebaute Wrapper mit:

+ + + + + + + + + + +
WrapperWofür
file://Lokales Dateisystem (Default)
http:// / https://HTTP-Requests
php://memoryIn-Memory Stream
php://stdin / php://stdoutStandard-IO
php://inputRaw POST-Body
data://Base64 oder URL-encoded Daten
compress.zlib://gzip-Streams
+ +

Eigenen Wrapper schreiben

+

Ein Stream-Wrapper ist eine Klasse mit bestimmten Methoden. Minimal-Beispiel: ein Wrapper, der Strings aus einem statischen Array liefert:

+ +
class DictionaryStreamWrapper {
+  private static array $dict = [
+    'hello' => 'Hallo, Welt!',
+    'goodbye' => 'Auf Wiedersehen.',
+  ];
+
+  private int $position = 0;
+  private string $data = '';
+
+  public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool {
+    $key = parse_url($path, PHP_URL_HOST);
+    if (!isset(self::$dict[$key])) return false;
+    $this->data = self::$dict[$key];
+    return true;
+  }
+
+  public function stream_read(int $count): string {
+    $chunk = substr($this->data, $this->position, $count);
+    $this->position += strlen($chunk);
+    return $chunk;
+  }
+
+  public function stream_eof(): bool {
+    return $this->position >= strlen($this->data);
+  }
+}
+
+// Registrieren und nutzen
+stream_wrapper_register('dict', DictionaryStreamWrapper::class);
+
+echo file_get_contents('dict://hello');    // 'Hallo, Welt!'
+ +

Praktischer Use Case: S3-Wrapper

+

AWS-SDK für PHP registriert einen s3://-Wrapper. Dein Code arbeitet dann mit S3 wie mit lokalen Dateien:

+ +
use Aws\S3\S3Client;
+
+$client = new S3Client([/* config */]);
+$client->registerStreamWrapper();
+
+// Jetzt arbeitet jede File-Funktion mit S3
+$data = file_get_contents('s3://my-bucket/users/42.json');
+file_put_contents('s3://my-bucket/log.txt', 'eintrag');
+
+foreach (scandir('s3://my-bucket/uploads/') as $file) {
+  // ...
+}
+ +

Stream-Filter

+

Filter transformieren Daten während des Streamings. Eingebaute Filter: string.rot13, string.toupper, zlib.deflate:

+ +
$handle = fopen('large.txt', 'r');
+stream_filter_append($handle, 'string.toupper');
+
+while (!feof($handle)) {
+  echo fread($handle, 8192);   // alles UPPERCASE
+}
+ +
+
+
+ Flysystem für Cloud-Storage + Für Production: nutze league/flysystem statt eigene Stream-Wrapper zu schreiben. Es abstrahiert S3, Azure, GCS, FTP, local mit einer einheitlichen API – stabiler und besser getestet als selbst gestrickte Wrapper. +
+
+ +
+ Recall +
    +
  1. Welche Methoden braucht ein Stream-Wrapper mindestens?
  2. +
  3. Wozu sind Stream-Filter da?
  4. +
  5. Welche Library nutzt du stattdessen für Cloud-Storage?
  6. +
+
+
+ + +
+
+
04
+
+

Garbage Collection

+
Zirkuläre Referenzen, Memory-Profiling
+
+
+ +
+ Frage zum Einstieg + PHP räumt Speicher automatisch auf – meistens. Aber in Long-Running-Prozessen (CLI-Tools, Workers, ReactPHP-Server) sammeln sich plötzlich GB an RAM. Was ist los? Die Antwort liegt in PHPs Garbage Collection und wie sie zirkuläre Referenzen behandelt. +
+ +

Reference Counting

+

PHP nutzt primär Reference Counting: jede Variable hat einen Counter, wie oft auf sie verwiesen wird. Fällt der Counter auf 0, wird der Speicher sofort freigegeben:

+ +
$a = 'hallo';       // refcount = 1
+$b = $a;            // refcount = 2
+unset($a);          // refcount = 1
+unset($b);          // refcount = 0 → Speicher frei
+ +

Das ist schnell und deterministisch. Aber es hat ein Problem: zirkuläre Referenzen.

+ +

Das Problem zirkulärer Referenzen

+ +
class Node {
+  public ?Node $next = null;
+}
+
+$a = new Node();
+$b = new Node();
+$a->next = $b;          // $b refcount = 2
+$b->next = $a;          // $a refcount = 2
+
+unset($a);              // $a refcount = 1 (von $b->next)
+unset($b);              // $b refcount = 1 (von $a->next)
+
+// Beide Counter sind 1, aber nichts greift mehr drauf zu
+// → Memory Leak!
+ +

Reference Counting allein erkennt das nicht. PHP hat dafür einen zusätzlichen Cycle Collector, der periodisch durchläuft und solche Zyklen findet.

+ +

Cycle Collector steuern

+

Der Collector läuft automatisch, wenn ein interner Buffer voll ist (Default 10.000 Roots). Du kannst ihn manuell triggern:

+ +
// Aktuellen Status prüfen
+gc_status();        // runs, collected, threshold, roots
+
+// Manuell laufen lassen
+$collected = gc_collect_cycles();   // Anzahl entsorgter Objekte
+
+// Deaktivieren (Performance-kritische Sektion)
+gc_disable();
+// ... Hot-Code ...
+gc_enable();
+ +

Long-Running-Prozesse

+

In CLI-Workern oder ReactPHP-Servern ist Memory-Management kritisch – PHP-Scripts sterben sonst nach Stunden. Pattern:

+ +
while ($message = $queue->consume()) {
+  processMessage($message);
+
+  // Memory-Check
+  if (memory_get_usage() > 100 * 1024 * 1024) {
+    gc_collect_cycles();
+
+    if (memory_get_usage() > 200 * 1024 * 1024) {
+      exit(0);   // Supervisor startet neu
+    }
+  }
+}
+ +

Symfony Messenger und Laravel Horizon nutzen genau dieses Pattern – sie killen Worker nach bestimmter Memory-Schwelle und lassen sie neu starten.

+ +

WeakReference und WeakMap

+

Seit PHP 7.4 (WeakReference) und 8.0 (WeakMap) gibt es einen offiziellen Mechanismus für Referenzen, die nicht in den Refcount zählen:

+ +
$user = new User('Marek');
+$weak = \WeakReference::create($user);
+
+$weak->get();                  // User-Objekt
+
+unset($user);                  // User wird sofort freigegeben
+$weak->get();                  // null
+
+// WeakMap: nützlich für Caches, die nicht "festhalten"
+$cache = new \WeakMap();
+$cache[$user] = 'cached-value';
+// Sobald $user weg ist, ist auch der Cache-Eintrag weg
+ +
+ Recall +
    +
  1. Was ist PHPs primäre GC-Strategie?
  2. +
  3. Warum reicht Reference Counting bei zirkulären Referenzen nicht?
  4. +
  5. Wofür nutzt du WeakReference?
  6. +
+
+
+ + +
+
+
05
+
+

FFI - C-Code direkt aufrufen

+
Foreign Function Interface
+
+
+ +
+ Frage zum Einstieg + Du brauchst eine C-Library – etwa für GPU-Berechnungen, Bildverarbeitung oder spezielle Algorithmen. Klassisch: PHP-Extension in C schreiben (großer Aufwand). Mit FFI seit PHP 7.4 rufst du C-Funktionen direkt aus PHP-Code auf, ohne Kompilieren. +
+ +

FFI aktivieren

+

FFI ist standardmäßig installiert, muss aber in der php.ini aktiviert werden:

+ +
# php.ini
+extension=ffi
+ffi.enable=preload     # sicher (nur für preloaded scripts)
+# oder: ffi.enable=true    # offen, unsicher in Web-Context
+ +

C-Funktion aufrufen

+

FFI braucht zwei Dinge: die Signatur der C-Funktion und die Library-Datei. Klassisches Beispiel: die libc-Funktion getpid:

+ +
$ffi = \FFI::cdef("
+  int getpid(void);
+  unsigned int sleep(unsigned int seconds);
+", "libc.so.6");
+
+echo $ffi->getpid();              // 12345 (Process-ID)
+$ffi->sleep(2);                    // 2 Sekunden schlafen
+ +

Header-Datei laden

+

Für größere Libraries kannst du komplette C-Header parsen lassen. Beispiel: SDL für ein Spiele-Backend, oder libxml für XML-Verarbeitung:

+ +
$ffi = \FFI::load('/usr/include/sdl2/SDL.h');
+
+// Jetzt sind alle SDL-Funktionen verfügbar
+$ffi->SDL_Init(SDL_INIT_VIDEO);
+$window = $ffi->SDL_CreateWindow(...);
+ +

Structs und Pointer

+

C-Strukturen werden zu PHP-Objekten. Pointer und Memory-Allokation funktionieren auch:

+ +
$ffi = \FFI::cdef("
+  typedef struct {
+    int x;
+    int y;
+  } Point;
+");
+
+// Struct allokieren
+$point = $ffi->new('Point');
+$point->x = 42;
+$point->y = 17;
+
+// Array von Structs
+$points = $ffi->new('Point[100]');
+for ($i = 0; $i < 100; $i++) {
+  $points[$i]->x = $i;
+}
+ +

Praxis-Use-Cases

+ + + + + + + + + +
Use CaseBeispiel-Library
Bildverarbeitunglibvips, OpenCV
Kompressionlibzstd, brotli
Cryptolibsodium (auch nativ in PHP)
ML-Inferenzlibtorch, onnxruntime
Grafik/UISDL, GTK
System-Callslibc direkt
+ +

Performance-Trade-Off

+

FFI-Calls sind nicht gratis: jeder Aufruf hat Overhead durch Type-Conversion. Faustregel:

+ +
    +
  • Lohnt sich: wenige Aufrufe mit viel C-Arbeit pro Call (Bildverarbeitung)
  • +
  • Lohnt sich nicht: viele kleine Aufrufe in Loops (klassische PHP-Logik schneller)
  • +
+ +
+
!
+
+ FFI ist nicht für Web-Apps gedacht + Mit ffi.enable=preload nur in CLI-Scripts oder Preload-Files nutzen. In Web-Context (Apache/php-fpm) bedeutet falsche FFI-Nutzung Memory-Corruption oder Crashes. Halte FFI auf CLI-Tools oder Daemon-Code beschränkt. +
+
+ +
+ Recall +
    +
  1. Welche zwei Dinge braucht FFI::cdef?
  2. +
  3. Wann lohnt sich FFI, wann nicht?
  4. +
  5. Warum solltest du FFI nicht in Web-Requests nutzen?
  6. +
+
+
+ + +
+
+
06
+
+

OpCache und Preloading

+
Code-Compilation einmal, nicht pro Request
+
+
+ +
+ Frage zum Einstieg + PHP parst und kompiliert standardmäßig deinen Code bei jedem Request neu. Bei einer Laravel-App mit hunderten Dateien sind das viele Millisekunden Overhead pro Request. OpCache und Preloading eliminieren das. +
+ +

OpCache aktivieren

+

OpCache ist ab PHP 5.5 dabei, muss aber in php.ini aktiviert werden:

+ +
# php.ini
+opcache.enable=1
+opcache.memory_consumption=256
+opcache.max_accelerated_files=20000
+opcache.validate_timestamps=1     # Dev: 1 (prüft Änderungen)
+opcache.revalidate_freq=0         # Prod: 0 (kein Re-Check)
+opcache.jit_buffer_size=128M      # PHP 8 JIT
+opcache.jit=tracing
+ +

OpCache cached die kompilierten OpCodes im Shared Memory. Beim nächsten Request wird der gecachte OpCode direkt ausgeführt – ohne Parse und Compile.

+ +

Validate Timestamps in Production

+

In Development willst du Code-Änderungen sofort sehen (validate_timestamps=1). In Production sind File-Stat-Calls bei jedem Request unnötiger Overhead – deshalb validate_timestamps=0:

+ +
# Bei Deployment manuell reload
+service php-fpm reload
+# oder
+opcache_reset()       # im PHP-Code
+# oder cache file/api
+curl http://localhost/opcache-reset.php
+ +

OpCache-Status zur Laufzeit

+ +
$status = opcache_get_status();
+
+$status['opcache_statistics']['hits'];        // Cache Hits
+$status['opcache_statistics']['misses'];      // Misses
+$status['memory_usage']['used_memory'];        // Speicher
+$status['memory_usage']['free_memory'];        // frei
+
+// Bei voller Speicher-Auslastung: opcache.memory_consumption erhöhen
+ +

Preloading: noch eine Stufe weiter

+

Seit PHP 7.4 gibt es Preloading: beim Start des PHP-Prozesses werden Klassen einmal komplett geladen und stehen dann in jedem Request sofort zur Verfügung – ohne OpCache-Lookup:

+ +
# php.ini
+opcache.preload=/var/www/preload.php
+opcache.preload_user=www-data
+ +
// preload.php
+<?php
+require '/var/www/vendor/autoload.php';
+
+// Lade alle Klassen aus src/
+foreach (new \RecursiveIteratorIterator(
+    new \RecursiveDirectoryIterator('/var/www/src')
+) as $file) {
+  if ($file->getExtension() === 'php') {
+    opcache_compile_file($file->getRealPath());
+  }
+}
+ +

Effekt: in Laravel- und Symfony-Apps oft 20-30% schnellere Requests, vor allem bei kleinen Aktionen.

+ +

JIT (Just-In-Time)

+

Mit PHP 8 kam der JIT. Er kompiliert OpCodes weiter zu nativem Maschinencode. In typischen Web-Apps bringt JIT wenig (I/O-bound), bei rechenintensiven Loops und CLI-Tools deutlich:

+ + + + + + +
Use CaseSpeedup mit JIT
Web (Database/Network-bound)~5-10%
CLI-Tools, Compute-bound~30-50%
Mandelbrot/Mathematik~2-3x
+ +
+
i
+
+ Preloading-Limitationen + Geladene Klassen können zur Laufzeit nicht mehr geändert werden. Wenn dein Framework Klassen dynamisch erweitert (z.B. Symfony Cache-Container), führt das zu Konflikten. Reload bei Deployment ist Pflicht. +
+
+ +
+ Recall +
    +
  1. Was cached OpCache genau?
  2. +
  3. Was ist der Unterschied zwischen validate_timestamps=1 und 0?
  4. +
  5. Wann lohnt sich JIT besonders?
  6. +
+
+
+ + +
+
+
07
+
+

Fibers - Cooperative Concurrency

+
Async ohne Threading (PHP 8.1+)
+
+
+ +
+ Frage zum Einstieg + PHP ist klassisch single-threaded und blockierend. Aber moderne Apps brauchen oft parallel laufende HTTP-Calls, gleichzeitige DB-Queries, Streaming. Fibers in PHP 8.1 bringen kooperative Concurrency – die Basis für moderne async Libraries. +
+ +

Was ist eine Fiber?

+

Eine Fiber ist eine "leichtgewichtige" Coroutine: sie hat ihren eigenen Stack, kann pausieren und später fortgesetzt werden. Anders als Threads laufen sie nicht parallel, sondern kooperativ – sie geben Kontrolle freiwillig ab:

+ +
$fiber = new \Fiber(function(): void {
+  echo "Fiber gestartet\n";
+  \Fiber::suspend('pause-wert');   // pausieren
+  echo "Fiber fortgesetzt\n";
+});
+
+$value = $fiber->start();          // 'pause-wert'
+echo "In Main: $value\n";
+
+$fiber->resume('antwort');          // Fiber läuft weiter
+ +

Ausgabe:

+ +
Fiber gestartet
+In Main: pause-wert
+Fiber fortgesetzt
+ +

Async-I/O mit Fibers

+

Klassischer Use Case: HTTP-Requests parallel. Statt zu blockieren, suspendiert die Fiber während sie wartet:

+ +
function fetchUrl(string $url): string {
+  return new \Fiber(function() use ($url) {
+    $ch = curl_init($url);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+
+    // Während wir warten: andere Fibers können laufen
+    \Fiber::suspend();
+
+    return curl_exec($ch);
+  });
+}
+ +

Das einfache Beispiel hier zeigt nur das Konzept – in der Praxis brauchst du eine Scheduler/Event-Loop, die mehrere Fibers verwaltet. Frameworks wie ReactPHP und Amp bieten das.

+ +

Amp 3.0 mit Fibers

+

Amp (revierschiff in PHP-Async-Welt) nutzt Fibers in Version 3.0 für eine viel saubere API als die alten Promise-basierten Patterns:

+ +
use function Amp\async;
+use function Amp\Future\await;
+
+// Drei parallele HTTP-Requests
+$results = await([
+  async(fetchUrl(...), 'https://api.a.com'),
+  async(fetchUrl(...), 'https://api.b.com'),
+  async(fetchUrl(...), 'https://api.c.com'),
+]);
+
+// Hier sind alle drei Responses verfügbar
+foreach ($results as $response) {
+  // ...
+}
+ +

Im Hintergrund läuft eine Event-Loop, die alle Fibers verwaltet und sie aufweckt, wenn ihre I/O fertig ist.

+ +

Fibers vs. echte Threads

+ + + + + + + + +
AspektFibersThreads (parallel)
ParallelitätNein, kooperativJa, true parallel
Memory pro Unit~8 KB~MB
Context-SwitchSehr schnellOS-Overhead
Shared StateDirekt zugreifbarLocking nötig
Use CaseI/O-boundCPU-bound
+ +

Für klassische Web-Apps (viele I/O-Calls, wenig CPU) sind Fibers fast immer die richtige Wahl. Wenn du echtes Parallel-Processing brauchst (Bildverarbeitung, Berechnungen), nutzt du PHP parallel-Extension oder lagerst es in separate Prozesse aus.

+ +
+
+
+ Symfony 7 mit Fibers + Symfony 7+ nutzt intern Fibers für HttpClient (Multi-Request) und Messenger (parallele Message-Verarbeitung). Auch ohne explizit Fibers zu schreiben profitierst du dadurch. +
+
+ +
+ Recall +
    +
  1. Was ist der Hauptunterschied zwischen Fiber und Thread?
  2. +
  3. Wann gibt eine Fiber Kontrolle ab?
  4. +
  5. Wofür braucht es zusätzlich eine Event-Loop wie in Amp?
  6. +
+
+
+ + +
+
+
08
+
+

ReactPHP & Event-Loops

+
Long-Running Server, non-blocking I/O
+
+
+ +
+ Frage zum Einstieg + PHP klassisch: ein Request, ein Prozess, Sterben am Ende. Aber WebSocket-Server, Pub/Sub-Workers oder Streaming-APIs brauchen Long-Running-Prozesse. ReactPHP bringt das Event-Loop-Modell (wie Node.js) in die PHP-Welt. +
+ +

Event-Loop-Grundprinzip

+

Eine Event-Loop ist eine Endlos-Schleife, die Events aus verschiedenen Quellen (Sockets, Timer, Streams) liest und Handler aufruft. I/O-Operationen blockieren nicht – sie geben Promises/Callbacks zurück.

+ +
use React\EventLoop\Loop;
+
+// Timer: nach 2 Sekunden, einmal
+Loop::addTimer(2.0, function() {
+  echo "Nach 2 Sekunden\n";
+});
+
+// Periodisch alle 5 Sekunden
+Loop::addPeriodicTimer(5.0, function() {
+  echo "Alle 5 Sekunden\n";
+});
+
+// Event-Loop starten (blockierend bis stop)
+Loop::run();
+ +

HTTP-Server mit ReactPHP

+

Ein vollständiger HTTP-Server in wenigen Zeilen – ohne Apache/nginx davor:

+ +
use React\Http\HttpServer;
+use React\Http\Message\Response;
+use React\Socket\SocketServer;
+use Psr\Http\Message\ServerRequestInterface;
+
+$http = new HttpServer(function(ServerRequestInterface $request) {
+  return Response::plaintext("Hello, " . $request->getUri()->getPath());
+});
+
+$socket = new SocketServer('0.0.0.0:8080');
+$http->listen($socket);
+
+echo "Server läuft auf http://localhost:8080\n";
+// Event-Loop läuft implizit weiter
+ +

Vorteil gegenüber php-fpm: State bleibt zwischen Requests erhalten. Datenbankverbindungen, Caches, geladene Klassen werden einmal aufgebaut. Bei vielen kleinen Requests ist das deutlich schneller.

+ +

Streams und Pipes

+ +
use React\Stream\ReadableResourceStream;
+use React\Stream\WritableResourceStream;
+
+$stdin = new ReadableResourceStream(STDIN);
+$stdout = new WritableResourceStream(STDOUT);
+
+$stdin->on('data', function($chunk) use ($stdout) {
+  $stdout->write(strtoupper($chunk));
+});
+
+$stdin->on('end', function() {
+  echo "\nInput beendet\n";
+});
+ +

WebSocket-Server mit Ratchet

+

Ratchet baut auf ReactPHP auf und bringt WebSocket-Support:

+ +
use Ratchet\Server\IoServer;
+use Ratchet\WebSocket\WsServer;
+use Ratchet\MessageComponentInterface;
+use Ratchet\ConnectionInterface;
+
+class ChatServer implements MessageComponentInterface {
+  protected \SplObjectStorage $clients;
+
+  public function __construct() {
+    $this->clients = new \SplObjectStorage();
+  }
+
+  public function onOpen(ConnectionInterface $conn) {
+    $this->clients->attach($conn);
+  }
+
+  public function onMessage(ConnectionInterface $from, $msg) {
+    foreach ($this->clients as $client) {
+      if ($client !== $from) $client->send($msg);
+    }
+  }
+
+  // onClose, onError ...
+}
+
+$server = IoServer::factory(new WsServer(new ChatServer()), 8081);
+$server->run();
+ +

Wann ReactPHP, wann klassisch?

+ + + + + + + + +
Use CaseWähle
Klassische CRUD-Appphp-fpm + Symfony/Laravel
WebSocket-ServerReactPHP + Ratchet
Pub/Sub-WorkerReactPHP oder Symfony Messenger
Streaming-APIReactPHP
Tausende kleine Microservice-CallsReactPHP (Connection-Reuse)
+ +
+
!
+
+ Blockierender Code killt die Event-Loop + Ein einziger sleep(10), file_get_contents() auf eine langsame URL, oder ein synchrones PDO::query() blockiert die gesamte Event-Loop. Alle Connections frieren ein. In ReactPHP musst du konsequent non-blocking I/O nutzen. +
+
+ +
+ Recall +
    +
  1. Was ist der Hauptunterschied zwischen php-fpm und ReactPHP?
  2. +
  3. Welches Pattern wäre für einen WebSocket-Chat geeignet?
  4. +
  5. Warum sind blockierende Calls in ReactPHP tabu?
  6. +
+
+
+ + +
+
+
09
+
+

Profiling mit Blackfire

+
CPU- und Memory-Hotspots finden
+
+
+ +
+ Frage zum Einstieg + Deine App ist langsam – aber wo genau? Der Controller? Die DB-Query? Die View-Rendering? Raten endet im Polieren der falschen Stellen. Blackfire und Xdebug Profiler zeigen dir Call-Graphs mit echten Messzahlen. +
+ +

Xdebug-Profiler (gratis)

+

Xdebug bringt einen einfachen Profiler mit. In php.ini:

+ +
xdebug.mode=profile
+xdebug.start_with_request=trigger
+xdebug.output_dir=/tmp/xdebug
+ +

Mit ?XDEBUG_TRIGGER=1 als Query-Parameter aktivierst du das Profiling für einen Request. Ergebnis: eine Cachegrind-Datei in /tmp/xdebug/, die du mit KCacheGrind (Linux) oder QCacheGrind (macOS) öffnest.

+ +

Blackfire (professionell)

+

Blackfire ist ein kommerzieller Profiler mit Web-UI, Vergleichen, Regressions-Detection und Production-Profiling. Setup: Agent installieren, Probe als PHP-Extension, dann profilen:

+ +
# CLI-Script profilen
+blackfire run php script.php
+
+# Eine bestimmte URL
+blackfire curl https://example.com/slow-page
+
+# Mit Iterationen (Mittelwert)
+blackfire --samples=10 curl https://example.com/page
+ +

Das Ergebnis ist ein Call-Graph: jede Funktion mit ihrer Zeit, wer sie aufruft, wie oft, welcher Anteil an der Gesamtzeit.

+ +

Im Code instrumentieren

+ +
use Blackfire\Client;
+use Blackfire\Profile\Configuration;
+
+$blackfire = new Client();
+$config = (new Configuration())->setTitle('Order Processing');
+$probe = $blackfire->createProbe($config);
+
+// Code, der gemessen werden soll
+$service->processOrders($orders);
+
+$blackfire->endProbe($probe);
+ +

Typische Findings

+ + + + + + + + +
PatternWie oftLösung
N+1 Queryfast jede AppEager-Loading
JSON-Encoding mehrfachoftCache
Twig ohne Cachehäufigopcache.preload
Doctrine: gleiche Entity 100x geladenhäufigIdentity Map
Composer Autoload langsamoftcomposer dump-autoload -o
+ +

Memory-Profiling

+

Memory-Leaks sind oft schlimmer als CPU-Probleme. memory_get_peak_usage() gibt dir die maximale Speichernutzung eines Requests:

+ +
$start = memory_get_usage();
+processLargeData();
+$end = memory_get_usage();
+$peak = memory_get_peak_usage();
+
+echo "Used: " . ($end - $start) . " bytes\n";
+echo "Peak: $peak bytes\n";
+ +

Für tieferes Memory-Profiling: php-meminfo (Extension) oder Blackfire's Memory-Profile.

+ +
+
+
+ Misst, was zählt – nicht alles + Optimiere nicht blind nach Profiler-Ausgabe. Eine Funktion, die 30% der Zeit braucht, aber nur einmal pro Tag läuft, ist weniger wichtig als eine, die 5% braucht, aber 1000 mal pro Minute. Schau dir absolute Zahlen × Frequenz an, nicht nur Prozent. +
+
+ +
+ Recall +
    +
  1. Welcher Profiler ist gratis dabei?
  2. +
  3. Was ist ein N+1 Query und warum häufig?
  4. +
  5. Wofür ist memory_get_peak_usage()?
  6. +
+
+
+ + +
+
+
10
+
+

Caching-Strategien

+
APCu, Redis, HTTP-Cache, ESI
+
+
+ +
+ Frage zum Einstieg + Eine Datenbank-Query, die 200ms dauert. Eine Berechnung, die immer wieder dasselbe Ergebnis liefert. Ein externer API-Call, der teuer ist. Caching ist meist die größte Performance-Optimierung – aber wo cachen und wie invalidieren? +
+ +

Die Caching-Schichten

+ + + + + + + + + + +
SchichtLatenzTech
OpCache (kompilierter Code)~0mseingebaut
Request-Cache (in-memory)~0msArrays
APCu (shared memory)~0.01msAPCu Extension
Redis (lokal)~0.5msRedis-Server
Redis (Netzwerk)~1-5msremote Redis
Datenbank-Query~5-100msMySQL/Postgres
Externe API~50-1000msHTTP
+ +

Jede Schicht hinunter ist 10-100x langsamer. Die Kunst: so weit oben wie möglich cachen, ohne stale Daten zu liefern.

+ +

PSR-6 / PSR-16 Cache

+

Die PHP-FIG hat Cache-Interfaces standardisiert. Sym­fo­ny Cache, Laravel Cache und Doctrine Cache implementieren sie. Dein Code bleibt agnostisch:

+ +
use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\ItemInterface;
+
+class UserRepository {
+  public function __construct(private CacheInterface $cache) {}
+
+  public function find(int $id): ?User {
+    return $this->cache->get("user.$id", function(ItemInterface $item) use ($id) {
+      $item->expiresAfter(3600);   // 1 Stunde
+      return $this->loadFromDb($id);
+    });
+  }
+}
+ +

APCu für Request-übergreifende Caches

+

APCu ist Shared Memory pro PHP-Server-Prozess. Sehr schnell, aber:

+ +
    +
  • Pro Server-Maschine – nicht über mehrere Server verteilt
  • +
  • Daten gehen bei PHP-FPM-Reload verloren
  • +
  • Begrenzte Größe (Standard: 32MB)
  • +
+ +
// Direkte API
+apcu_store('key', $data, 300);   // 5 Min TTL
+$data = apcu_fetch('key');
+
+// Atomic increment für Counter
+$hits = apcu_inc('hits:home');
+ +

Redis für verteilte Caches

+ +
$redis = new \Redis();
+$redis->connect('redis-server', 6379);
+
+// Einfacher Key-Value
+$redis->set('user:42', json_encode($user), 3600);
+$cached = json_decode($redis->get('user:42'), true);
+
+// Pipeline für Batch-Operationen
+$pipe = $redis->multi(Redis::PIPELINE);
+foreach ($ids as $id) {
+  $pipe->get("user:$id");
+}
+$results = $pipe->exec();         // alle in einem Roundtrip
+ +

HTTP-Cache und ESI

+

Für komplette Seiten oder Fragmente bietet sich HTTP-Caching an. Cache-Control und ETag sagen Browser und CDNs, wann sie nicht neu fragen müssen:

+ +
// Symfony Response
+$response->setPublic();
+$response->setMaxAge(3600);
+$response->setSharedMaxAge(86400);    // CDN cached 1 Tag
+$response->setEtag(md5($content));
+ +

ESI (Edge Side Includes) erlaubt, dass CDN/Varnish Teile einer Seite separat cachen – Header bleibt 1 Stunde gecached, der User-spezifische Mini-Cart nur 1 Minute. Symfony unterstützt ESI nativ.

+ +

Cache-Invalidierung

+

Das schwerste Problem. Strategien:

+ +
    +
  • TTL – einfach, aber kann stale sein
  • +
  • Manuell bei Schreib-Operationen – fehlt leicht eine Stelle
  • +
  • Tags – Cache-Items haben Labels, du invalidierst per Label
  • +
  • Event-driven – Listener auf Domain-Events räumen Cache auf
  • +
+ +
+
i
+
+ "Es gibt zwei schwere Probleme in der Informatik..." + Cache-Invalidierung und Naming. Beide passieren immer. Akzeptiere, dass dein Cache manchmal stale ist, baue Monitoring ein, und plane Strategien für Invalidierung von Anfang an mit. +
+
+ +
+ Recall +
    +
  1. Wann nutzt du APCu, wann Redis?
  2. +
  3. Was macht ESI in Varnish?
  4. +
  5. Welche Strategien für Cache-Invalidierung kennst du?
  6. +
+
+
+ + +
+
+
11
+
+

Dependency Injection Container

+
Eigener Container in 100 Zeilen
+
+
+ +
+ Frage zum Einstieg + Symfony Container, Laravel Container, PHP-DI – alle lösen dasselbe Problem: Klassen automatisch instanziieren mit den richtigen Abhängigkeiten. Wie funktioniert das intern? Ein eigener DI-Container in 100 Zeilen Code zeigt das Prinzip. +
+ +

Was ein DI-Container tut

+

Du sagst dem Container: "Gib mir eine Instanz von OrderService". Er liest die Konstruktor-Signatur, sieht "braucht PaymentGateway und EmailSender", instanziiert die auch automatisch und übergibt sie:

+ +
class OrderService {
+  public function __construct(
+    private PaymentGateway $gateway,
+    private EmailSender $mailer,
+  ) {}
+}
+
+// Container kümmert sich um alles
+$container = new Container();
+$service = $container->get(OrderService::class);
+ +

Minimaler Container

+

Hier ein vereinfachter Container, der Klassen über Reflection auflöst. Production-Container haben mehr Features, das Kern-Prinzip bleibt:

+ +
class Container {
+  private array $bindings = [];
+  private array $instances = [];
+
+  public function bind(string $abstract, callable|string $concrete): void {
+    $this->bindings[$abstract] = $concrete;
+  }
+
+  public function singleton(string $abstract, callable|string $concrete): void {
+    $this->bind($abstract, $concrete);
+    $this->instances[$abstract] = null;
+  }
+
+  public function get(string $id): object {
+    // Singleton-Cache
+    if (array_key_exists($id, $this->instances) && $this->instances[$id]) {
+      return $this->instances[$id];
+    }
+
+    $instance = $this->resolve($id);
+
+    if (array_key_exists($id, $this->instances)) {
+      $this->instances[$id] = $instance;
+    }
+
+    return $instance;
+  }
+
+  private function resolve(string $id): object {
+    $concrete = $this->bindings[$id] ?? $id;
+
+    if (is_callable($concrete)) {
+      return $concrete($this);
+    }
+
+    $reflection = new \ReflectionClass($concrete);
+    $constructor = $reflection->getConstructor();
+
+    if (!$constructor) {
+      return new $concrete();
+    }
+
+    $args = [];
+    foreach ($constructor->getParameters() as $param) {
+      $type = $param->getType();
+      if ($type && !$type->isBuiltin()) {
+        $args[] = $this->get($type->getName());
+      } elseif ($param->isDefaultValueAvailable()) {
+        $args[] = $param->getDefaultValue();
+      } else {
+        throw new \Exception("Cannot resolve $id");
+      }
+    }
+
+    return $reflection->newInstanceArgs($args);
+  }
+}
+ +

Bindings konfigurieren

+ +
$container = new Container();
+
+// Interface → konkrete Klasse
+$container->bind(LoggerInterface::class, FileLogger::class);
+
+// Factory-Closure
+$container->bind(\PDO::class, fn() =>
+  new \PDO('mysql:host=localhost', 'user', 'pass')
+);
+
+// Singleton (gleiche Instanz für alle Calls)
+$container->singleton(CacheInterface::class, RedisCache::class);
+
+// Auflösen
+$service = $container->get(OrderService::class);
+// Container baut automatisch: PaymentGateway, EmailSender, deren Dependencies
+ +

Production-Container-Features

+

Echte Container haben noch viel mehr:

+ +
    +
  • Compile-Time-Optimierung – Bindings werden zu generiertem PHP-Code (Symfony)
  • +
  • Auto-Wiring – kein explizites Binding nötig, wenn Class-Name = Interface
  • +
  • Tags – Services mit Tags wie "event_listener" automatisch sammeln
  • +
  • Lazy-Loading – Services werden erst beim ersten Zugriff erzeugt
  • +
  • Parameter – Konfigurations-Werte (Strings, Arrays) als Konstanten injizieren
  • +
+ +
+
+
+ Nutze einen ausgereiften Container + Für eigene Projekte: PHP-DI oder Symfony DependencyInjection. Eigene Container bauen ist lehrreich, aber Production-Container haben Edge Cases und Performance-Optimierung, die du selbst nicht erreichst. +
+
+ +
+ Recall +
    +
  1. Was ist die Hauptaufgabe eines DI-Containers?
  2. +
  3. Welcher Reflection-Mechanismus ist Basis dafür?
  4. +
  5. Was machen "Tags" in Symfony's Container?
  6. +
+
+
+ + +
+
+
12
+
+

Event-Dispatcher Pattern

+
Lose Kopplung zwischen Bounded Contexts
+
+
+ +
+ Frage zum Einstieg + Eine Bestellung wird platziert. Folge: E-Mail an Kunde, Lagerplatz reservieren, Statistik aktualisieren, Webhook an Partner senden. Sollst du das alles in OrderService::place() reincoden? Das Event-Dispatcher-Pattern trennt das sauber. +
+ +

Das Problem direkter Aufrufe

+ +
class OrderService {
+  public function __construct(
+    private EmailService $mailer,
+    private InventoryService $inventory,
+    private AnalyticsService $analytics,
+    private WebhookService $webhooks,
+    // noch 5 weitere ...
+  ) {}
+
+  public function place(Order $order): void {
+    $this->save($order);
+    $this->mailer->sendConfirmation($order);
+    $this->inventory->reserve($order);
+    $this->analytics->track($order);
+    $this->webhooks->notify($order);
+    // ...
+  }
+}
+ +

OrderService kennt alle anderen Services. Jeder neue Listener bedeutet eine Code-Änderung. Tests brauchen alle Mocks. Tight Coupling.

+ +

Mit Event-Dispatcher

+

Der Service publiziert nur ein Event. Wer darauf reagiert, ist ihm egal. Andere Bounded Contexts subscriben sich:

+ +
class OrderPlaced {
+  public function __construct(public readonly Order $order) {}
+}
+
+class OrderService {
+  public function __construct(private EventDispatcherInterface $events) {}
+
+  public function place(Order $order): void {
+    $this->save($order);
+    $this->events->dispatch(new OrderPlaced($order));
+  }
+}
+
+// Listener leben in ihren eigenen Modulen
+class SendConfirmationListener {
+  public function __invoke(OrderPlaced $event): void {
+    $this->mailer->send($event->order->customer, 'Bestätigung');
+  }
+}
+
+class ReserveInventoryListener {
+  public function __invoke(OrderPlaced $event): void {
+    $this->inventory->reserve($event->order);
+  }
+}
+ +

Jetzt kann ein neues Team einen neuen Listener hinzufügen, ohne OrderService anzufassen. Tests des Services brauchen nur einen Mock-Dispatcher.

+ +

Symfony EventDispatcher

+

Symfony hat einen ausgereiften Dispatcher (PSR-14-konform). Listener werden über Tags oder Attribute registriert:

+ +
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
+
+#[AsEventListener(event: OrderPlaced::class)]
+class SendConfirmationListener {
+  public function __invoke(OrderPlaced $event): void {
+    // ...
+  }
+}
+ +

Symfony scannt beim Container-Build alle Klassen mit dem Attribut und verdrahtet sie automatisch.

+ +

Synchron vs. Asynchron

+

Standardmäßig laufen Listener synchron – im selben Request, hintereinander. Für langsame Operationen (E-Mail, Webhook) willst du sie async machen – sonst friert der User-Request ein:

+ +
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
+
+// Statt EventListener: MessageHandler in Symfony Messenger
+#[AsMessageHandler]
+class SendConfirmationHandler {
+  public function __invoke(OrderPlaced $message): void {
+    $this->mailer->send(...);
+  }
+}
+
+# config/messenger.yaml
+framework:
+  messenger:
+    routing:
+      App\Event\OrderPlaced: 'async'     # landet in Queue
+ +

Der User-Request kehrt sofort zurück, der Mail-Versand passiert im Worker-Prozess. Wichtig für UX und Resilienz.

+ +
+
i
+
+ Domain Events vs. Application Events + Domain Events (OrderPlaced) sind Teil der Geschäftslogik, beschreiben "was passiert ist". Application Events sind technisch (kernel.request in Symfony). Mische sie nicht – Domain Events leben im Domain-Layer, Application Events im Framework. +
+
+ +
+ Recall +
    +
  1. Was ist das Hauptproblem von direkten Service-Aufrufen ohne Events?
  2. +
  3. Wann nutzt du sync, wann async Event-Handling?
  4. +
  5. Was ist der Unterschied zwischen Domain Event und Application Event?
  6. +
+
+
+ + +
+
+
13
+
+

CQRS und Command Bus

+
Lese- und Schreibmodelle trennen
+
+
+ +
+ Frage zum Einstieg + Deine User-Liste braucht 12 verschiedene Filter, Pagination, Aggregate. Deine User-Updates haben komplexe Business-Regeln. Beide in einer Klasse abzubilden wird unhandlich. CQRS trennt Reads und Writes radikal. +
+ +

Was ist CQRS?

+

Command Query Responsibility Segregation – ein Pattern, das Lese-Operationen (Queries) und Schreib-Operationen (Commands) auf separate Modelle teilt:

+ +
    +
  • Commands ändern State, geben nichts zurück (oder nur ID)
  • +
  • Queries lesen State, ändern nichts
  • +
+ +
// Command: Intention "tu das"
+class CreateUserCommand {
+  public function __construct(
+    public readonly string $name,
+    public readonly string $email,
+  ) {}
+}
+
+// Query: Frage "wie ist das"
+class FindUsersByCountryQuery {
+  public function __construct(
+    public readonly string $country,
+    public readonly int $page = 1,
+  ) {}
+}
+ +

Command Bus

+

Statt Controller-Code, der direkt Repository-Aufrufe macht, dispatched er einen Command an den Bus. Der Bus findet den richtigen Handler:

+ +
class UserController {
+  public function create(Request $request, CommandBus $bus): Response {
+    $command = new CreateUserCommand(
+      name: $request->getString('name'),
+      email: $request->getString('email'),
+    );
+
+    $bus->dispatch($command);
+
+    return new Response('Created', 201);
+  }
+}
+
+// Handler – die einzige Stelle mit Business-Logik
+class CreateUserCommandHandler {
+  public function __construct(private UserRepository $repo) {}
+
+  public function __invoke(CreateUserCommand $cmd): void {
+    $user = new User($cmd->name, $cmd->email);
+    $this->repo->save($user);
+  }
+}
+ +

Query Bus

+

Analog für Reads – aber mit Rückgabewert. Queries lesen oft aus optimierten Read-Modellen (denormalisierte Views, Search-Index):

+ +
class FindUsersByCountryQueryHandler {
+  public function __construct(private \PDO $db) {}
+
+  public function __invoke(FindUsersByCountryQuery $q): array {
+    // Direktes SQL, optimiert für die View
+    $stmt = $this->db->prepare('
+      SELECT id, name, email, created_at, order_count
+      FROM users_with_stats
+      WHERE country = ?
+      LIMIT ? OFFSET ?
+    ');
+    $stmt->execute([$q->country, 50, ($q->page - 1) * 50]);
+
+    return $stmt->fetchAll();
+  }
+}
+ +

Beachte: keine Entities, kein Repository – direkter DB-Zugriff auf eine speziell für diese Query optimierte View. Performance über Domain-Sauberkeit, weil Queries nichts ändern.

+ +

Symfony Messenger als Bus

+

Symfony Messenger ist nicht nur für async – auch als Command/Query Bus nutzbar:

+ +
# config/messenger.yaml
+framework:
+  messenger:
+    buses:
+      command.bus:
+        middleware:
+          - validation
+          - doctrine_transaction
+      query.bus:
+        middleware:
+          - validation
+ +
class UserController {
+  public function __construct(
+    private MessageBusInterface $commandBus,
+    private MessageBusInterface $queryBus,
+  ) {}
+
+  public function list(string $country): Response {
+    $users = $this->queryBus->dispatch(new FindUsersByCountryQuery($country));
+    return Response::json($users);
+  }
+}
+ +

Wann lohnt sich CQRS?

+ + + + + + + + +
Use CaseCQRS sinnvoll?
Simple CRUD-Appnein, Overkill
Komplexe Business-Logik bei Writesja
Lese-Performance ist kritischja
Sehr unterschiedliche Read- und Write-Modelleja
Mehrere Teams an einem Bounded Contextja
+ +
+
!
+
+ CQRS ist keine Silver Bullet + Es bringt mehr Code, mehr Klassen, mehr Indirection. Bei einer Standard-CRUD-App ist es Overkill. Wende es gezielt an: nur dort wo Komplexität es rechtfertigt. Innerhalb einer App kannst du auch nur einen Bounded Context mit CQRS bauen, den Rest mit klassischen Services. +
+
+ +
+ Recall +
    +
  1. Was ist der Hauptunterschied zwischen Command und Query?
  2. +
  3. Warum dürfen Query-Handler "denormalisiert" lesen?
  4. +
  5. Wann ist CQRS Overkill?
  6. +
+
+
+ + +
+
+
14
+
+

Domain-Driven Design in PHP

+
Value Objects, Entities, Aggregates
+
+
+ +
+ Frage zum Einstieg + Anämische Entities sind in PHP weit verbreitet: Klassen mit nur Properties und Getter/Setter, alle Business-Logik in Service-Klassen verstreut. Domain-Driven Design dreht das um – Logik lebt im Domain-Modell, nicht außerhalb. +
+ +

Value Objects

+

Ein Value Object ist ein Wert ohne eigene Identität, unveränderlich. Statt string $email nimm Email $email – mit eingebauter Validierung:

+ +
final class Email {
+  public function __construct(public readonly string $value) {
+    if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
+      throw new \InvalidArgumentException("Ungültige Email: $value");
+    }
+  }
+
+  public function domain(): string {
+    return substr($this->value, strpos($this->value, '@') + 1);
+  }
+
+  public function equals(Email $other): bool {
+    return strtolower($this->value) === strtolower($other->value);
+  }
+}
+
+$email = new Email('marek@example.com');  // validiert automatisch
+$email->domain();                          // 'example.com'
+ +

Wo immer eine Email durch deinen Code wandert, ist sie garantiert valide. Keine "ist das ein gültiges Format?"-Checks mehr überall.

+ +

Entities mit Geschäftslogik

+

Eine Entity hat eine Identität (z.B. ID), kann sich über die Zeit verändern, aber bleibt dasselbe Objekt. Klassisches Anti-Pattern: anämische Entity mit nur Gettern/Settern. DDD-Ansatz: Logik lebt in der Entity:

+ +
class Order {
+  private array $items = [];
+  private OrderStatus $status;
+
+  public function __construct(public readonly OrderId $id, public readonly CustomerId $customerId) {
+    $this->status = OrderStatus::Draft;
+  }
+
+  public function addItem(Product $product, int $quantity): void {
+    if ($this->status !== OrderStatus::Draft) {
+      throw new CannotModifyConfirmedOrder();
+    }
+    $this->items[] = new OrderItem($product, $quantity);
+  }
+
+  public function confirm(): void {
+    if (empty($this->items)) throw new CannotConfirmEmptyOrder();
+    $this->status = OrderStatus::Confirmed;
+  }
+
+  public function total(): Money {
+    return array_reduce(
+      $this->items,
+      fn(Money $sum, OrderItem $item) => $sum->add($item->subtotal()),
+      Money::zero()
+    );
+  }
+}
+ +

Die Order schützt ihre eigenen Invarianten – sie lässt nicht zu, dass jemand sie in einen inkonsistenten Zustand bringt.

+ +

Aggregates und Aggregate Root

+

Ein Aggregate ist eine Gruppe von Objekten, die zusammen konsistent bleiben müssen. Der Aggregate Root ist der einzige Einstiegspunkt:

+ +
// Order ist Aggregate Root
+// OrderItem ist Teil des Aggregates, NICHT direkt zugreifbar
+
+// Schlecht: OrderItem direkt aus Repository holen
+$item = $itemRepo->find(123);
+$item->quantity = 5;
+$itemRepo->save($item);   // Order weiß nichts davon → Inkonsistenz
+
+// Gut: Immer durch Aggregate Root
+$order = $orderRepo->find($orderId);
+$order->changeItemQuantity($itemId, 5);  // Order prüft Invarianten
+$orderRepo->save($order);
+ +

Repositories

+

Ein Repository ist die Schnittstelle zur Persistenz für ein Aggregate. Er sieht aus wie eine Collection:

+ +
interface OrderRepository {
+  public function find(OrderId $id): ?Order;
+  public function save(Order $order): void;
+  public function remove(Order $order): void;
+}
+
+// Implementierung mit Doctrine
+class DoctrineOrderRepository implements OrderRepository {
+  public function __construct(private EntityManagerInterface $em) {}
+
+  public function find(OrderId $id): ?Order {
+    return $this->em->find(Order::class, $id->value);
+  }
+
+  public function save(Order $order): void {
+    $this->em->persist($order);
+    $this->em->flush();
+  }
+}
+ +
+
i
+
+ DDD ist mehr als nur Klassen-Struktur + DDD beinhaltet auch strategisches Design: Bounded Contexts, Ubiquitous Language, Context Maps. Die taktischen Patterns (Value Object, Entity, Aggregate) sind nur das Werkzeug für die strategische Trennung von Geschäftsbereichen. +
+
+ +
+ Recall +
    +
  1. Was unterscheidet Value Object von Entity?
  2. +
  3. Was ist die Regel für Zugriff auf Aggregate-Member?
  4. +
  5. Wofür ist ein Repository da?
  6. +
+
+
+ + +
+
+
15
+
+

Hexagonal Architecture

+
Ports und Adapter, Domain im Zentrum
+
+
+ +
+ Frage zum Einstieg + Deine App hängt überall an Symfony, an Doctrine, an Redis. Refactor auf ein anderes Framework? Praktisch unmöglich. Tests, die echte DB brauchen? Langsam. Hexagonal Architecture (auch "Ports und Adapter") löst das mit konsequenter Inversion of Control. +
+ +

Das Prinzip

+

Im Zentrum steht die Domain (Geschäftslogik). Drumherum sind Ports (Interfaces) und Adapter (Implementierungen). Die Domain weiß nichts von Symfony, Doctrine oder HTTP – sie kennt nur ihre eigenen Interfaces:

+ +
             ┌─────────────────┐
+   HTTP →    │                 │
+             │                 │
+   CLI  →    │     DOMAIN      │  → Database
+             │   (Geschäfts-   │  → File-Storage
+   Worker →  │     logik)      │  → External API
+             │                 │
+             └─────────────────┘
+              ↑               ↓
+           Adapter         Adapter
+           (Inbound)       (Outbound)
+ +

Inbound Port: was die Domain anbietet

+ +
// Inbound Port: definiert, was die Domain anbietet
+// Liegt im src/Application/
+interface PlaceOrderUseCase {
+  public function execute(PlaceOrderInput $input): PlaceOrderOutput;
+}
+
+// Implementierung in der Domain (kennt keine Framework-Klassen)
+class PlaceOrderService implements PlaceOrderUseCase {
+  public function __construct(
+    private OrderRepository $orders,
+    private PaymentGateway $payment,
+    private EventDispatcher $events,
+  ) {}
+
+  public function execute(PlaceOrderInput $input): PlaceOrderOutput {
+    $order = new Order(...);
+    $this->payment->charge($order);
+    $this->orders->save($order);
+    $this->events->dispatch(new OrderPlaced($order));
+
+    return new PlaceOrderOutput($order->id);
+  }
+}
+ +

Inbound Adapter: HTTP, CLI, etc.

+

Der HTTP-Controller ist ein Adapter – er übersetzt zwischen HTTP-Request und Domain-Input:

+ +
// Inbound Adapter (Symfony Controller)
+// Liegt im src/Infrastructure/Http/
+class PlaceOrderController {
+  public function __construct(private PlaceOrderUseCase $useCase) {}
+
+  #[Route('/orders', methods: ['POST'])]
+  public function __invoke(Request $request): JsonResponse {
+    $input = new PlaceOrderInput(
+      customerId: $request->get('customer_id'),
+      items: $request->get('items'),
+    );
+
+    try {
+      $output = $this->useCase->execute($input);
+      return new JsonResponse(['order_id' => $output->orderId], 201);
+    } catch (DomainException $e) {
+      return new JsonResponse(['error' => $e->getMessage()], 400);
+    }
+  }
+}
+ +

Outbound Port und Adapter

+

Die Domain definiert, was sie braucht (Port), die Infrastruktur liefert wie (Adapter):

+ +
// Outbound Port (in der Domain)
+// Liegt im src/Domain/
+interface PaymentGateway {
+  public function charge(Order $order): PaymentReceipt;
+}
+
+// Outbound Adapter (in der Infrastructure)
+// Liegt im src/Infrastructure/Payment/
+class StripeGateway implements PaymentGateway {
+  public function __construct(private StripeClient $stripe) {}
+
+  public function charge(Order $order): PaymentReceipt {
+    $charge = $this->stripe->charges->create([...]);
+    return new PaymentReceipt($charge->id);
+  }
+}
+
+// Alternative für Tests
+class FakePaymentGateway implements PaymentGateway {
+  public function charge(Order $order): PaymentReceipt {
+    return new PaymentReceipt('fake-receipt');
+  }
+}
+ +

Vorteile

+ +
    +
  • Tests ohne Infrastruktur – Domain mit Fakes statt echter DB/API
  • +
  • Framework-Wechsel möglich – Domain unabhängig von Symfony
  • +
  • Klare Verantwortung – jede Schicht hat ihren Job
  • +
  • Adapter parallel – HTTP, CLI und Worker rufen denselben Use Case
  • +
+ +

Verzeichnis-Struktur

+ +
src/
+├── Domain/                  # Reine Business-Logik
+│   ├── Order/
+│   │   ├── Order.php
+│   │   ├── OrderId.php
+│   │   ├── OrderRepository.php   # Interface
+│   │   └── PaymentGateway.php    # Interface
+│   └── User/...
+├── Application/             # Use Cases
+│   ├── PlaceOrderUseCase.php
+│   └── PlaceOrderService.php
+└── Infrastructure/          # Adapters
+    ├── Http/
+    │   └── PlaceOrderController.php
+    ├── Persistence/
+    │   └── DoctrineOrderRepository.php
+    └── Payment/
+        └── StripeGateway.php
+ +
+
!
+
+ Nicht jede App braucht Hexagonal + Die Architektur kostet: mehr Interfaces, mehr Klassen, mehr Indirection. Für CRUD-Apps Overkill. Lohnt sich, wenn die Geschäftslogik komplex und langlebig ist, Tests wichtig sind und die App über Jahre weiterleben soll. +
+
+ +
+ Recall +
    +
  1. Wo lebt die Geschäftslogik in Hexagonal Architecture?
  2. +
  3. Was ist der Unterschied zwischen Port und Adapter?
  4. +
  5. Warum kann man die Domain ohne Datenbank testen?
  6. +
+
+
+ + +
+
+
16
+
+

Event Sourcing

+
State als Sequenz von Events
+
+
+ +
+ Frage zum Einstieg + Klassische DB speichert den aktuellen Zustand: "Bestellung X hat 3 Items, Status 'paid'". Wer hat wann was geändert? Verloren. Event Sourcing dreht das um: speichere die Events, nicht den Zustand. Der aktuelle Zustand ist die Summe aller Events. +
+ +

Das Grundprinzip

+

Statt: UPDATE orders SET status='paid' WHERE id=42 speicherst du: OrderPaid(orderId: 42, at: 2026-01-15) in einer Event-Store-Tabelle. Der aktuelle Zustand entsteht durch Replay aller Events:

+ +
events_table:
++----+------------+------------------+----------+
+| id | aggregate  | event_type       | data     |
++----+------------+------------------+----------+
+|  1 | order:42   | OrderCreated     | {...}    |
+|  2 | order:42   | ItemAdded        | {...}    |
+|  3 | order:42   | ItemAdded        | {...}    |
+|  4 | order:42   | OrderConfirmed   | {...}    |
+|  5 | order:42   | OrderPaid        | {...}    |
++----+------------+------------------+----------+
+ +

Aggregate mit Events

+ +
abstract class AggregateRoot {
+  private array $pendingEvents = [];
+  private int $version = 0;
+
+  protected function recordEvent(DomainEvent $event): void {
+    $this->apply($event);
+    $this->pendingEvents[] = $event;
+  }
+
+  public function pullEvents(): array {
+    $events = $this->pendingEvents;
+    $this->pendingEvents = [];
+    return $events;
+  }
+
+  abstract protected function apply(DomainEvent $event): void;
+}
+
+class Order extends AggregateRoot {
+  private OrderStatus $status;
+  private array $items = [];
+
+  public static function create(OrderId $id, CustomerId $customer): self {
+    $order = new self();
+    $order->recordEvent(new OrderCreated($id, $customer));
+    return $order;
+  }
+
+  public function addItem(Product $product, int $quantity): void {
+    if ($this->status !== OrderStatus::Draft) throw new ...;
+    $this->recordEvent(new ItemAdded($product, $quantity));
+  }
+
+  protected function apply(DomainEvent $event): void {
+    match($event::class) {
+      OrderCreated::class => $this->whenOrderCreated($event),
+      ItemAdded::class    => $this->whenItemAdded($event),
+      // ...
+    };
+  }
+
+  private function whenOrderCreated(OrderCreated $e): void {
+    $this->status = OrderStatus::Draft;
+  }
+
+  private function whenItemAdded(ItemAdded $e): void {
+    $this->items[] = new OrderItem($e->product, $e->quantity);
+  }
+}
+ +

Beachte die zwei Phasen: recordEvent erstellt das Event und ruft apply auf. apply ändert nur State, niemals Validierung – sonst kann beim Replay nichts schiefgehen.

+ +

Rekonstruieren aus Events

+

Beim Laden eines Aggregates: alle Events aus dem Store holen, neuen Aggregate-Instanz erstellen, jedes Event apply'en:

+ +
class EventSourcedOrderRepository {
+  public function __construct(private EventStore $store) {}
+
+  public function find(OrderId $id): ?Order {
+    $events = $this->store->getEvents("order:$id");
+    if (empty($events)) return null;
+
+    $order = (new \ReflectionClass(Order::class))->newInstanceWithoutConstructor();
+    foreach ($events as $event) {
+      $order->applyHistoric($event);
+    }
+    return $order;
+  }
+
+  public function save(Order $order): void {
+    foreach ($order->pullEvents() as $event) {
+      $this->store->append("order:$order->id", $event);
+    }
+  }
+}
+ +

Vorteile

+ +
    +
  • Komplette Historie – jede Änderung ist nachvollziehbar, "wer hat wann was?"
  • +
  • Audit-Trail – ideal für Banking, Healthcare, Compliance
  • +
  • Time-Travel-Debugging – State zu einem beliebigen Zeitpunkt rekonstruieren
  • +
  • Multiple Projections – verschiedene Read-Modelle aus denselben Events bauen
  • +
  • Natürlich mit CQRS kombinierbar
  • +
+ +

Nachteile

+ +
    +
  • Komplexer als CRUD – nicht für alles geeignet
  • +
  • Event-Schema-Evolution – Events sind ewig, Schema-Änderungen schwierig
  • +
  • Performance – Replay vieler Events kostet → Snapshots als Optimierung
  • +
  • Eventual Consistency – Reads kommen aus Projektionen, nicht aus Events direkt
  • +
+ +
+
i
+
+ Libraries für Event Sourcing in PHP + EventSauce ist die populärste Library im PHP-Ökosystem – ausgereift, mit Snapshot-Support, Upcasting für Schema-Evolution und CQRS-Integration. Broadway und Prooph sind ältere Alternativen. +
+
+ +
+ Recall +
    +
  1. Was speichert ein Event Store statt aktuellem State?
  2. +
  3. Warum trennt man recordEvent und apply?
  4. +
  5. Welche Library nutzt man typischerweise in PHP?
  6. +
+
+
+ + +
+

Wie es weitergeht

+ +

Du hast PHP jetzt von Sprach-Internals über Performance-Optimierung bis zu fortgeschrittenen Architektur-Patterns durchlaufen. Damit kennst du PHP an seinen Grenzen.

+ +

Spaced-Repetition-Plan

+
+
+ Heute +

Guide gelesen, Recall-Fragen aus jedem Kapitel beantwortet.

+
+
+ +7 Tage +

Zwei Kapitel auswählen, vertiefen, eigene Implementierung versuchen.

+
+
+ +30 Tage +

Spezialthema umsetzen: ReactPHP-Server, eigener DI-Container oder Hexagonal Architecture in echtem Projekt.

+
+
+ +90 Tage +

Source-Code von Symfony, Doctrine oder EventSauce lesen – Patterns erkennen.

+
+
+ +

Was als nächstes lernen

+

Du bist jetzt jenseits des offiziellen PHP-Lernpfads. Empfehlungen für Tiefenexpertise:

+ +
    +
  • PHP-Source-Code – Zend Engine in C, internal-Verzeichnis auf GitHub
  • +
  • Symfony-Internals – Container-Compilation, HttpKernel-Lifecycle
  • +
  • Doctrine Internals – Unit-of-Work, Hydration, Schema-Tool
  • +
  • Eigene Extensions schreiben – C-Extension mit PHP-CPP oder Zephir
  • +
  • Sprach-Design – RFCs lesen, an PHP Internals Mailing-List teilnehmen
  • +
  • Performance-Tuning – OpCache-Internals, JIT-Verhalten, Memory-Layout
  • +
+ +

Begleitmaterial

+

Dieser Guide schließt das Set ab:

+
    +
  • PHP OnePager – die visuelle Übersicht
  • +
  • PHP Cheatsheet – die dichte Referenz
  • +
  • PHP Mini-Guide – der 15-Min-Schnelleinstieg
  • +
  • PHP Anfänger-Guide – die Grundlagen
  • +
  • PHP Fortgeschritten-Guide – Patterns und Production
  • +
  • PHP Extended-Guide (dieses Dokument) – Internals und Architektur
  • +
+
+ + + +``` \ No newline at end of file diff --git a/templates/Referenz/IntermediateGuide.md b/templates/Referenz/IntermediateGuide.md new file mode 100644 index 0000000..b50e449 --- /dev/null +++ b/templates/Referenz/IntermediateGuide.md @@ -0,0 +1,1874 @@ +``` + + + + +PHP Fortgeschritten-Guide + + + + + +
+
+ +
Fortgeschritten-Guide · 3h · Stand 2026
+
+ +
+

PHP
tiefer.

+

Von Interfaces und Generators bis Tests – Production-PHP, das hält.

+
+ +
+
+ Was du danach kannst + OOP-Patterns idiomatisch einsetzen · funktional mit Closures und Generators arbeiten · Attribute für saubere Metadaten nutzen · Datenbanken mit PDO sicher anbinden · APIs konsumieren · mit PHPStan und PHPUnit produktionsreifen Code schreiben. +
+
PHP 8.4
+
+
+ + +
+

Inhalt

+ +
Teil 1 · OOP-Patterns
+ +
+
1
+
+

Interfaces und abstrakte Klassen

+

Kontrakte definieren, Implementierungen trennen

+
+
15 Min
+
+ +
+
2
+
+

Traits

+

Code-Reuse jenseits klassischer Vererbung

+
+
15 Min
+
+ +
+
3
+
+

Enums richtig nutzen

+

Backed Enums, Methods, Pattern-Matching

+
+
15 Min
+
+ +
+
4
+
+

Attribute (PHP 8)

+

Metadaten typensicher statt Annotations

+
+
15 Min
+
+ +
Teil 2 · Funktional & Generators
+ +
+
5
+
+

Closures und Arrow Functions

+

First-Class Callables, use-Capture

+
+
15 Min
+
+ +
+
6
+
+

Higher-Order Functions

+

array_map, array_filter, array_reduce in der Praxis

+
+
15 Min
+
+ +
+
7
+
+

Iterators und Generators

+

Lazy Evaluation mit yield

+
+
15 Min
+
+ +
+
8
+
+

Generics via PHPDoc

+

Templates ohne native Generics

+
+
15 Min
+
+ +
Teil 3 · Production-Tools
+ +
+
9
+
+

PDO und sichere Datenbanken

+

Prepared Statements, Transaktionen

+
+
15 Min
+
+ +
+
10
+
+

HTTP-Requests mit Guzzle

+

APIs konsumieren, async Requests

+
+
15 Min
+
+ +
+
11
+
+

PHPStan und Static Analysis

+

Bugs vor der Ausführung finden

+
+
15 Min
+
+ +
+
12
+
+

Testen mit PHPUnit

+

Unit-Tests, Mocks, Data Providers

+
+
15 Min
+
+
+ + +
+
+
01
+
+

Interfaces und abstrakte Klassen

+
Kontrakte definieren, Implementierungen trennen
+
+
+ +
+ Frage zum Einstieg + Du hast drei verschiedene Logger – Datei, Datenbank, Sentry. Sie sollen austauschbar sein, ohne dass der aufrufende Code etwas merkt. Wie zwingst du sie zu einer gemeinsamen API, ohne sie zu Geschwistern in einer Klassen-Hierarchie zu machen? Interfaces sind die Antwort. +
+ +

Interface als reiner Kontrakt

+

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.

+ +

Dependency Injection in Aktion

+ +
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'));
+ +

Abstrakte Klassen

+

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;
+}
+ +

Interface oder abstrakte Klasse?

+ + + + + + + +
Use CaseWähle
Reiner KontraktInterface
Mehrere "Verträge" erfüllenInterface (mehrfach implementierbar)
Gemeinsame Code-Basis teilenAbstrakte Klasse
Template Method PatternAbstrakte Klasse
+ +
+
+
+ PSR-Interfaces nutzen + Die PHP-FIG hat Standard-Interfaces für Logger, Container, HTTP und mehr definiert: Psr\Log\LoggerInterface, Psr\Container\ContainerInterface. Wenn du deine Klassen daran ausrichtest, passen sie zu jedem Framework. +
+
+ +
+ Recall +
    +
  1. Was ist der Hauptunterschied zwischen Interface und abstrakter Klasse?
  2. +
  3. Wie viele Interfaces darf eine Klasse implementieren?
  4. +
  5. Was ist Dependency Injection in einem Satz?
  6. +
+
+
+ + +
+
+
02
+
+

Traits

+
Code-Reuse jenseits klassischer Vererbung
+
+
+ +
+ Frage zum Einstieg + PHP unterstützt nur Einfach-Vererbung – eine Klasse hat genau eine Eltern-Klasse. Aber was, wenn zwei nicht verwandte Klassen denselben Code brauchen? Logging-Methoden in User und Order kopieren? Traits lösen das eleganter. +
+ +

Trait: wiederverwendbarer Code-Block

+

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
+}
+ +

Mehrere Traits kombinieren

+ +
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
+}
+ +

Konflikt-Auflösung

+

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 kein Multi-Inheritance-Ersatz + Traits machen Code-Reuse, keinen Typ-Polymorphismus. Eine Klasse mit Trait ist nicht "vom Typ Trait" – nutze dafür Interfaces. Faustregel: Trait für Implementierung, Interface für Vertrag. +
+
+ +

Wann Trait, wann Komposition?

+

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:

+ +
    +
  • Trait, wenn die Logik Teil des Objekts ist (Timestamps, Soft-Delete)
  • +
  • Komposition, wenn die Logik austauschbar sein soll (verschiedene Logger, Strategien)
  • +
+ +
+ Recall +
    +
  1. Was ist der Unterschied zwischen Trait und abstrakter Klasse?
  2. +
  3. Wie löst du Methoden-Konflikte zwischen zwei Traits?
  4. +
  5. Wann nutzt du Komposition statt Trait?
  6. +
+
+
+ + +
+
+
03
+
+

Enums richtig nutzen

+
Backed Enums, Methods, Pattern-Matching
+
+
+ +
+ Frage zum Einstieg + Früher waren Status-Werte oft Strings ("draft", "published") oder Klassen-Konstanten – fehleranfällig und ohne Typ-Sicherheit. PHP 8.1 hat echte Enums gebracht. Wie nutzt du sie idiomatisch? +
+ +

Einfache Enums

+ +
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.

+ +

Backed Enums (mit Wert)

+

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)
+ +

Methoden in Enums

+

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
+ +

Alle Cases iterieren

+ +
foreach (Status::cases() as $status) {
+  echo $status->label() . "\n";
+}
+// Entwurf
+// Veröffentlicht
+// Archiv
+ +

Praktisch für Dropdowns, Validierung oder Migration zwischen Status.

+ +
+
+
+ Statt String-Constants immer Enum + Wenn du heute 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. +
+
+ +
+ Recall +
    +
  1. Was ist der Unterschied zwischen einfachem Enum und Backed Enum?
  2. +
  3. Wie konvertierst du einen String sicher zu einem Enum-Case?
  4. +
  5. Was gibt Status::cases() zurück?
  6. +
+
+
+ + +
+
+
04
+
+

Attribute (PHP 8)

+
Metadaten typensicher statt Annotations
+
+
+ +
+ Frage zum Einstieg + Bevor PHP 8 nutzten Frameworks wie Symfony und Doctrine Doc-Block-Annotations für Metadaten: /** @Route("/users") */. Kommentare, die geparst werden – brüchig, ohne Typ-Check. PHP 8 brachte native Attribute. Wie schreibst und nutzt du sie? +
+ +

Attribut definieren

+

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.

+ +

Attribut anwenden

+ +
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 { /* ... */ }
+}
+ +

Attribute zur Laufzeit auslesen

+

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
+ +

Typische Anwendungsfälle

+ + + + + + + +
FrameworkBeispiel-Attribute
Symfony#[Route], #[AsCommand]
Doctrine#[ORM\Entity], #[ORM\Column]
PHPUnit#[DataProvider], #[Test]
Validator#[Assert\NotBlank]
+ +
+
i
+
+ Attribute statt Doc-Block-Annotations + Alter Code mit /** @Route("/users") */ funktioniert noch, aber das Doctrine/Symfony-Ökosystem migriert auf native Attribute. Bei Neuentwicklungen: immer Attribute, nie Annotations. +
+
+ +
+ Recall +
    +
  1. Was ist der Vorteil von Attributen gegenüber Doc-Block-Annotations?
  2. +
  3. Wie liest du Attribute zur Laufzeit aus?
  4. +
  5. Welche Frameworks nutzen Attribute intensiv?
  6. +
+
+
+ + +
+
+
05
+
+

Closures und Arrow Functions

+
First-Class Callables, use-Capture
+
+
+ +
+ Frage zum Einstieg + Funktionen als Werte – das ist seit Jahren in JavaScript Standard. Auch PHP kann das, mit Closures, Arrow Functions und seit PHP 8.1 First-Class Callable Syntax. Wann nutzt du was? +
+ +

Closures: anonyme Funktionen

+

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]
+ +

Variablen aus dem Kontext: use

+

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
+ +

Arrow Functions: kompakt

+

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);
+ +

First-Class Callable Syntax

+

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);
+ +
+
+
+ Arrow für einfach, Closure für komplex + Faustregel: Arrow Function (fn) wenn der Body ein einzelner Ausdruck ist. Volle Closure (function) wenn du mehrere Statements brauchst oder explizites Variablen-Capturing willst. +
+
+ +
+ Recall +
    +
  1. Was ist der Unterschied zwischen use ($x) und use (&$x)?
  2. +
  3. Was capturen Arrow Functions automatisch?
  4. +
  5. Wofür nutzt du First-Class Callable Syntax strtoupper(...)?
  6. +
+
+
+ + +
+
+
06
+
+

Higher-Order Functions

+
array_map, array_filter, array_reduce in der Praxis
+
+
+ +
+ Frage zum Einstieg + Du willst aus einer Liste von Bestellungen die Beträge aller bezahlten zusammenrechnen. Klassisch mit foreach, Zwischenvariable, Bedingung – fünf Zeilen. Mit Higher-Order Functions wird daraus ein lesbarer Einzeiler. Wie? +
+ +

array_map: transformieren

+

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: filtern

+

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: zusammenfassen

+

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
+ +

Verkettung in der Praxis

+

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.

+ +
+
i
+
+ illuminate/collections für komplexere Fälle + Wenn du oft mit verketteten Operationen arbeitest, lohnt sich Laravels Collection-Library (auch standalone nutzbar): collect($users)->filter(...)->map(...)->sum(). Lesbarer als verschachtelte PHP-Standardfunktionen. +
+
+ +
+ Recall +
    +
  1. Was ist die Reihenfolge der Argumente bei array_map vs. array_filter?
  2. +
  3. Welches Detail bei array_filter überrascht oft?
  4. +
  5. Wann ist eine verschachtelte Verkettung weniger lesbar als Zwischenvariablen?
  6. +
+
+
+ + +
+
+
07
+
+

Iterators und Generators

+
Lazy Evaluation mit yield
+
+
+ +
+ Frage zum Einstieg + Du sollst die Zeilen einer 5-GB-Logdatei verarbeiten. Komplett ins Array laden? Speicher reicht nicht. Klassische File-Funktionen mit Schleife? Funktioniert, aber Code wird unhandlich, wenn er weiterverarbeitet werden soll. Generators lösen das elegant. +
+ +

yield: Werte häppchenweise produzieren

+

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.

+ +

yield with key

+ +
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";
+}
+ +

Unendliche Sequenzen

+

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
+ +

yield from: Generators verschachteln

+

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;
+}
+ +
+
!
+
+ Generator nur einmal durchlaufen + Generatoren sind nicht rewindable. Wenn du sie zweimal mit foreach durchläufst, ist der zweite Durchlauf leer. Brauchst du Mehrfach-Zugriff, konvertiere mit iterator_to_array($gen) zu einem Array. +
+
+ +
+ Recall +
    +
  1. Welcher Vorteil von Generators bei großen Datenmengen?
  2. +
  3. Was passiert, wenn du einen Generator zweimal mit foreach durchläufst?
  4. +
  5. Wofür ist yield from da?
  6. +
+
+
+ + +
+
+
08
+
+

Generics via PHPDoc

+
Templates ohne native Generics
+
+
+ +
+ Frage zum Einstieg + PHP hat keine nativen Generics wie TypeScript oder Java. Wenn du eine Collection-Klasse schreibst, weiß die IDE nicht, ob da User oder Order drin sind – jeder Zugriff wird zu mixed. Die Lösung: PHPDoc-Generics mit PHPStan oder Psalm. +
+ +

Das Problem ohne Generics

+ +
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
+ +

Generics in PHPDoc

+

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;
+  }
+}
+ +

Konkreten Typ angeben

+

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
+ +

Constraints und Bounds

+ +
/**
+ * @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();
+ +

array-shape für strukturierte Arrays

+

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
+ +
+
i
+
+ Native Generics kommen vielleicht + Es gibt seit Jahren Diskussionen um native Generics in PHP. Ein RFC ist mehrfach gescheitert wegen Implementierungs-Komplexität (Runtime-Type-Erasure vs. Reified). Bis dahin sind PHPDoc-Generics mit PHPStan/Psalm der Standard. +
+
+ +
+ Recall +
    +
  1. Welche Tools verstehen PHPDoc-Generics?
  2. +
  3. Wie deklarierst du eine generische Klasse?
  4. +
  5. Wofür ist array{name: string, age: int}?
  6. +
+
+
+ + +
+
+
09
+
+

PDO und sichere Datenbanken

+
Prepared Statements, Transaktionen
+
+
+ +
+ Frage zum Einstieg + SQL-Injection ist seit 20 Jahren die häufigste Web-Sicherheitslücke. Sie ist trivial zu verhindern – mit Prepared Statements. PDO ist PHPs eingebaute, datenbank-agnostische Schnittstelle dafür. Wie nutzt du sie richtig? +
+ +

PDO-Verbindung aufbauen

+

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).

+ +

Prepared Statements (immer!)

+

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]);
+ +

Ergebnisse abrufen

+ +
// 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();
+ +

Transaktionen

+

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;
+}
+ +
+
!
+
+ Niemals mysql_* oder mysqli ohne Prepared Statements + Die alten 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. +
+
+ +
+ Recall +
    +
  1. Welche drei Options gehören in jeden PDO-Konstruktor?
  2. +
  3. Warum sind Prepared Statements sicherer als String-Konkatenation?
  4. +
  5. Wann nutzt du eine Transaktion?
  6. +
+
+
+ + +
+
+
10
+
+

HTTP-Requests mit Guzzle

+
APIs konsumieren, async Requests
+
+
+ +
+ Frage zum Einstieg + Du musst eine externe API ansprechen – REST mit JSON, Authentifizierung, vielleicht Retry bei 503. Mit nativen Funktionen wie file_get_contents oder cURL geht das, aber wird schnell hässlich. Guzzle ist Standard-Library für HTTP in PHP. +
+ +

Setup

+ +
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 und POST

+ +
// 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');
+ +

Fehlerbehandlung

+

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
+}
+ +

Async Requests

+

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-18 als alternative + Wenn du framework-agnostisch bleiben willst, programmiere gegen Psr\Http\Client\ClientInterface. Guzzle implementiert das, aber auch andere Clients (z.B. Symfony HttpClient). Du tauschst sie dann ohne Code-Änderung aus. +
+
+ +
+ Recall +
    +
  1. Wofür ist base_uri in der Client-Konfiguration?
  2. +
  3. Welche Exception fängst du für HTTP 4xx?
  4. +
  5. Wann lohnen sich async Requests?
  6. +
+
+
+ + +
+
+
11
+
+

PHPStan und Static Analysis

+
Bugs vor der Ausführung finden
+
+
+ +
+ Frage zum Einstieg + Ein Tippfehler im Property-Namen, ein null-Wert, der zu einer Method-Aufruf-Exception führt, eine Funktion mit falscher Argument-Anzahl – all das passiert in PHP zur Laufzeit, oft erst in Production. PHPStan findet diese Bugs ohne den Code auszuführen. +
+ +

Setup

+ +
composer require --dev phpstan/phpstan
+ +
# phpstan.neon
+parameters:
+  level: 8           # strikteste Stufe
+  paths:
+    - src
+  excludePaths:
+    - vendor
+ +
# Analyse ausführen
+vendor/bin/phpstan analyse
+ +

Die Level-Stufen

+

PHPStan kennt Stufen von 0 (loose) bis 9 (sehr strikt). Beim Einstieg in ein Projekt fängst du niedrig an und arbeitest dich hoch:

+ + + + + + + + + +
LevelWas geprüft wird
0Existenz von Klassen und Funktionen
2Falsche Typen in Operatoren
5Typen der Funktionsargumente
7Möglicherweise null-Werte
8Calls auf nullable-Typen
9mixed-Werte korrekt einschränken
+ +

Typische Bugs, die PHPStan findet

+ +
// 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
+ +

PHPDoc für Inferenz nutzen

+

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
+ +

Baseline für Legacy-Code

+

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.

+ +
+
i
+
+ Psalm als Alternative + Psalm ist ein vergleichbares Tool von Vimeo. Die Features überschneiden sich zu 95% – PHPStan ist verbreiteter, Psalm geht bei bestimmten Edge Cases tiefer. In Symfony und Laravel-Welt: meist PHPStan. +
+
+ +
+ Recall +
    +
  1. Was tut PHPStan, was PHP-Runtime nicht tut?
  2. +
  3. Welche Level-Stufe sollte langfristig das Ziel sein?
  4. +
  5. Wofür ist eine PHPStan-Baseline?
  6. +
+
+
+ + +
+
+
12
+
+

Testen mit PHPUnit

+
Unit-Tests, Mocks, Data Providers
+
+
+ +
+ Frage zum Einstieg + Tests fühlen sich als zusätzliche Arbeit an – bis du das erste Mal eine Änderung machst und in Sekunden weißt, dass nichts kaputt ist. PHPUnit ist Standard für PHP-Tests. Wie schreibst du erste Tests und welche Patterns lohnen sich? +
+ +

Setup

+ +
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>
+ +

Erste Test-Klasse

+ +
// 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.

+ +

Data Providers

+

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.

+ +

Mocks für Abhängigkeiten

+

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));
+}
+ +

Was zuerst testen?

+

Faustregel für Anfang:

+ +
    +
  • Business-Logik – Berechnungen, Domänen-Regeln, Validierung
  • +
  • Edge Cases – null, leere Strings, Grenzwerte
  • +
  • Bug Fixes – jeder gefixte Bug bekommt einen Test, damit er nicht wiederkehrt
  • +
+ +

Was du nicht testen musst: Getter/Setter, fremde Library-Funktionen, Framework-Code.

+ +
+
+
+ Test-Pyramid: viele Unit, wenige E2E + Schreibe viele Unit-Tests (schnell, isoliert), weniger Integration-Tests (echte DB, API), und wenige End-to-End-Tests (gesamte App). Eine Pyramide, keine Eisbecher-Form. Schnelles Feedback ist wichtiger als 100% Test-Coverage. +
+
+ +
+ Recall +
    +
  1. Was prüft assertSame im Gegensatz zu assertEquals?
  2. +
  3. Wofür sind Data Providers?
  4. +
  5. Warum nutzt du Mocks statt echter Abhängigkeiten in Tests?
  6. +
+
+
+ + +
+

Wie es weitergeht

+ +

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.

+ +

Spaced-Repetition-Plan

+
+
+ Heute +

Guide gelesen, Recall-Fragen aus jedem Kapitel beantwortet.

+
+
+ +3 Tage +

Drei beliebige Kapitel überfliegen, Recall-Fragen aus dem Kopf.

+
+
+ +14 Tage +

Mini-Projekt: REST-API mit PDO, Tests und PHPStan Level 8.

+
+
+ +60 Tage +

Bestehendes Projekt um Tests und Static Analysis erweitern.

+
+
+ +

Was als nächstes lernen

+

Mit diesen Werkzeugen kannst du in jede Spezialisierung tiefer einsteigen:

+ +
    +
  • Frameworks – Symfony oder Laravel mit voller Tiefe (DI, Events, Forms, Security)
  • +
  • Doctrine ORM – Domain-Modelle mit komplexen Beziehungen abbilden
  • +
  • Event-Driven Architecture – Messenger, Queues, Async-Processing
  • +
  • Performance – Profiling mit Blackfire, OpCache, Caching-Strategien
  • +
  • Security – Authentication, OWASP Top 10, Code-Review
  • +
  • Microservices – kleine PHP-Services, API-Gateways, Service-Discovery
  • +
+ +

Begleitmaterial

+

Dieser Guide ist Teil eines Sets:

+
    +
  • PHP OnePager – die visuelle Übersicht
  • +
  • PHP Cheatsheet – die dichte Referenz
  • +
  • PHP Mini-Guide – der 15-Min-Schnelleinstieg
  • +
  • PHP Anfänger-Guide – die Grundlagen
  • +
  • PHP Fortgeschritten-Guide (dieses Dokument) – Patterns und Production
  • +
+
+ + + +``` \ No newline at end of file diff --git a/templates/Referenz/MiniGuide.md b/templates/Referenz/MiniGuide.md new file mode 100644 index 0000000..cdef1f8 --- /dev/null +++ b/templates/Referenz/MiniGuide.md @@ -0,0 +1,383 @@ +``` + + + + +PHP Mini-Guide + + + + + +
+ +
+

PHP in 15 Minuten

+
Dein erstes PHP-Programm – Schritt für Schritt
+
+
+ Mini-Guide + 15 Min · von Null +
+
+ + +
+ Frage zum Einstieg + PHP läuft hinter rund drei Viertel aller Webseiten – inklusive WordPress, Wikipedia und Facebook. Aber wie sieht PHP-Code überhaupt aus, und wie startet man? In 15 Minuten kannst du dein erstes Programm schreiben. +
+ + +

PHP starten

+ +

PHP ist eine Programmiersprache, die auf deinem Computer oder einem Webserver läuft. Im Gegensatz zu HTML oder CSS wird PHP nicht im Browser angezeigt – es erzeugt Ausgaben (zum Beispiel HTML), die dann ausgeliefert werden.

+ +

Um anzufangen, brauchst du PHP auf deinem Computer. Auf Mac: brew install php. Auf Ubuntu: apt install php8.4-cli. Auf Windows: am einfachsten WSL2 mit Ubuntu darin nutzen.

+ +

PHP-Code lebt in Dateien mit der Endung .php. Lege eine Datei hallo.php an mit diesem Inhalt:

+ +
<?php
+
+echo "Hallo Welt!";
+ +

Die erste Zeile <?php sagt PHP: "ab hier kommt mein Code". Das Wort echo heißt: "gib das aus, was danach kommt". Strings (also Text) stehen in Anführungszeichen. Jede Anweisung endet mit einem Semikolon.

+ +

Im Terminal ausführen mit:

+ +
php hallo.php
+ +

Du siehst "Hallo Welt!" – dein erstes PHP-Programm läuft.

+ + +

Variablen

+ +

Eine Variable ist ein benannter Platz, an dem du einen Wert speicherst. In PHP beginnen Variablen immer mit einem Dollar-Zeichen $. Das macht sie im Code sofort erkennbar:

+ +
<?php
+
+$name = "Marek";
+$alter = 34;
+$istAktiv = true;
+
+echo $name;
+ +

Variablen können verschiedene Arten von Werten enthalten. Die drei wichtigsten:

+ + + +

Mit dem Punkt verbindest du Strings:

+ +
$gruss = "Hallo, " . $name . "!";
+echo $gruss;             // Hallo, Marek!
+ + +

Bedingungen

+ +

Programme müssen oft entscheiden: "wenn X zutrifft, mach Y, sonst Z". Dafür gibt es if und else:

+ +
$alter = 17;
+
+if ($alter >= 18) {
+  echo "Du bist erwachsen.";
+} else {
+  echo "Du bist noch minderjährig.";
+}
+ +

Die Klammer hinter if enthält die Bedingung. Die geschweiften Klammern { } umschließen den Code, der ausgeführt wird, wenn die Bedingung wahr ist. else ist der Block, wenn sie falsch ist.

+ +

Wichtige Vergleichs-Operatoren:

+ + + +
+
i
+
+ Ein Gleichheitszeichen reicht nicht + Zum Zuweisen nutzt du = (ein Gleichheitszeichen). Zum Vergleichen brauchst du == (zwei). Das ist eine häufige Verwechslung bei Anfängern. +
+
+ + +

Listen und Schleifen

+ +

Mehrere Werte fasst du in einer Liste zusammen. In PHP heißen Listen array:

+ +
$obst = ["Apfel", "Birne", "Kirsche"];
+ +

Über jede Liste kannst du mit foreach Schritt für Schritt gehen:

+ +
foreach ($obst as $frucht) {
+  echo $frucht . "\n";
+}
+ +

Das gibt "Apfel", "Birne", "Kirsche" untereinander aus. Das \n ist ein Zeilenumbruch. Die Variable $frucht bekommt bei jedem Durchlauf den nächsten Wert aus der Liste.

+ +

Listen müssen nicht aus Strings bestehen. Zahlen gehen genauso:

+ +
$zahlen = [10, 20, 30];
+$summe = 0;
+
+foreach ($zahlen as $zahl) {
+  $summe = $summe + $zahl;
+}
+
+echo $summe;                  // 60
+ + +

Funktionen

+ +

Wenn du denselben Code mehrfach brauchst, packst du ihn in eine Funktion. Du gibst der Funktion einen Namen und kannst sie dann beliebig oft aufrufen:

+ +
function begruessen($name) {
+  echo "Hallo, " . $name . "!\n";
+}
+
+begruessen("Marek");
+begruessen("Anna");
+begruessen("Tom");
+ +

Die Funktion begruessen nimmt einen Parameter entgegen ($name). Beim Aufruf übergibst du den konkreten Wert in den Klammern.

+ +

Funktionen können auch Werte zurückgeben. Dafür gibt es return:

+ +
function addiere($a, $b) {
+  return $a + $b;
+}
+
+$ergebnis = addiere(3, 5);
+echo $ergebnis;                // 8
+ +

Die Funktion macht ihre Berechnung und liefert das Ergebnis zurück. Du fängst es in einer Variable auf und kannst damit weiterarbeiten.

+ +
+
+
+ Übung macht den Meister + Schreibe jetzt selbst ein kleines PHP-Programm. Zum Beispiel: eine Liste deiner Lieblings-Filme, die mit foreach ausgegeben werden. Oder eine Funktion, die das Doppelte einer Zahl zurückgibt. Praktisch ausprobieren ist der schnellste Weg, PHP zu lernen. +
+
+ + + +``` \ No newline at end of file diff --git a/templates/Referenz/OnePager.md b/templates/Referenz/OnePager.md new file mode 100644 index 0000000..bea56eb --- /dev/null +++ b/templates/Referenz/OnePager.md @@ -0,0 +1,526 @@ +``` + + + + +PHP OnePager + + + +
+ + +
+ +
+

PHP – Server-Sprache des Web

+

Dynamische Webseiten · seit 1995 · 75% aller Websites · objektorientiert & funktional

+
+
+
+
+
8.4
+
Aktuelle Version
+
+
+
1995
+
Erstes Release
+
+
+
75%
+
Aller Websites
+
+
+
300k+
+
Composer-Pakete
+
+
+
+
+ +
+ + +
+ + +
+ +
+

+ + + + Kernkonzepte +

+
+
+
1
+
Server-seitig – läuft auf Webservern, generiert HTML pro Request
+
+
+
2
+
Dynamisch typisiert – Typen zur Laufzeit, optional mit Type-Hints prüfbar
+
+
+
3
+
OOP & funktional – Klassen, Interfaces, Traits, Enums, First-Class-Funktionen
+
+
+
4
+
Composer – moderner Paket-Manager für Dependencies und Autoloading
+
+
+
+ +
+

+ + + + Datentypen +

+
+
intGanze Zahlen
+
floatKommazahlen
+
stringText in '' oder ""
+
booltrue / false
+
arrayListe oder Map
+
objectKlassen-Instanz
+
nullkein Wert
+
callableFunktion / Closure
+
+
+ +
+ + +
+ +
+

+ + + + Hello World +

+
<?php
+declare(strict_types=1);
+
+function greet(string $name): string {
+  return "Hallo, $name!";
+}
+
+echo greet('Marek');
+// Hallo, Marek!
+
+ +
+

+ + + + Moderne vs. Legacy +

+
+
+

Modern (8.x)

+
    +
  • strict_types
  • +
  • Readonly Properties
  • +
  • Enums & Match
  • +
  • Named Arguments
  • +
  • Promoted Constructor
  • +
  • Composer + PSR
  • +
+
+
+

Legacy (5.x)

+
    +
  • magic_quotes
  • +
  • register_globals
  • +
  • mysql_* Funktionen
  • +
  • kein Typ-System
  • +
  • include/require Chaos
  • +
  • Spaghetti-Code
  • +
+
+
+
+ +
+ + +
+ +
+

+ + + + + + Ökosystem +

+
+
+
+
+ Laravel + Full-Stack Framework +
+
+
+
+
+ Symfony + Enterprise-Framework +
+
+
+
+
+ Composer + Paket-Manager +
+
+
+
+
+ PHPUnit + Testing-Framework +
+
+
+
+
+ PHPStan + Static Analysis +
+
+
+
+
+ Shopware + E-Commerce-Plattform +
+
+
+
+ +
+

+ + + + Einsatzgebiete +

+
+
+
W
+
Web-Backends – REST/GraphQL APIs, MVC-Apps mit Laravel oder Symfony
+
+
+
C
+
CMS – WordPress, Drupal, TYPO3 für Content-Management
+
+
+
E
+
E-Commerce – Shopware, Magento, WooCommerce
+
+
+
$
+
CLI-Tools – Symfony Console, Laravel Artisan für Automation
+
+
+
+ +
+ +
+ + + + +
+ + +``` \ No newline at end of file