diff --git a/backend/config.py b/backend/config.py index 0fc1c7b..f5af700 100644 --- a/backend/config.py +++ b/backend/config.py @@ -11,9 +11,7 @@ ALLOWED_FORMATS = [ "Cheatsheet", "MiniGuide", "Guide", - "BeginnerGuide", - "IntermediateGuide", - "ExtendedGuide", + "EndGuide", ] FORMAT_META = { @@ -21,9 +19,7 @@ FORMAT_META = { "Cheatsheet": {"pages": "1 Seite", "time": "~10 Min"}, "MiniGuide": {"pages": "3-4 Seiten", "time": "~15 Min"}, "Guide": {"pages": "15-250 Seiten", "time": "variabel"}, - "BeginnerGuide": {"pages": "35-40 Seiten", "time": "~3h"}, - "IntermediateGuide": {"pages": "42-50 Seiten", "time": "~4h"}, - "ExtendedGuide": {"pages": "47-60 Seiten", "time": "~5h"}, + "EndGuide": {"pages": "120-150 Seiten", "time": "~6h"}, } AGENT_TIMEOUT = 3600 diff --git a/backend/models.py b/backend/models.py index c7ebe03..95bfab6 100644 --- a/backend/models.py +++ b/backend/models.py @@ -6,9 +6,7 @@ FormatType = Literal[ "Cheatsheet", "MiniGuide", "Guide", - "BeginnerGuide", - "IntermediateGuide", - "ExtendedGuide", + "EndGuide", ] diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 6621d55..d885d86 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -79,7 +79,7 @@ async function loadGuides() { } } -const FORMAT_ORDER = ['OnePager', 'Cheatsheet', 'MiniGuide', 'Guide', 'BeginnerGuide', 'IntermediateGuide', 'ExtendedGuide'] +const FORMAT_ORDER = ['OnePager', 'Cheatsheet', 'MiniGuide', 'Guide', 'EndGuide'] function autoPreview() { const map = doneByFormat.value diff --git a/frontend/src/components/TopicSidebar.vue b/frontend/src/components/TopicSidebar.vue index 1be688f..f7db6fd 100644 --- a/frontend/src/components/TopicSidebar.vue +++ b/frontend/src/components/TopicSidebar.vue @@ -27,9 +27,7 @@ const formats = [ { key: 'Cheatsheet', label: 'Cheatsheet' }, { key: 'MiniGuide', label: 'MiniGuide' }, { key: 'Guide', label: 'Guide' }, - { key: 'BeginnerGuide', label: 'BeginnerGuide' }, - { key: 'IntermediateGuide', label: 'IntermediateGuide' }, - { key: 'ExtendedGuide', label: 'ExtendedGuide' }, + { key: 'EndGuide', label: 'EndGuide' }, ] const activeGenerations = computed(() => { diff --git a/templates/Format/BeginnerGuide.md b/templates/Format/BeginnerGuide.md deleted file mode 100644 index 6622d78..0000000 --- a/templates/Format/BeginnerGuide.md +++ /dev/null @@ -1,147 +0,0 @@ -``` -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/EndGuide b/templates/Format/EndGuide.md similarity index 100% rename from templates/Format/EndGuide rename to templates/Format/EndGuide.md diff --git a/templates/Format/ExtendedGuide.md b/templates/Format/ExtendedGuide.md deleted file mode 100644 index e226c78..0000000 --- a/templates/Format/ExtendedGuide.md +++ /dev/null @@ -1,171 +0,0 @@ -``` -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 deleted file mode 100644 index f8f28f1..0000000 --- a/templates/Format/IntermediateGuide.md +++ /dev/null @@ -1,166 +0,0 @@ -``` -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/Referenz/BeginnerGuide.md b/templates/Referenz/BeginnerGuide.md deleted file mode 100644 index a4662b0..0000000 --- a/templates/Referenz/BeginnerGuide.md +++ /dev/null @@ -1,1438 +0,0 @@ -``` - - -
- -Von der Installation bis zur ersten Composer-Anwendung – modern und idiomatisch.
-Setup mit Docker, DDEV oder lokal, erstes Script
-$-Variablen, strict_types, Typumwandlung
-Interpolation, Heredoc, String-Funktionen
-Indiziert, assoziativ, gemischt
-if, match, foreach, while
-Parameter, Rückgabewerte, Named Arguments
-Promoted Constructor, readonly, Methoden
-PSR-4, use-Statements, Datei-Organisation
-Exceptions, try/catch, eigene Fehler
-Pakete installieren, kleines CLI-Tool bauen
-php -S für schnelle Tests? Die richtige Wahl spart dir später Stunden.
- Für lokale Entwicklung gibt es drei sinnvolle Optionen, jede mit ihrem Anwendungsfall:
- -| Methode | Wann |
|---|---|
| Lokal (brew, apt) | Kleine Scripts, schneller Test |
| DDEV | Mehrere Projekte mit DB |
| Docker direkt | Custom-Setup, CI/CD |
Für den Einstieg reicht eine lokale Installation. Auf macOS mit Homebrew: brew install php. Auf Ubuntu: apt install php8.4-cli. Auf Windows: am einfachsten WSL2 mit Ubuntu darin.
Lege eine Datei hello.php an mit folgendem Inhalt:
<?php -declare(strict_types=1); - -echo "Hallo Welt!\n"; -echo "PHP-Version: " . PHP_VERSION . "\n";- -
Im Terminal: php hello.php. Du siehst die Ausgabe. Glückwunsch – das ist alles, was PHP zum Start braucht.
Die Zeile declare(strict_types=1) gehört in jede moderne PHP-Datei. Sie aktiviert strenge Typprüfung und verhindert, dass PHP heimlich Strings in Zahlen umwandelt. Verlässlicher Code.
Für Web-Entwicklung braucht PHP normalerweise einen Webserver (Apache, nginx). Für lokales Testen gibt es einen eingebauten Server:
- -# Im Verzeichnis mit deinen .php-Dateien
-php -S localhost:8000
-
- Öffne http://localhost:8000/hello.php im Browser. Der Server ist nicht für Production gedacht, aber für lokale Entwicklung perfekt.
ddev config in deinem Projektordner reicht meist.
- declare(strict_types=1) gut?php -S, wann DDEV?$. Warum eigentlich? Und wie kann eine Variable mal eine Zahl, mal ein String sein, ohne dass es explodiert? Die Antwort zeigt, wie PHPs Typsystem funktioniert.
- Jede Variable in PHP beginnt mit dem Dollar-Zeichen. Das macht sie im Code sofort erkennbar – du musst nie raten, ob ein Name eine Variable oder eine Funktion ist:
- -$name = 'Marek'; -$age = 34; -$height = 1.82; -$isActive = true; -$email = null;- -
PHP ist dynamisch typisiert: derselbe Variable darf erst eine Zahl, dann ein String zugewiesen werden. Das ist flexibel, kann aber zu Bugs führen – deshalb sind Type-Hints in Funktionen so wichtig (Kapitel 6).
- -| Typ | Beispiel | Wofür |
|---|---|---|
int | 42 | Ganze Zahlen |
float | 3.14 | Kommazahlen |
string | 'Hi' | Text |
bool | true | Wahrheitswert |
array | [1, 2] | Liste oder Map |
null | null | Kein Wert |
Den Typ einer Variable findest du mit gettype($x) oder mit Funktionen wie is_int($x), is_string($x).
PHP wandelt Typen oft automatisch um – das ist bequem, aber tückisch. Mit explizitem Cast bist du auf der sicheren Seite:
- -$input = '42'; // string -$number = (int) $input; // 42 als int -$float = (float) '3.14'; // 3.14 -$text = (string) 42; // "42" - -// Boolean-Cast: 0, "", "0", null, [] sind false -if ($value) { // implizit zu bool - echo 'truthy'; -}- -
'0' == false ist true. 'abc' == 0 war in alten PHP-Versionen auch true. Nutze immer === für strikten Vergleich, der auch den Typ prüft.
- == und ===?false?Beide sind Strings, aber doppelte Anführungszeichen interpolieren Variablen, einfache nicht:
- -$name = 'Marek'; - -echo "Hallo $name"; // Hallo Marek -echo 'Hallo $name'; // Hallo $name (wörtlich) - -// Komplexere Ausdrücke: geschweifte Klammern -$user = ['name' => 'Marek']; -echo "Hallo {$user['name']}"; - -// Verkettung mit . -echo 'Hallo ' . $name . '!';- -
Faustregel: Doppelte Anführungszeichen wenn du Variablen einsetzen willst, einfache sonst. Einfache sind minimal schneller, aber das ist heute praktisch egal.
- -Für längeren Text mit Variablen oder ohne gibt es Heredoc und Nowdoc:
- -$name = 'Marek'; - -$mail = <<<TEXT -Hallo $name, - -vielen Dank für deine Bestellung. - -Grüße -TEXT; - -// Nowdoc (kein Interpolation, wörtlich) -$tpl = <<<'TPL' -Verwende {{ name }} für den Namen. -TPL;- -
PHP hat eine riesige Auswahl an String-Funktionen. Diese kennst du nach einer Woche auswendig:
- -| Funktion | Effekt |
|---|---|
strlen($s) | Länge des Strings |
strtoupper / strtolower | Groß-/Kleinschreibung |
trim($s) | Whitespace vorne/hinten entfernen |
str_replace($a, $b, $s) | a durch b ersetzen |
str_contains($s, $needle) | Prüft, ob enthalten |
explode(',', $s) | String zu Array |
implode(',', $arr) | Array zu String |
sprintf('%05d', 42) | Formatiert "00042" |
trim(strtolower($s)) ist die Schreibweise – von innen nach außen lesen.
- "", wann ''?array – das macht vieles flexibel, aber auch leicht verwirrend. Wie funktioniert das?
- Eine Liste ohne explizite Keys – die Indizes werden automatisch 0, 1, 2, ...:
- -$fruits = ['Apfel', 'Birne', 'Kirsche']; - -$fruits[0]; // 'Apfel' -$fruits[2]; // 'Kirsche' -count($fruits); // 3 - -// Hinten anhängen -$fruits[] = 'Banane'; // jetzt 4 Elemente -array_push($fruits, 'Mango'); // alternative Schreibweise- -
Mit String-Keys wird das Array zu einer Map – die häufigste Form in PHP-Code, weil viele APIs (JSON, Datenbank-Zeilen) so aussehen:
- -$user = [ - 'name' => 'Marek', - 'age' => 34, - 'roles' => ['admin', 'editor'], -]; - -$user['name']; // 'Marek' -$user['email'] = 'm@e.de'; // neuer Key - -// Sicher: gibt null wenn nicht da, kein Fehler -$phone = $user['phone'] ?? 'unbekannt';- -
Der Null-Coalescing-Operator ?? ist eine der nützlichsten PHP-Features. Er gibt den linken Wert zurück, wenn er existiert und nicht null ist, sonst den rechten.
foreach ist das Werkzeug der Wahl. Es funktioniert für beide Array-Typen:
// Nur Werte -foreach ($fruits as $fruit) { - echo $fruit; -} - -// Key und Wert -foreach ($user as $key => $value) { - echo "$key: $value\n"; -}- -
| Funktion | Wofür |
|---|---|
array_map($fn, $arr) | Jedes Element transformieren |
array_filter($arr, $fn) | Elemente mit Bedingung behalten |
array_reduce($arr, $fn, $init) | Aggregieren zu einem Wert |
array_keys / array_values | Keys oder Werte extrahieren |
in_array($x, $arr) | Enthält Wert? |
sort($arr) | Sortiert in-place |
?? in PHP?match ein modernes Werkzeug ergänzt, das vieles eleganter macht.
- if ($score >= 90) { - $grade = 'A'; -} elseif ($score >= 75) { - $grade = 'B'; -} else { - $grade = 'C oder schlechter'; -} - -// Ternary für einfache Fälle -$status = $age >= 18 ? 'erwachsen' : 'minderjährig';- -
Seit PHP 8 gibt es match – ähnlich wie switch, aber strikt im Vergleich, ein Ausdruck (also mit Rückgabewert), kein fallthrough:
$status = match($code) { - 200, 201, 204 => 'success', - 301, 302 => 'redirect', - 404 => 'not found', - 500 => 'server error', - default => 'unknown', -};- -
Beachte die Kommas zwischen Werten – sie bedeuten "oder". Und das default ist Pflicht (sonst Exception), das schützt vor vergessenen Fällen.
// for: bekannte Anzahl -for ($i = 0; $i < 10; $i++) { - echo $i; -} - -// foreach: über Arrays -foreach ($items as $item) { - echo $item; -} - -// while: unbestimmte Anzahl -while ($line = fgets($file)) { - processLine($line); -} - -// break und continue -foreach ($items as $item) { - if ($item->skip) continue; - if ($item->stop) break; - process($item); -}- -
match. Strikter Vergleich (===), kein fallthrough-Fehler, Ergebnis als Wert. switch nur noch in Legacy-Code oder wenn du komplexe Blöcke pro Fall brauchst.
- foreach, wann for?match gegenüber switch?break und continue?function add(int $a, int $b): int { - return $a + $b; -} - -function greet(string $name, string $greeting = 'Hallo'): string { - return "$greeting, $name!"; -} - -echo add(3, 4); // 7 -echo greet('Marek'); // Hallo, Marek!- -
Die Syntax: function name(typ $param): rückgabetyp. Beides ist optional, aber heute Standard.
Seit PHP 8 kannst du Argumente beim Aufruf mit Namen übergeben. Praktisch bei Funktionen mit vielen Defaults:
- -function createUser( - string $name, - int $age = 0, - bool $isAdmin = false, - ?string $email = null, -): User { /* ... */ } - -// Positional (alt) -createUser('Marek', 34, false, 'm@e.de'); - -// Named (modern, lesbarer) -createUser( - name: 'Marek', - email: 'm@e.de', - isAdmin: true, -);- -
Mit ? wird ein Typ nullable, mit | sind mehrere Typen erlaubt:
// Darf string oder null sein -function find(int $id): ?User { - return $id > 0 ? new User($id) : null; -} - -// Union: int ODER string -function parse(int|string $input): int { - return (int) $input; -} - -// void: gibt nichts zurück -function log(string $msg): void { - file_put_contents('app.log', $msg); -}- -
use ($var) Variablen importieren – aber Globals mit global $x sind tabu.
- string zurückgibt??string als Typ?Das wichtigste PHP-8-Feature für Klassen: Konstruktor-Parameter werden automatisch zu Properties, wenn du sie mit einem Sichtbarkeits-Modifier markierst:
- -class User { - public function __construct( - public readonly string $name, - public readonly int $age, - private ?string $email = null, - ) {} - - public function isAdult(): bool { - return $this->age >= 18; - } - - public function getEmail(): ?string { - return $this->email; - } -} - -$user = new User(name: 'Marek', age: 34); -echo $user->name; // 'Marek' -echo $user->isAdult() ? 'ja' : 'nein';- -
readonly verhindert nachträgliche Änderungen. public, private, protected steuern die Sichtbarkeit von außen.
interface Greetable { - public function greet(): string; -} - -class User implements Greetable { - public function __construct(public readonly string $name) {} - - public function greet(): string { - return "Hallo, $this->name"; - } -} - -class Admin extends User { - public function greet(): string { - return "Hallo Admin $this->name"; - } -}- -
class Math { - public const PI = 3.14159; - - public static function square(int $x): int { - return $x * $x; - } -} - -echo Math::PI; // 3.14159 -echo Math::square(5); // 25- -
public readonly string $name im Konstruktor?extends und implements?User – eine für Admins, eine für Kunden – kracht es. Wie verhindert PHP solche Konflikte? Namespaces sind die Antwort, und sie hängen eng mit Composers Autoloading zusammen.
- Ein Namespace gibt deinen Klassen einen "Pfad". Konvention: ein Namespace pro Datei, am Anfang deklariert:
- -<?php -declare(strict_types=1); - -namespace App\Domain\User; - -class User { - public function __construct(public readonly string $name) {} -}- -
Die volle Bezeichnung dieser Klasse ist jetzt App\Domain\User\User. Eine andere Klasse mit demselben Namen in einem anderen Namespace kollidiert nicht.
<?php -namespace App\Controller; - -use App\Domain\User\User; -use App\Domain\Order\Order; - -class UserController { - public function show(int $id): User { - return new User('Marek'); // kein voller Pfad nötig - } -}- -
Mit Aliassen kannst du Namen umbenennen, wenn es Konflikte gibt:
- -use App\Domain\User\User as DomainUser; -use App\Http\User as HttpUser;- -
PSR-4 ist die Konvention, wie Namespaces auf Dateipfade abgebildet werden. Composer nutzt das für Autoloading. Eine typische Struktur:
- -my-project/ -├── composer.json -├── vendor/ # installierte Pakete -└── src/ - ├── Controller/ - │ └── UserController.php # App\Controller\UserController - └── Domain/ - └── User/ - └── User.php # App\Domain\User\User- -
In composer.json definierst du das Mapping:
{
- "autoload": {
- "psr-4": {
- "App\\": "src/"
- }
- }
-}
-
- Nach composer dump-autoload findet PHP jede Klasse anhand ihres Namespaces automatisch – kein manuelles require mehr.
User.php enthält class User. Die meisten Frameworks und Tools verlassen sich darauf.
- false-Rückgaben und moderne Exceptions. Wie navigierst du beide?
- Eine Exception ist PHPs moderne Art, Fehler zu signalisieren. Wird sie nicht gefangen, bricht das Script ab. Mit try/catch reagierst du gezielt:
try { - $data = json_decode($json, flags: JSON_THROW_ON_ERROR); - $user = processData($data); -} catch (\JsonException $e) { - echo "Ungültiges JSON: " . $e->getMessage(); -} catch (\Exception $e) { - echo "Anderer Fehler: " . $e->getMessage(); -} finally { - cleanup(); -}- -
Der finally-Block läuft immer – auch wenn die Exception nicht gefangen wurde. Praktisch für Cleanup wie Datei schließen oder Locks freigeben.
PHP kennt eine ganze Hierarchie von Exception-Typen. Du kannst gezielt darauf reagieren:
- -| Exception | Wann |
|---|---|
InvalidArgumentException | Falsche Funktionsargumente |
RuntimeException | Fehler zur Laufzeit |
TypeError | Type-Hint verletzt |
ValueError | Wert außerhalb erlaubtem Bereich |
JsonException | JSON-Parsing fehlgeschlagen |
PDOException | Datenbankfehler |
Für domänenspezifische Fehler definierst du eigene Exception-Klassen. Sie erben von \Exception:
class InsufficientFundsException extends \Exception {} - -function withdraw(int $amount): void { - if ($amount > $this->balance) { - throw new InsufficientFundsException( - "Brauche $amount, habe nur $this->balance" - ); - } - $this->balance -= $amount; -}- -
@functionCall() kannst du Warnings unterdrücken. Tu's nicht – du versteckst nur Probleme, die später schwer zu finden sind. Wenn du weißt, dass etwas schiefgehen kann, behandle es explizit mit try/catch.
- finally-Block?Composer ist der Standard-Paketmanager für PHP. Installation einmal global, dann pro Projekt:
- -# Neues Projekt -mkdir hello-cli -cd hello-cli -composer init # interaktiver Wizard - -# Paket hinzufügen -composer require symfony/console - -# Projektstruktur anlegen -mkdir src -mkdir bin- -
Composer erzeugt composer.json mit deinen Abhängigkeiten und den Ordner vendor/ mit den installierten Paketen.
{
- "name": "marek/hello-cli",
- "type": "project",
- "require": {
- "php": "^8.4",
- "symfony/console": "^7.0"
- },
- "autoload": {
- "psr-4": {
- "App\\": "src/"
- }
- }
-}
-
- Nach Änderungen am Autoload: composer dump-autoload ausführen, damit PHP die neuen Pfade kennt.
Mit Symfony Console schreibst du in wenigen Zeilen ein professionelles CLI-Programm:
- -// src/GreetCommand.php -<?php -namespace App; - -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Output\OutputInterface; - -class GreetCommand extends Command { - protected static $defaultName = 'greet'; - - protected function configure(): void { - $this->addArgument('name', InputArgument::REQUIRED); - } - - protected function execute(InputInterface $in, OutputInterface $out): int { - $name = $in->getArgument('name'); - $out->writeln("<info>Hallo, $name!</info>"); - return Command::SUCCESS; - } -}- -
// bin/app.php -<?php -require __DIR__ . '/../vendor/autoload.php'; - -use Symfony\Component\Console\Application; -use App\GreetCommand; - -$app = new Application(); -$app->add(new GreetCommand()); -$app->run();- -
Im Terminal: php bin/app.php greet Marek. Du siehst "Hallo, Marek!" in Grün. Das ist ein vollständiges CLI-Tool mit Argument-Parsing, Help-Output und Exit-Codes – mit weniger als 50 Zeilen Code.
composer dump-autoload?Du hast PHP jetzt in 10 Kapiteln vom ersten Script bis zur Composer-Anwendung durchlaufen. Aber Wissen verblasst ohne Wiederholung. Plane aktive Wiederholung ein – effektiver als jedes Re-Reading.
- -Guide gelesen, Recall-Fragen aus jedem Kapitel beantwortet.
-OnePager überfliegen, alle Recall-Fragen aus dem Kopf beantworten.
-Mini-Projekt: ein eigenes CLI-Tool mit Composer und Symfony Console.
-Cheatsheet als Referenz – ein neues Paket aus Packagist einbinden.
-Mit diesen Grundlagen kannst du in jede Spezialisierung einsteigen. Empfehlungen je nach Interesse:
- -Dieser Guide ist Teil eines Sets:
-Internals, Fibers, FFI, DDD & Event Sourcing – PHP über das Übliche hinaus.
-Klassen, Methoden, Attribute zur Laufzeit erkunden
-SplStack, SplQueue, SplPriorityQueue, SplObjectStorage
-Eigene Protokolle wie file:// schreiben
-Zirkuläre Referenzen, Memory-Profiling
-Foreign Function Interface für Native-Libraries
-Code-Compilation einmal, nicht pro Request
-Async ohne Threading (PHP 8.1+)
-Long-Running Server, non-blocking I/O
-CPU- und Memory-Hotspots finden
-APCu, Redis, HTTP-Cache, ESI
-Eigener Container in 100 Zeilen
-Lose Kopplung zwischen Bounded Contexts
-Lese- und Schreibmodelle trennen
-Value Objects, Entities, Aggregates
-Ports und Adapter, Domain im Zentrum
-State als Sequenz von Events
-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[]- -
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.
- -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);- -
Reflection ist nicht gratis. Pro ReflectionClass-Instanz parst PHP intern viele Metadaten. Frameworks wie Symfony cachen Reflection-Ergebnisse aggressiv. Faustregel:
| Use Case | OK |
|---|---|
| Boot-Zeit, einmal pro Request | ja |
| Dependency-Injection-Container Build | ja, aber cachen |
| In jedem Method-Call | nein, das ist langsam |
| Hot-Path-Loops | nein, niemals |
$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.
- 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.
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- -
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.
- -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; -}- -
SplQueue gegenüber array_shift?SplObjectStorage?SplFixedArray?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?
- 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:
- -| Wrapper | Wofür |
|---|---|
file:// | Lokales Dateisystem (Default) |
http:// / https:// | HTTP-Requests |
php://memory | In-Memory Stream |
php://stdin / php://stdout | Standard-IO |
php://input | Raw POST-Body |
data:// | Base64 oder URL-encoded Daten |
compress.zlib:// | gzip-Streams |
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!'- -
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) { - // ... -}- -
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 -}- -
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.
- -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.
- -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();- -
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.
- -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- -
WeakReference?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- -
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- -
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(...);- -
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; -}- -
| Use Case | Beispiel-Library |
|---|---|
| Bildverarbeitung | libvips, OpenCV |
| Kompression | libzstd, brotli |
| Crypto | libsodium (auch nativ in PHP) |
| ML-Inferenz | libtorch, onnxruntime |
| Grafik/UI | SDL, GTK |
| System-Calls | libc direkt |
FFI-Calls sind nicht gratis: jeder Aufruf hat Overhead durch Type-Conversion. Faustregel:
- -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.
- FFI::cdef?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.
- -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- -
$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- -
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.
- -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 Case | Speedup mit JIT |
|---|---|
| Web (Database/Network-bound) | ~5-10% |
| CLI-Tools, Compute-bound | ~30-50% |
| Mandelbrot/Mathematik | ~2-3x |
validate_timestamps=1 und 0?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- -
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 (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.
- -| Aspekt | Fibers | Threads (parallel) |
|---|---|---|
| Parallelität | Nein, kooperativ | Ja, true parallel |
| Memory pro Unit | ~8 KB | ~MB |
| Context-Switch | Sehr schnell | OS-Overhead |
| Shared State | Direkt zugreifbar | Locking nötig |
| Use Case | I/O-bound | CPU-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.
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();- -
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.
- -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"; -});- -
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();- -
| Use Case | Wähle |
|---|---|
| Klassische CRUD-App | php-fpm + Symfony/Laravel |
| WebSocket-Server | ReactPHP + Ratchet |
| Pub/Sub-Worker | ReactPHP oder Symfony Messenger |
| Streaming-API | ReactPHP |
| Tausende kleine Microservice-Calls | ReactPHP (Connection-Reuse) |
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.
- 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 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.
- -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);- -
| Pattern | Wie oft | Lösung |
|---|---|---|
| N+1 Query | fast jede App | Eager-Loading |
| JSON-Encoding mehrfach | oft | Cache |
| Twig ohne Cache | häufig | opcache.preload |
| Doctrine: gleiche Entity 100x geladen | häufig | Identity Map |
| Composer Autoload langsam | oft | composer dump-autoload -o |
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.
memory_get_peak_usage()?| Schicht | Latenz | Tech |
|---|---|---|
| OpCache (kompilierter Code) | ~0ms | eingebaut |
| Request-Cache (in-memory) | ~0ms | Arrays |
| APCu (shared memory) | ~0.01ms | APCu Extension |
| Redis (lokal) | ~0.5ms | Redis-Server |
| Redis (Netzwerk) | ~1-5ms | remote Redis |
| Datenbank-Query | ~5-100ms | MySQL/Postgres |
| Externe API | ~50-1000ms | HTTP |
Jede Schicht hinunter ist 10-100x langsamer. Die Kunst: so weit oben wie möglich cachen, ohne stale Daten zu liefern.
- -Die PHP-FIG hat Cache-Interfaces standardisiert. Symfony 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 ist Shared Memory pro PHP-Server-Prozess. Sehr schnell, aber:
- -// 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 = 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- -
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.
- -Das schwerste Problem. Strategien:
- -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);- -
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); - } -}- -
$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- -
Echte Container haben noch viel mehr:
- -OrderService::place() reincoden? Das Event-Dispatcher-Pattern trennt das sauber.
- 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.
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 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.
- -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.
- -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.
- Command Query Responsibility Segregation – ein Pattern, das Lese-Operationen (Queries) und Schreib-Operationen (Commands) auf separate Modelle teilt:
- -// 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, - ) {} -}- -
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); - } -}- -
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 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); - } -}- -
| Use Case | CQRS sinnvoll? |
|---|---|
| Simple CRUD-App | nein, Overkill |
| Komplexe Business-Logik bei Writes | ja |
| Lese-Performance ist kritisch | ja |
| Sehr unterschiedliche Read- und Write-Modelle | ja |
| Mehrere Teams an einem Bounded Context | ja |
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.
- -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.
- -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);- -
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(); - } -}- -
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: 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); - } -}- -
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); - } - } -}- -
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'); - } -}- -
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- -
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 | {...} |
-+----+------------+------------------+----------+
-
- 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.
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); - } - } -}- -
recordEvent und apply?Du hast PHP jetzt von Sprach-Internals über Performance-Optimierung bis zu fortgeschrittenen Architektur-Patterns durchlaufen. Damit kennst du PHP an seinen Grenzen.
- -Guide gelesen, Recall-Fragen aus jedem Kapitel beantwortet.
-Zwei Kapitel auswählen, vertiefen, eigene Implementierung versuchen.
-Spezialthema umsetzen: ReactPHP-Server, eigener DI-Container oder Hexagonal Architecture in echtem Projekt.
-Source-Code von Symfony, Doctrine oder EventSauce lesen – Patterns erkennen.
-Du bist jetzt jenseits des offiziellen PHP-Lernpfads. Empfehlungen für Tiefenexpertise:
- -Dieser Guide schließt das Set ab:
-Von Interfaces und Generators bis Tests – Production-PHP, das hält.
-Kontrakte definieren, Implementierungen trennen
-Code-Reuse jenseits klassischer Vererbung
-Backed Enums, Methods, Pattern-Matching
-Metadaten typensicher statt Annotations
-First-Class Callables, use-Capture
-array_map, array_filter, array_reduce in der Praxis
-Lazy Evaluation mit yield
-Templates ohne native Generics
-Prepared Statements, Transaktionen
-APIs konsumieren, async Requests
-Bugs vor der Ausführung finden
-Unit-Tests, Mocks, Data Providers
-Ein Interface definiert nur, was Methoden tun, nicht wie. Klassen, die das Interface implementieren, müssen alle Methoden bereitstellen:
- -interface LoggerInterface { - public function info(string $message): void; - public function error(string $message, array $context = []): void; -} - -class FileLogger implements LoggerInterface { - public function __construct(private readonly string $path) {} - - public function info(string $message): void { - file_put_contents($this->path, "[INFO] $message\n", FILE_APPEND); - } - - public function error(string $message, array $context = []): void { - file_put_contents($this->path, "[ERROR] $message\n", FILE_APPEND); - } -}- -
Der Gewinn: jede Funktion kann den Typ LoggerInterface erwarten und akzeptiert dadurch jede Implementierung – ohne deren Klasse zu kennen.
class OrderService { - public function __construct( - private readonly LoggerInterface $logger - ) {} - - public function place(Order $order): void { - $this->logger->info("Order $order->id placed"); - // ... - } -} - -// In Tests: Fake-Logger reinreichen -// In Production: FileLogger oder SentryLogger -$service = new OrderService(new FileLogger('/var/log/app.log'));- -
Wenn du gemeinsamen Code teilen willst, aber bestimmte Methoden offen lassen, nimmst du eine abstrakte Klasse. Sie kann Properties, fertige Methoden und abstrakte Methoden mischen:
- -abstract class Notification { - public function __construct(protected readonly string $recipient) {} - - // Fertige Methode - public function send(string $message): void { - $formatted = $this->format($message); - $this->deliver($formatted); - } - - // Muss von Subklassen implementiert werden - abstract protected function format(string $msg): string; - abstract protected function deliver(string $msg): void; -}- -
| Use Case | Wähle |
|---|---|
| Reiner Kontrakt | Interface |
| Mehrere "Verträge" erfüllen | Interface (mehrfach implementierbar) |
| Gemeinsame Code-Basis teilen | Abstrakte Klasse |
| Template Method Pattern | Abstrakte Klasse |
Psr\Log\LoggerInterface, Psr\Container\ContainerInterface. Wenn du deine Klassen daran ausrichtest, passen sie zu jedem Framework.
- User und Order kopieren? Traits lösen das eleganter.
- Ein Trait ist wie eine Klasse, aber du erstellst keine Instanz davon. Stattdessen wirst du in andere Klassen "einkopiert". Mehrere Traits pro Klasse sind erlaubt:
- -trait Timestampable { - private ?\DateTimeImmutable $createdAt = null; - private ?\DateTimeImmutable $updatedAt = null; - - public function touch(): void { - $this->updatedAt = new \DateTimeImmutable(); - $this->createdAt ??= $this->updatedAt; - } - - public function getCreatedAt(): ?\DateTimeImmutable { - return $this->createdAt; - } -} - -class Article { - use Timestampable; - - public function __construct(public string $title) { - $this->touch(); - } -} - -class Comment { - use Timestampable; // gleiche Methoden, ohne Doppelung -}- -
trait SoftDeletable { - private ?\DateTimeImmutable $deletedAt = null; - - public function delete(): void { - $this->deletedAt = new \DateTimeImmutable(); - } - - public function isDeleted(): bool { - return $this->deletedAt !== null; - } -} - -class Article { - use Timestampable, SoftDeletable; // beide Sets von Methoden -}- -
Wenn zwei Traits Methoden mit gleichem Namen haben, gibt es einen Konflikt. PHP zwingt dich, explizit zu wählen:
- -trait A { public function hello(): string { return 'A'; } } -trait B { public function hello(): string { return 'B'; } } - -class X { - use A, B { - A::hello insteadof B; // nimm A's hello, ignoriere B's - B::hello as helloFromB; // B's hello als 'helloFromB' verfügbar - } -}- -
Traits sind verlockend, aber sie binden Code statisch in Klassen ein. Eine moderne Alternative ist Komposition: ein Service-Objekt als Property, das die Logik kapselt. Faustregel:
- -enum Status { - case Draft; - case Published; - case Archived; -} - -$status = Status::Draft; - -if ($status === Status::Draft) { - echo 'noch nicht veröffentlicht'; -}- -
Die Cases sind Singletons – jede Verwendung von Status::Draft ist dasselbe Objekt. Vergleich mit === funktioniert wie erwartet.
Für Serialisierung – etwa in Datenbank oder JSON – brauchst du Backed Enums mit konkretem String oder Int als Backing-Value:
- -enum Status: string { - case Draft = 'draft'; - case Published = 'published'; - case Archived = 'archived'; -} - -// Wert auslesen -Status::Draft->value; // 'draft' - -// Aus Wert zurückbauen -Status::from('draft'); // Status::Draft -Status::tryFrom('unknown'); // null (statt Exception)- -
Enums dürfen Methoden haben. Damit packst du Verhalten direkt zum Wert, statt es in Helper-Funktionen zu verstecken:
- -enum Status: string { - case Draft = 'draft'; - case Published = 'published'; - case Archived = 'archived'; - - public function label(): string { - return match($this) { - self::Draft => 'Entwurf', - self::Published => 'Veröffentlicht', - self::Archived => 'Archiv', - }; - } - - public function isPublic(): bool { - return $this === self::Published; - } -} - -Status::Draft->label(); // 'Entwurf' -Status::Draft->isPublic(); // false- -
foreach (Status::cases() as $status) { - echo $status->label() . "\n"; -} -// Entwurf -// Veröffentlicht -// Archiv- -
Praktisch für Dropdowns, Validierung oder Migration zwischen Status.
- -const STATUS_DRAFT = 'draft' schreibst, ist es fast immer besser, ein Enum zu nehmen. Typ-Sicherheit, Methoden direkt am Wert, IDE-Vervollständigung, exhaustive match-Checks.
- Status::cases() zurück?/** @Route("/users") */. Kommentare, die geparst werden – brüchig, ohne Typ-Check. PHP 8 brachte native Attribute. Wie schreibst und nutzt du sie?
- Ein Attribut ist eine Klasse, die mit #[Attribute] markiert ist. Sie kann Konstruktor-Parameter haben wie jede andere Klasse:
#[\Attribute(\Attribute::TARGET_METHOD)] -class Route { - public function __construct( - public readonly string $path, - public readonly string $method = 'GET', - ) {} -}- -
Das TARGET_METHOD sagt: dieses Attribut darf nur auf Methoden. Andere Optionen: TARGET_CLASS, TARGET_PROPERTY, TARGET_PARAMETER.
class UserController { - #[Route('/users')] - public function list(): Response { /* ... */ } - - #[Route('/users/{id}', method: 'GET')] - public function show(int $id): Response { /* ... */ } - - #[Route('/users', method: 'POST')] - public function create(): Response { /* ... */ } -}- -
Mit der Reflection-API liest du Attribute aus Klassen, Methoden oder Properties aus – das ist die Grundlage für Frameworks wie Symfony, Doctrine oder ORM-Libraries:
- -$reflection = new \ReflectionClass(UserController::class); - -foreach ($reflection->getMethods() as $method) { - foreach ($method->getAttributes(Route::class) as $attr) { - $route = $attr->newInstance(); // Route-Instanz - echo "$route->method $route->path → " . $method->name . "\n"; - } -} -// Output: -// GET /users → list -// GET /users/{id} → show -// POST /users → create- -
| Framework | Beispiel-Attribute |
|---|---|
| Symfony | #[Route], #[AsCommand] |
| Doctrine | #[ORM\Entity], #[ORM\Column] |
| PHPUnit | #[DataProvider], #[Test] |
| Validator | #[Assert\NotBlank] |
/** @Route("/users") */ funktioniert noch, aber das Doctrine/Symfony-Ökosystem migriert auf native Attribute. Bei Neuentwicklungen: immer Attribute, nie Annotations.
- Eine Closure ist eine Funktion ohne Namen, die in einer Variable lebt. Du übergibst sie als Argument oder gibst sie zurück:
- -$double = function(int $x): int { - return $x * 2; -}; - -echo $double(5); // 10 - -// Als Argument an array_map -$numbers = [1, 2, 3]; -$doubled = array_map($double, $numbers); // [2, 4, 6]- -
Im Gegensatz zu normalen Funktionen haben Closures Zugriff auf den umgebenden Scope – aber nur, wenn du es explizit mit use erlaubst:
$prefix = 'Mr. '; - -$greet = function(string $name) use ($prefix): string { - return $prefix . $name; -}; - -echo $greet('Marek'); // 'Mr. Marek' - -// use kopiert standardmäßig: spätere Änderung wirkt nicht -$prefix = 'Dr. '; -echo $greet('Marek'); // immer noch 'Mr. Marek' - -// use mit Referenz: spätere Änderung wirkt -$counter = 0; -$increment = function() use (&$counter): void { - $counter++; -}; -$increment(); -$increment(); -echo $counter; // 2- -
Für einzeilige Closures gibt es seit PHP 7.4 die kürzere Arrow Function Syntax. Sie capturet Variablen automatisch (kein use nötig):
$multiplier = 3; -$multiply = fn(int $x): int => $x * $multiplier; - -echo $multiply(5); // 15 - -// Praktisch in array_map / array_filter -$names = ['marek', 'anna', 'tom']; -$upper = array_map(fn($n) => strtoupper($n), $names);- -
Seit PHP 8.1 kannst du jede Funktion oder Methode als Closure referenzieren – ohne sie aufzurufen:
- -// Globale Funktion -$upper = strtoupper(...); -$upper('hallo'); // 'HALLO' - -// Methode -$user = new User('Marek'); -$greet = $user->greet(...); -$greet(); // 'Hallo, Marek' - -// Static Methode -$square = Math::square(...); -$square(5); // 25 - -// Direkt an array_map übergeben -$upper = array_map(strtoupper(...), $names);- -
fn) wenn der Body ein einzelner Ausdruck ist. Volle Closure (function) wenn du mehrere Statements brauchst oder explizites Variablen-Capturing willst.
- use ($x) und use (&$x)?strtoupper(...)?foreach, Zwischenvariable, Bedingung – fünf Zeilen. Mit Higher-Order Functions wird daraus ein lesbarer Einzeiler. Wie?
- array_map wendet eine Funktion auf jedes Element an und gibt ein neues Array zurück:
$prices = [10, 25, 99]; - -$withTax = array_map(fn($p) => $p * 1.19, $prices); -// [11.9, 29.75, 117.81] - -// Objekte transformieren -$users = [new User('Marek'), new User('Anna')]; -$names = array_map(fn(User $u) => $u->name, $users); -// ['Marek', 'Anna']- -
array_filter behält nur Elemente, für die die Funktion true zurückgibt:
$numbers = [1, 2, 3, 4, 5, 6]; - -$even = array_filter($numbers, fn($n) => $n % 2 === 0); -// [2, 4, 6] - -// Achtung: Keys werden beibehalten! -// $even ist [1 => 2, 3 => 4, 5 => 6] - -// Re-indexen mit array_values -$even = array_values(array_filter($numbers, fn($n) => $n % 2 === 0)); -// [2, 4, 6] mit Keys 0, 1, 2- -
array_reduce reduziert ein Array zu einem einzelnen Wert. Der Akkumulator startet mit einem Initialwert und wird mit jedem Element aktualisiert:
$prices = [10, 25, 99]; - -$total = array_reduce( - $prices, - fn(float $sum, int $price) => $sum + $price, - 0.0 // Initialwert -); -// 134.0- -
Das volle Potenzial entfaltet sich, wenn du mehrere Funktionen kombinierst – z.B. Beträge der bezahlten Bestellungen aufsummieren:
- -$paidTotal = array_reduce( - array_filter($orders, fn($o) => $o->status === Status::Paid), - fn($sum, $o) => $sum + $o->amount, - 0.0 -); - -// Mit Zwischen-Variablen leichter zu lesen -$paidOrders = array_filter($orders, fn($o) => $o->isPaid()); -$amounts = array_map(fn($o) => $o->amount, $paidOrders); -$total = array_sum($amounts);- -
Letzteres ist oft lesbarer als verschachtelte Aufrufe – nicht immer ist die kompakteste Version die beste.
- -collect($users)->filter(...)->map(...)->sum(). Lesbarer als verschachtelte PHP-Standardfunktionen.
- array_map vs. array_filter?array_filter überrascht oft?Ein Generator ist eine Funktion, die Werte mit yield ausgibt statt mit return. Sie pausiert zwischen den Werten – Speicher bleibt frei:
function readLines(string $path): \Generator { - $handle = fopen($path, 'r'); - while (($line = fgets($handle)) !== false) { - yield rtrim($line); - } - fclose($handle); -} - -// Aufruf liefert sofort einen Generator – Datei wird noch nicht gelesen -$lines = readLines('/var/log/huge.log'); - -// Erst die Iteration liest tatsächlich -foreach ($lines as $line) { - if (str_contains($line, 'ERROR')) { - echo $line; - } -}- -
Der Speicher-Vorteil: zu jedem Zeitpunkt ist nur eine Zeile geladen, egal wie groß die Datei ist.
- -function readCsv(string $path): \Generator { - $handle = fopen($path, 'r'); - $headers = fgetcsv($handle); - $rowNumber = 0; - - while (($row = fgetcsv($handle)) !== false) { - yield $rowNumber++ => array_combine($headers, $row); - } - fclose($handle); -} - -foreach (readCsv('users.csv') as $num => $row) { - echo "Zeile $num: " . $row['name'] . "\n"; -}- -
Da Generators lazy sind, kannst du theoretisch unendliche Sequenzen erzeugen – solange du sie nicht komplett auswertest:
- -function fibonacci(): \Generator { - $a = 0; $b = 1; - while (true) { - yield $a; - [$a, $b] = [$b, $a + $b]; - } -} - -// Erste 10 Fibonacci-Zahlen -$count = 0; -foreach (fibonacci() as $n) { - echo $n . ' '; - if (++$count >= 10) break; -} -// 0 1 1 2 3 5 8 13 21 34- -
Mit yield from delegierst du an einen anderen Generator – nützlich, um Logik zu kapseln:
function range1to3(): \Generator { - yield 1; - yield 2; - yield 3; -} - -function range1to6(): \Generator { - yield from range1to3(); // gibt 1, 2, 3 - yield 4; - yield 5; - yield 6; -}- -
foreach durchläufst, ist der zweite Durchlauf leer. Brauchst du Mehrfach-Zugriff, konvertiere mit iterator_to_array($gen) zu einem Array.
- foreach durchläufst?yield from da?mixed. Die Lösung: PHPDoc-Generics mit PHPStan oder Psalm.
- class Collection { - private array $items = []; - - public function add(mixed $item): void { - $this->items[] = $item; - } - - public function first(): mixed { - return $this->items[0] ?? null; - } -} - -$users = new Collection(); -$users->add(new User('Marek')); - -$user = $users->first(); // IDE: mixed, keine Autocompletion -$user->name; // keine Hilfe von IDE oder PHPStan- -
Mit PHPDoc-Annotationen kannst du Templates definieren. PHPStan und Psalm verstehen sie und prüfen Typen statisch:
- -/** - * @template T - */ -class Collection { - /** @var array<T> */ - private array $items = []; - - /** - * @param T $item - */ - public function add(mixed $item): void { - $this->items[] = $item; - } - - /** - * @return T|null - */ - public function first(): mixed { - return $this->items[0] ?? null; - } -}- -
Bei der Verwendung gibst du den konkreten Typ als PHPDoc-Annotation an. PHPStan ersetzt dann T intern:
/** @var Collection<User> $users */ -$users = new Collection(); -$users->add(new User('Marek')); - -$user = $users->first(); // PHPStan weiß: User|null -$user->name; // OK, IDE und PHPStan kennen User - -$users->add('string'); // PHPStan-Fehler: erwartet User, nicht string- -
/** - * @template T of \Throwable - */ -class ErrorCollection { - /** @var array<T> */ - private array $errors = []; - - /** - * @param T $error - */ - public function add(\Throwable $error): void { - $this->errors[] = $error; - } -} - -// Erlaubt nur Throwables und Subklassen -/** @var ErrorCollection<\RuntimeException> $errors */ -$errors = new ErrorCollection();- -
PHP-Arrays sind oft "halb-Objekte" mit festen Keys. PHPStan kennt Array-Shapes:
- -/** - * @return array{name: string, age: int, email: string|null} - */ -function getUser(int $id): array { - return ['name' => 'Marek', 'age' => 34, 'email' => null]; -} - -$user = getUser(42); -echo $user['name']; // PHPStan weiß: string -echo $user['phone']; // PHPStan-Fehler: Key existiert nicht- -
array{name: string, age: int}?PDO unterstützt MySQL, PostgreSQL, SQLite und mehr über dieselbe API. Der DSN-String unterscheidet sich, der Rest ist gleich:
- -$dsn = 'mysql:host=localhost;dbname=myapp;charset=utf8mb4'; -$pdo = new \PDO($dsn, 'username', 'password', [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_EMULATE_PREPARES => false, -]);- -
Die drei Options sind Pflicht in jedem Projekt: Exceptions bei Fehlern, assoziative Arrays beim Fetch, echte Prepared Statements (statt emulierter Client-Side).
- -Niemals User-Input direkt in SQL-Strings konkatenieren. Stattdessen: Placeholder mit ? oder Named Parameters:
// Falsch: SQL-Injection-Lücke -$id = $_GET['id']; -$result = $pdo->query("SELECT * FROM users WHERE id = $id"); -// $_GET['id'] = "1; DROP TABLE users" → katastrophal - -// Richtig: Prepared Statement mit Positional Parameters -$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?'); -$stmt->execute([$id]); -$user = $stmt->fetch(); - -// Oder mit Named Parameters -$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id AND active = :active'); -$stmt->execute([':id' => $id, ':active' => true]);- -
// Einzelne Zeile -$user = $stmt->fetch(); // assoc array oder false - -// Alle Zeilen -$users = $stmt->fetchAll(); // Array von Zeilen - -// Iterativ für große Result-Sets -while ($row = $stmt->fetch()) { - processRow($row); -} - -// Direkt in Objekte mappen (PDO::FETCH_CLASS) -$stmt->setFetchMode(PDO::FETCH_CLASS, User::class); -$users = $stmt->fetchAll();- -
Wenn mehrere Operationen atomar sein müssen (alle oder keine), nutzt du Transaktionen:
- -$pdo->beginTransaction(); - -try { - $pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?') - ->execute([100, $fromId]); - - $pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?') - ->execute([100, $toId]); - - $pdo->commit(); -} catch (\PDOException $e) { - $pdo->rollBack(); - throw $e; -}- -
mysql_*-Funktionen sind seit PHP 7 entfernt. mysqli existiert noch, aber nur mit Prepared Statements nutzen. Standard heute: PDO für direkten DB-Zugriff, Doctrine DBAL für Query-Builder, Doctrine ORM für komplexe Domänen.
- file_get_contents oder cURL geht das, aber wird schnell hässlich. Guzzle ist Standard-Library für HTTP in PHP.
- composer require guzzlehttp/guzzle- -
use GuzzleHttp\Client; - -$client = new Client([ - 'base_uri' => 'https://api.example.com', - 'timeout' => 5, - 'headers' => [ - 'Accept' => 'application/json', - 'Authorization' => 'Bearer ' . $token, - ], -]);- -
// GET -$response = $client->get('/users/42'); -$data = json_decode($response->getBody()->getContents(), true); - -// GET mit Query-Parametern -$response = $client->get('/users', [ - 'query' => ['page' => 2, 'limit' => 50], -]); - -// POST mit JSON-Body -$response = $client->post('/users', [ - 'json' => ['name' => 'Marek', 'email' => 'm@e.de'], -]); - -// Status und Header -$response->getStatusCode(); // 201 -$response->getHeader('Content-Type');- -
Guzzle wirft bei HTTP-Status 4xx/5xx Exceptions. Die hierarchische Struktur erlaubt gezieltes Catchen:
- -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\ServerException; -use GuzzleHttp\Exception\ConnectException; - -try { - $response = $client->get('/users/42'); -} catch (ClientException $e) { - // 4xx Fehler – z.B. 404 Not Found, 401 Unauthorized - $status = $e->getResponse()->getStatusCode(); -} catch (ServerException $e) { - // 5xx Fehler – z.B. 500, 503 -} catch (ConnectException $e) { - // Netzwerk-Fehler, Timeout, DNS -}- -
Mehrere Requests parallel statt seriell? Guzzle unterstützt Promises:
- -use GuzzleHttp\Promise; - -$promises = [ - 'users' => $client->getAsync('/users'), - 'orders' => $client->getAsync('/orders'), - 'stats' => $client->getAsync('/stats'), -]; - -// Warte bis alle fertig sind -$results = Promise\Utils::settle($promises)->wait(); - -foreach ($results as $key => $result) { - if ($result['state'] === 'fulfilled') { - $body = (string) $result['value']->getBody(); - // ... - } -}- -
Drei API-Calls in der Zeit eines einzelnen – wenn die API es zulässt, ist das ein riesiger Performance-Gewinn.
- -Psr\Http\Client\ClientInterface. Guzzle implementiert das, aber auch andere Clients (z.B. Symfony HttpClient). Du tauschst sie dann ohne Code-Änderung aus.
- base_uri in der Client-Konfiguration?composer require --dev phpstan/phpstan- -
# phpstan.neon -parameters: - level: 8 # strikteste Stufe - paths: - - src - excludePaths: - - vendor- -
# Analyse ausführen
-vendor/bin/phpstan analyse
-
- PHPStan kennt Stufen von 0 (loose) bis 9 (sehr strikt). Beim Einstieg in ein Projekt fängst du niedrig an und arbeitest dich hoch:
- -| Level | Was geprüft wird |
|---|---|
| 0 | Existenz von Klassen und Funktionen |
| 2 | Falsche Typen in Operatoren |
| 5 | Typen der Funktionsargumente |
| 7 | Möglicherweise null-Werte |
| 8 | Calls auf nullable-Typen |
| 9 | mixed-Werte korrekt einschränken |
// 1. Tippfehler in Property -class User { - public function __construct(public readonly string $name) {} -} - -$user = new User('Marek'); -echo $user->naem; // PHPStan: Access to undefined property - -// 2. null-Aufruf -function findUser(int $id): ?User { /* ... */ } - -$user = findUser(42); -echo $user->name; // PHPStan: $user might be null - -// 3. Falsche Argument-Typen -function double(int $x): int { return $x * 2; } - -double('5'); // PHPStan: expects int, gets string- -
Je präziser deine PHPDoc-Annotationen, desto mehr findet PHPStan. Besonders mächtig bei Arrays:
- -/** - * @param array<User> $users - * @return array<string> - */ -function getNames(array $users): array { - return array_map(fn($u) => $u->name, $users); -} - -// Ohne PHPDoc würde PHPStan nicht wissen, was $users enthält -// Mit PHPDoc kann es fehlerhafte Aufrufe finden: -getNames([new User('M'), 'string']); // PHPStan: erwartet User, nicht string- -
Wenn du PHPStan zu einem bestehenden Projekt hinzufügst, sind oft hunderte Fehler in altem Code. Eine Baseline ignoriert bestehende Fehler – neue müssen gefixt werden:
- -vendor/bin/phpstan analyse --generate-baseline- -
Das erzeugt phpstan-baseline.neon. Bestehende Fehler werden eingefroren, neuer Code wird streng geprüft. Über die Zeit räumst du die Baseline ab.
composer require --dev phpunit/phpunit- -
// phpunit.xml -<?xml version="1.0"?> -<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - <testsuites> - <testsuite name="default"> - <directory>tests</directory> - </testsuite> - </testsuites> -</phpunit>- -
// src/Calculator.php -namespace App; - -class Calculator { - public function add(int $a, int $b): int { - return $a + $b; - } - - public function divide(int $a, int $b): float { - if ($b === 0) throw new \DivisionByZeroError(); - return $a / $b; - } -} - -// tests/CalculatorTest.php -namespace App\Tests; - -use App\Calculator; -use PHPUnit\Framework\TestCase; - -class CalculatorTest extends TestCase { - public function testAdd(): void { - $calc = new Calculator(); - $this->assertSame(5, $calc->add(2, 3)); - } - - public function testDivideByZero(): void { - $this->expectException(\DivisionByZeroError::class); - (new Calculator())->divide(10, 0); - } -}- -
Ausführen: vendor/bin/phpunit. Du siehst grüne Punkte für jeden bestandenen Test.
Wenn du eine Methode mit vielen Input-Output-Paaren testen willst, ist ein Data Provider effizienter als einzelne Testmethoden:
- -use PHPUnit\Framework\Attributes\DataProvider; - -class CalculatorTest extends TestCase { - #[DataProvider('addCases')] - public function testAdd(int $a, int $b, int $expected): void { - $this->assertSame($expected, (new Calculator())->add($a, $b)); - } - - public static function addCases(): array { - return [ - 'positives' => [2, 3, 5], - 'negatives' => [-1, -2, -3], - 'mixed' => [-5, 10, 5], - 'zero' => [0, 0, 0], - ]; - } -}- -
PHPUnit ruft testAdd einmal pro Datensatz auf. Bei Fehler zeigt es den Key (z.B. 'negatives'), du findest den problematischen Fall sofort.
Wenn deine Klasse externe Services nutzt (DB, API), willst du im Test nicht wirklich zugreifen. Mit Mocks simulierst du das Verhalten:
- -public function testOrderServiceLogsCreation(): void { - // Mock erstellen - $logger = $this->createMock(LoggerInterface::class); - - // Erwarte: info wird genau 1x mit 'Order created' aufgerufen - $logger->expects($this->once()) - ->method('info') - ->with($this->stringContains('Order created')); - - $service = new OrderService($logger); - $service->create(new Order(42)); -}- -
Faustregel für Anfang:
- -Was du nicht testen musst: Getter/Setter, fremde Library-Funktionen, Framework-Code.
- -assertSame im Gegensatz zu assertEquals?Du hast PHP jetzt von OOP-Patterns über funktionale Werkzeuge bis zu Production-Tools durchlaufen. Damit baust du wartbare, getestete Applikationen. Aber Praxis schlägt Theorie – setze diese Patterns in echtem Code ein.
- -Guide gelesen, Recall-Fragen aus jedem Kapitel beantwortet.
-Drei beliebige Kapitel überfliegen, Recall-Fragen aus dem Kopf.
-Mini-Projekt: REST-API mit PDO, Tests und PHPStan Level 8.
-Bestehendes Projekt um Tests und Static Analysis erweitern.
-Mit diesen Werkzeugen kannst du in jede Spezialisierung tiefer einsteigen:
- -Dieser Guide ist Teil eines Sets:
-