Files
guides/templates/Referenz/ExtendedGuide.md
2026-05-25 19:33:48 +02:00

2542 lines
122 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
```
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>PHP Extended-Guide</title>
<style>
@page {
size: A4;
margin: 22mm 20mm 20mm 20mm;
@bottom-center {
content: counter(page) " / " counter(pages);
font-family: -apple-system, "Segoe UI", sans-serif;
font-size: 8pt;
color: #888;
}
@bottom-right {
content: "PHP Extended-Guide";
font-family: -apple-system, "Segoe UI", sans-serif;
font-size: 8pt;
color: #888;
}
}
@page :first {
margin: 0;
@bottom-center { content: none; }
@bottom-right { content: none; }
}
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--php: #777BB4;
--php-dark: #4F5B93;
--php-darker: #2C3E66;
--ink: #1a1a1a;
--muted: #5a6470;
--line: #d8dde3;
--bg-soft: #f5f5fb;
--code-bg: #1e2a3a;
--code-fg: #e6e6e6;
--plus: #2c8a3e;
--minus: #c0392b;
}
html, body {
font-family: Charter, "Source Serif Pro", Georgia, serif;
color: var(--ink);
font-size: 10.5pt;
line-height: 1.55;
}
/* ===== COVER ===== */
.cover {
width: 210mm;
height: 297mm;
padding: 35mm 25mm;
background: linear-gradient(135deg, var(--php-dark) 0%, var(--php-darker) 100%);
color: white;
display: flex;
flex-direction: column;
justify-content: space-between;
page-break-after: always;
}
.cover-top { display: flex; align-items: center; gap: 8mm; }
.cover-logo {
width: 28mm; height: 28mm;
background: linear-gradient(135deg, var(--php) 0%, white 100%);
border-radius: 7mm;
display: flex; align-items: center; justify-content: center;
font-family: -apple-system, sans-serif;
font-size: 22pt; font-weight: 800;
color: var(--php-dark);
}
.cover-meta {
font-family: -apple-system, sans-serif;
font-size: 9pt;
text-transform: uppercase;
letter-spacing: 2pt;
opacity: 0.8;
}
.cover-main h1 {
font-family: -apple-system, sans-serif;
font-size: 56pt;
font-weight: 800;
letter-spacing: -2pt;
line-height: 0.95;
margin-bottom: 8mm;
}
.cover-main h1 .accent { color: var(--php); }
.cover-main .subtitle {
font-size: 16pt;
font-weight: 400;
line-height: 1.3;
opacity: 0.9;
font-family: -apple-system, sans-serif;
max-width: 130mm;
}
.cover-bottom {
display: grid;
grid-template-columns: 1fr auto;
gap: 10mm;
align-items: end;
font-family: -apple-system, sans-serif;
}
.cover-promise {
font-size: 10pt;
line-height: 1.5;
opacity: 0.85;
max-width: 110mm;
padding-top: 4mm;
border-top: 1pt solid rgba(255,255,255,0.3);
}
.cover-promise b { color: var(--php); text-transform: uppercase; letter-spacing: 1pt; font-size: 8pt; display: block; margin-bottom: 2mm; }
.cover-tag {
background: var(--php);
color: white;
padding: 3mm 6mm;
border-radius: 2mm;
font-weight: 800;
font-size: 11pt;
}
/* ===== TOC ===== */
.toc { page-break-after: always; }
.toc h2 {
font-family: -apple-system, sans-serif;
font-size: 24pt;
font-weight: 800;
color: var(--php-dark);
margin-bottom: 6mm;
border-bottom: 2pt solid var(--php);
padding-bottom: 3mm;
}
.toc-section {
font-family: -apple-system, sans-serif;
font-size: 9pt;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1pt;
color: var(--muted);
margin: 6mm 0 2mm 0;
}
.toc-item {
display: grid;
grid-template-columns: 8mm 1fr auto;
gap: 4mm;
padding: 2.5mm 0;
border-bottom: 0.3pt solid var(--line);
align-items: baseline;
}
.toc-num {
font-family: -apple-system, sans-serif;
font-size: 14pt;
font-weight: 800;
color: var(--php);
line-height: 1;
}
.toc-text { font-family: -apple-system, sans-serif; }
.toc-text h3 {
font-size: 10.5pt;
font-weight: 700;
color: var(--ink);
margin-bottom: 0.5mm;
}
.toc-text p {
font-size: 8.5pt;
color: var(--muted);
margin: 0;
}
.toc-time {
font-family: -apple-system, sans-serif;
font-size: 7.5pt;
color: var(--muted);
background: var(--bg-soft);
padding: 0.8mm 2.5mm;
border-radius: 1.5mm;
white-space: nowrap;
}
/* ===== CHAPTER ===== */
.chapter { page-break-before: always; }
.chapter-head {
display: grid;
grid-template-columns: auto 1fr;
gap: 6mm;
align-items: center;
border-bottom: 2pt solid var(--ink);
padding-bottom: 4mm;
margin-bottom: 6mm;
}
.chapter-num {
font-family: -apple-system, sans-serif;
font-size: 42pt;
font-weight: 800;
color: var(--php);
line-height: 0.9;
width: 22mm;
text-align: center;
}
.chapter-title h1 {
font-family: -apple-system, sans-serif;
font-size: 22pt;
font-weight: 800;
color: var(--php-dark);
letter-spacing: -0.5pt;
line-height: 1.1;
}
.chapter-title .subtitle {
font-family: -apple-system, sans-serif;
font-size: 11pt;
color: var(--muted);
margin-top: 1.5mm;
}
/* ===== GAP ===== */
.gap {
background: var(--bg-soft);
border-left: 3pt solid var(--php);
padding: 4mm 5mm;
margin: 4mm 0 6mm 0;
font-style: italic;
font-size: 10.5pt;
}
.gap > b:first-child {
font-style: normal;
color: var(--php-dark);
font-family: -apple-system, sans-serif;
text-transform: uppercase;
font-size: 8pt;
letter-spacing: 1pt;
display: block;
margin-bottom: 1.5mm;
}
/* ===== SECTIONS ===== */
.chapter h2 {
font-family: -apple-system, sans-serif;
font-size: 14pt;
font-weight: 700;
color: var(--php-dark);
margin: 7mm 0 3mm 0;
page-break-after: avoid;
}
.chapter h3 {
font-family: -apple-system, sans-serif;
font-size: 11pt;
font-weight: 700;
color: var(--ink);
margin: 5mm 0 2mm 0;
page-break-after: avoid;
}
.chapter p {
margin-bottom: 3mm;
text-align: justify;
hyphens: auto;
}
.chapter p b { color: var(--php-dark); }
.chapter ul, .chapter ol { margin: 2mm 0 4mm 6mm; }
.chapter li { margin-bottom: 1.5mm; }
/* ===== CODE ===== */
.chapter pre {
background: var(--code-bg);
color: var(--code-fg);
font-family: "SF Mono", Consolas, monospace;
font-size: 8.5pt;
line-height: 1.5;
padding: 3mm 4mm;
border-radius: 2mm;
margin: 3mm 0 4mm 0;
white-space: pre;
overflow: hidden;
page-break-inside: avoid;
}
.c { color: #6b8aae; font-style: italic; }
.k { color: #ff79c6; }
.s { color: #f1c40f; }
.f { color: #50fa7b; }
.t { color: #8be9fd; }
.v { color: #ffb86c; }
code.inline {
font-family: "SF Mono", Consolas, monospace;
font-size: 9pt;
background: var(--bg-soft);
padding: 0.3mm 1.5mm;
border-radius: 1mm;
color: var(--php-dark);
}
/* ===== CALLOUTS ===== */
.callout {
border-radius: 2mm;
padding: 3mm 4mm;
margin: 4mm 0;
font-size: 10pt;
page-break-inside: avoid;
display: grid;
grid-template-columns: 6mm 1fr;
gap: 3mm;
align-items: start;
}
.callout-icon {
font-family: -apple-system, sans-serif;
font-weight: 800;
font-size: 14pt;
line-height: 1;
text-align: center;
}
.callout-body > b:first-child {
font-family: -apple-system, sans-serif;
text-transform: uppercase;
font-size: 8pt;
letter-spacing: 1pt;
display: block;
margin-bottom: 1.5mm;
}
.callout.tip { background: #e8f4ea; border-left: 3pt solid var(--plus); }
.callout.tip .callout-icon, .callout.tip .callout-body > b:first-child { color: var(--plus); }
.callout.warn { background: #fdecea; border-left: 3pt solid var(--minus); }
.callout.warn .callout-icon, .callout.warn .callout-body > b:first-child { color: var(--minus); }
.callout.note { background: var(--bg-soft); border-left: 3pt solid var(--php); }
.callout.note .callout-icon, .callout.note .callout-body > b:first-child { color: var(--php-dark); }
/* ===== RECALL ===== */
.recall {
background: linear-gradient(135deg, var(--php-dark) 0%, var(--php-darker) 100%);
color: white;
padding: 5mm 6mm;
border-radius: 2mm;
margin: 6mm 0 0 0;
page-break-inside: avoid;
}
.recall b {
font-family: -apple-system, sans-serif;
display: block;
text-transform: uppercase;
letter-spacing: 1.5pt;
font-size: 8.5pt;
color: var(--php);
margin-bottom: 2.5mm;
}
.recall ol {
margin: 0;
padding-left: 5mm;
font-size: 10pt;
}
.recall li {
margin-bottom: 1.5mm;
color: rgba(255,255,255,0.95);
}
.recall li::marker { color: var(--php); font-weight: 700; }
/* ===== TABLES ===== */
.chapter table {
width: 100%;
border-collapse: collapse;
margin: 3mm 0 4mm 0;
font-size: 9.5pt;
font-family: -apple-system, sans-serif;
}
.chapter th {
background: var(--php-dark);
color: white;
padding: 2mm 3mm;
text-align: left;
font-weight: 700;
font-size: 9pt;
text-transform: uppercase;
letter-spacing: 0.5pt;
}
.chapter td {
padding: 2mm 3mm;
border-bottom: 0.5pt solid var(--line);
vertical-align: top;
}
.chapter td code {
font-family: "SF Mono", Consolas, monospace;
font-size: 8.5pt;
color: var(--php-dark);
}
/* ===== ENDING ===== */
.ending { page-break-before: always; }
.ending h1 {
font-family: -apple-system, sans-serif;
font-size: 28pt;
font-weight: 800;
color: var(--php-dark);
margin-bottom: 6mm;
border-bottom: 2pt solid var(--php);
padding-bottom: 3mm;
}
.ending h2 {
font-family: -apple-system, sans-serif;
font-size: 14pt;
font-weight: 700;
color: var(--php-dark);
margin: 7mm 0 3mm 0;
}
.spaced-plan {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 3mm;
margin: 4mm 0;
}
.spaced-day {
background: var(--bg-soft);
border-top: 3pt solid var(--php);
padding: 4mm 3mm;
border-radius: 2mm;
}
.spaced-day b {
font-family: -apple-system, sans-serif;
display: block;
color: var(--php-dark);
font-size: 9pt;
text-transform: uppercase;
letter-spacing: 0.5pt;
margin-bottom: 2mm;
}
.spaced-day p {
font-size: 9pt;
margin: 0;
text-align: left;
}
</style>
</head>
<body>
<!-- ===== COVER ===== -->
<section class="cover">
<div class="cover-top">
<div class="cover-logo">php</div>
<div class="cover-meta">Extended-Guide · 4h · Stand 2026</div>
</div>
<div class="cover-main">
<h1>PHP<br><span class="accent">an der Grenze.</span></h1>
<p class="subtitle">Internals, Fibers, FFI, DDD &amp; Event Sourcing PHP über das Übliche hinaus.</p>
</div>
<div class="cover-bottom">
<div class="cover-promise">
<b>Was du danach kannst</b>
Reflection und Stream-Wrapper produktiv einsetzen · mit Fibers asynchron programmieren · eigenen DI-Container schreiben · Performance gezielt profilen und optimieren · komplexe Domänen mit DDD, Hexagonal Architecture und Event Sourcing strukturieren.
</div>
<div class="cover-tag">PHP 8.4</div>
</div>
</section>
<!-- ===== TOC ===== -->
<section class="toc">
<h2>Inhalt</h2>
<div class="toc-section">Teil 1 · Sprach-Internals</div>
<div class="toc-item">
<div class="toc-num">1</div>
<div class="toc-text">
<h3>Reflection-API tief</h3>
<p>Klassen, Methoden, Attribute zur Laufzeit erkunden</p>
</div>
<div class="toc-time">15 Min</div>
</div>
<div class="toc-item">
<div class="toc-num">2</div>
<div class="toc-text">
<h3>SPL Datenstrukturen</h3>
<p>SplStack, SplQueue, SplPriorityQueue, SplObjectStorage</p>
</div>
<div class="toc-time">15 Min</div>
</div>
<div class="toc-item">
<div class="toc-num">3</div>
<div class="toc-text">
<h3>Stream-Wrapper und Filter</h3>
<p>Eigene Protokolle wie file:// schreiben</p>
</div>
<div class="toc-time">15 Min</div>
</div>
<div class="toc-item">
<div class="toc-num">4</div>
<div class="toc-text">
<h3>Garbage Collection</h3>
<p>Zirkuläre Referenzen, Memory-Profiling</p>
</div>
<div class="toc-time">15 Min</div>
</div>
<div class="toc-item">
<div class="toc-num">5</div>
<div class="toc-text">
<h3>FFI - C-Code direkt aufrufen</h3>
<p>Foreign Function Interface für Native-Libraries</p>
</div>
<div class="toc-time">15 Min</div>
</div>
<div class="toc-section">Teil 2 · Performance &amp; Async</div>
<div class="toc-item">
<div class="toc-num">6</div>
<div class="toc-text">
<h3>OpCache und Preloading</h3>
<p>Code-Compilation einmal, nicht pro Request</p>
</div>
<div class="toc-time">15 Min</div>
</div>
<div class="toc-item">
<div class="toc-num">7</div>
<div class="toc-text">
<h3>Fibers - Cooperative Concurrency</h3>
<p>Async ohne Threading (PHP 8.1+)</p>
</div>
<div class="toc-time">15 Min</div>
</div>
<div class="toc-item">
<div class="toc-num">8</div>
<div class="toc-text">
<h3>ReactPHP &amp; Event-Loops</h3>
<p>Long-Running Server, non-blocking I/O</p>
</div>
<div class="toc-time">15 Min</div>
</div>
<div class="toc-item">
<div class="toc-num">9</div>
<div class="toc-text">
<h3>Profiling mit Blackfire</h3>
<p>CPU- und Memory-Hotspots finden</p>
</div>
<div class="toc-time">15 Min</div>
</div>
<div class="toc-item">
<div class="toc-num">10</div>
<div class="toc-text">
<h3>Caching-Strategien</h3>
<p>APCu, Redis, HTTP-Cache, ESI</p>
</div>
<div class="toc-time">15 Min</div>
</div>
<div class="toc-section">Teil 3 · Architektur &amp; Patterns</div>
<div class="toc-item">
<div class="toc-num">11</div>
<div class="toc-text">
<h3>Dependency Injection Container</h3>
<p>Eigener Container in 100 Zeilen</p>
</div>
<div class="toc-time">15 Min</div>
</div>
<div class="toc-item">
<div class="toc-num">12</div>
<div class="toc-text">
<h3>Event-Dispatcher Pattern</h3>
<p>Lose Kopplung zwischen Bounded Contexts</p>
</div>
<div class="toc-time">15 Min</div>
</div>
<div class="toc-item">
<div class="toc-num">13</div>
<div class="toc-text">
<h3>CQRS und Command Bus</h3>
<p>Lese- und Schreibmodelle trennen</p>
</div>
<div class="toc-time">15 Min</div>
</div>
<div class="toc-item">
<div class="toc-num">14</div>
<div class="toc-text">
<h3>Domain-Driven Design in PHP</h3>
<p>Value Objects, Entities, Aggregates</p>
</div>
<div class="toc-time">15 Min</div>
</div>
<div class="toc-item">
<div class="toc-num">15</div>
<div class="toc-text">
<h3>Hexagonal Architecture</h3>
<p>Ports und Adapter, Domain im Zentrum</p>
</div>
<div class="toc-time">15 Min</div>
</div>
<div class="toc-item">
<div class="toc-num">16</div>
<div class="toc-text">
<h3>Event Sourcing</h3>
<p>State als Sequenz von Events</p>
</div>
<div class="toc-time">15 Min</div>
</div>
</section>
<!-- ===== KAPITEL 1 ===== -->
<section class="chapter">
<div class="chapter-head">
<div class="chapter-num">01</div>
<div class="chapter-title">
<h1>Reflection-API tief</h1>
<div class="subtitle">Klassen zur Laufzeit erkunden</div>
</div>
</div>
<div class="gap">
<b>Frage zum Einstieg</b>
Wie weiß Symfony zur Laufzeit, welche Routen in deinem Controller stecken? Wie generiert Doctrine SQL-Schemas aus deinen Entity-Klassen? Beide nutzen <b>Reflection</b> PHPs eingebauter Mechanismus, um Code-Struktur zu introspecten.
</div>
<h2>ReflectionClass: alles über eine Klasse</h2>
<p>Mit <code class="inline">ReflectionClass</code> bekommst du alle Metadaten einer Klasse: Properties, Methoden, Attribute, Parent, Interfaces. Das ist die Basis fast aller Framework-Magie:</p>
<pre><span class="v">$reflection</span> = <span class="k">new</span> \<span class="t">ReflectionClass</span>(<span class="t">User</span>::<span class="k">class</span>);
<span class="v">$reflection</span>-&gt;<span class="f">getName</span>(); <span class="c">// 'App\User'</span>
<span class="v">$reflection</span>-&gt;<span class="f">getShortName</span>(); <span class="c">// 'User'</span>
<span class="v">$reflection</span>-&gt;<span class="f">getMethods</span>(); <span class="c">// ReflectionMethod[]</span>
<span class="v">$reflection</span>-&gt;<span class="f">getProperties</span>(); <span class="c">// ReflectionProperty[]</span>
<span class="v">$reflection</span>-&gt;<span class="f">getParentClass</span>(); <span class="c">// ReflectionClass|false</span>
<span class="v">$reflection</span>-&gt;<span class="f">getInterfaceNames</span>(); <span class="c">// string[]</span>
<span class="v">$reflection</span>-&gt;<span class="f">getAttributes</span>(); <span class="c">// ReflectionAttribute[]</span></pre>
<h2>Instanzen ohne Konstruktor</h2>
<p>Manchmal willst du eine Klasse instanziieren, ohne den Konstruktor aufzurufen z.B. zum Deserialisieren von Datenbank-Rows oder JSON. Reflection erlaubt das:</p>
<pre><span class="v">$reflection</span> = <span class="k">new</span> \<span class="t">ReflectionClass</span>(<span class="t">User</span>::<span class="k">class</span>);
<span class="v">$user</span> = <span class="v">$reflection</span>-&gt;<span class="f">newInstanceWithoutConstructor</span>();
<span class="c">// Jetzt Properties direkt setzen</span>
<span class="v">$nameProperty</span> = <span class="v">$reflection</span>-&gt;<span class="f">getProperty</span>(<span class="s">'name'</span>);
<span class="v">$nameProperty</span>-&gt;<span class="f">setValue</span>(<span class="v">$user</span>, <span class="s">'Marek'</span>);</pre>
<p>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.</p>
<h2>Private Properties und Methoden</h2>
<p>Reflection ignoriert Sichtbarkeitsregeln essentiell für Testing-Frameworks und Serializer:</p>
<pre><span class="k">class</span> <span class="t">Order</span> {
<span class="k">private string</span> <span class="v">$internalRef</span>;
<span class="k">private function</span> <span class="f">calculateTax</span>(): <span class="t">float</span> { <span class="c">/* ... */</span> }
}
<span class="v">$order</span> = <span class="k">new</span> <span class="t">Order</span>();
<span class="v">$reflection</span> = <span class="k">new</span> \<span class="t">ReflectionClass</span>(<span class="v">$order</span>);
<span class="c">// Private property setzen (für Tests)</span>
<span class="v">$prop</span> = <span class="v">$reflection</span>-&gt;<span class="f">getProperty</span>(<span class="s">'internalRef'</span>);
<span class="v">$prop</span>-&gt;<span class="f">setValue</span>(<span class="v">$order</span>, <span class="s">'TEST-123'</span>);
<span class="c">// Private Methode aufrufen</span>
<span class="v">$method</span> = <span class="v">$reflection</span>-&gt;<span class="f">getMethod</span>(<span class="s">'calculateTax'</span>);
<span class="v">$tax</span> = <span class="v">$method</span>-&gt;<span class="f">invoke</span>(<span class="v">$order</span>);</pre>
<h2>Performance-Implikationen</h2>
<p>Reflection ist <i>nicht gratis</i>. Pro <code class="inline">ReflectionClass</code>-Instanz parst PHP intern viele Metadaten. Frameworks wie Symfony cachen Reflection-Ergebnisse aggressiv. Faustregel:</p>
<table>
<tr><th>Use Case</th><th>OK</th></tr>
<tr><td>Boot-Zeit, einmal pro Request</td><td>ja</td></tr>
<tr><td>Dependency-Injection-Container Build</td><td>ja, aber cachen</td></tr>
<tr><td>In jedem Method-Call</td><td>nein, das ist langsam</td></tr>
<tr><td>Hot-Path-Loops</td><td>nein, niemals</td></tr>
</table>
<div class="callout note">
<div class="callout-icon">i</div>
<div class="callout-body">
<b>Attribute via Reflection</b>
Wie im Fortgeschritten-Guide gezeigt: <code class="inline">$reflection-&gt;getAttributes()</code> liest PHP 8 Attribute aus. Das ist die Brücke zwischen deinem deklarativen Markup (<code class="inline">#[Route('/users')]</code>) und dem Framework-Code, der es interpretiert.
</div>
</div>
<div class="recall">
<b>Recall</b>
<ol>
<li>Welche zwei großen Frameworks nutzen Reflection als Kern-Mechanismus?</li>
<li>Wie umgehst du den Konstruktor beim Instanziieren?</li>
<li>Warum ist Reflection in Hot-Paths problematisch?</li>
</ol>
</div>
</section>
<!-- ===== KAPITEL 2 ===== -->
<section class="chapter">
<div class="chapter-head">
<div class="chapter-num">02</div>
<div class="chapter-title">
<h1>SPL Datenstrukturen</h1>
<div class="subtitle">Stack, Queue, Heap, ObjectStorage</div>
</div>
</div>
<div class="gap">
<b>Frage zum Einstieg</b>
PHP-Arrays sind extrem flexibel aber für spezielle Use Cases nicht optimal. Eine Priority Queue mit Array nachbauen kostet O(n log n) pro Insert. Die <b>Standard PHP Library (SPL)</b> liefert spezialisierte Datenstrukturen. Wann lohnt sich der Wechsel?
</div>
<h2>SplStack und SplQueue</h2>
<p>Stack (LIFO) und Queue (FIFO) als typisierte Strukturen. Bei großen Mengen schneller als Array-Operationen mit <code class="inline">array_push</code>/<code class="inline">array_shift</code>:</p>
<pre><span class="v">$stack</span> = <span class="k">new</span> \<span class="t">SplStack</span>();
<span class="v">$stack</span>-&gt;<span class="f">push</span>(<span class="s">'a'</span>);
<span class="v">$stack</span>-&gt;<span class="f">push</span>(<span class="s">'b'</span>);
<span class="v">$stack</span>-&gt;<span class="f">push</span>(<span class="s">'c'</span>);
<span class="v">$stack</span>-&gt;<span class="f">pop</span>(); <span class="c">// 'c'</span>
<span class="v">$stack</span>-&gt;<span class="f">top</span>(); <span class="c">// 'b' (peek ohne entfernen)</span>
<span class="v">$queue</span> = <span class="k">new</span> \<span class="t">SplQueue</span>();
<span class="v">$queue</span>-&gt;<span class="f">enqueue</span>(<span class="s">'first'</span>);
<span class="v">$queue</span>-&gt;<span class="f">enqueue</span>(<span class="s">'second'</span>);
<span class="v">$queue</span>-&gt;<span class="f">dequeue</span>(); <span class="c">// 'first'</span></pre>
<p>Wichtiger Vorteil gegenüber Array: O(1) für alle Operationen, auch <code class="inline">dequeue</code>. <code class="inline">array_shift</code> ist O(n) wegen Re-Indexing.</p>
<h2>SplPriorityQueue</h2>
<p>Bei Aufgaben mit Prioritäten Job-Scheduler, A*-Pathfinding, Event-Loops ist eine Priority Queue Standard:</p>
<pre><span class="v">$pq</span> = <span class="k">new</span> \<span class="t">SplPriorityQueue</span>();
<span class="v">$pq</span>-&gt;<span class="f">insert</span>(<span class="s">'low-priority-task'</span>, <span class="s">1</span>);
<span class="v">$pq</span>-&gt;<span class="f">insert</span>(<span class="s">'urgent-task'</span>, <span class="s">10</span>);
<span class="v">$pq</span>-&gt;<span class="f">insert</span>(<span class="s">'normal-task'</span>, <span class="s">5</span>);
<span class="k">while</span> (!<span class="v">$pq</span>-&gt;<span class="f">isEmpty</span>()) {
<span class="k">echo</span> <span class="v">$pq</span>-&gt;<span class="f">extract</span>() . <span class="s">"\n"</span>;
}
<span class="c">// urgent-task</span>
<span class="c">// normal-task</span>
<span class="c">// low-priority-task</span></pre>
<h2>SplObjectStorage: Map mit Objekten als Keys</h2>
<p>Reguläre PHP-Arrays erlauben nur Strings/Ints als Keys. Mit <code class="inline">SplObjectStorage</code> kannst du <b>Objekte selbst als Keys</b> nutzen:</p>
<pre><span class="v">$permissions</span> = <span class="k">new</span> \<span class="t">SplObjectStorage</span>();
<span class="v">$alice</span> = <span class="k">new</span> <span class="t">User</span>(<span class="s">'Alice'</span>);
<span class="v">$bob</span> = <span class="k">new</span> <span class="t">User</span>(<span class="s">'Bob'</span>);
<span class="v">$permissions</span>[<span class="v">$alice</span>] = [<span class="s">'read'</span>, <span class="s">'write'</span>];
<span class="v">$permissions</span>[<span class="v">$bob</span>] = [<span class="s">'read'</span>];
<span class="v">$permissions</span>[<span class="v">$alice</span>]; <span class="c">// ['read', 'write']</span>
<span class="v">$permissions</span>-&gt;<span class="f">contains</span>(<span class="v">$bob</span>); <span class="c">// true</span>
<span class="c">// Iteration</span>
<span class="k">foreach</span> (<span class="v">$permissions</span> <span class="k">as</span> <span class="v">$user</span>) {
<span class="v">$perms</span> = <span class="v">$permissions</span>[<span class="v">$user</span>];
<span class="k">echo</span> <span class="v">$user</span>-&gt;name . <span class="s">': '</span> . <span class="f">implode</span>(<span class="s">','</span>, <span class="v">$perms</span>) . <span class="s">"\n"</span>;
}</pre>
<p>Praktisch für Identity-Maps in ORMs, Permission-Systems, Graphen mit Objekt-Knoten.</p>
<h2>SplFixedArray für Memory-Optimierung</h2>
<p>Bei sehr großen, indizierten Arrays mit fester Größe ist <code class="inline">SplFixedArray</code> deutlich speichereffizienter:</p>
<pre><span class="c">// Reguläres Array: ~100MB für 1M Elemente</span>
<span class="v">$arr</span> = [];
<span class="k">for</span> (<span class="v">$i</span> = <span class="s">0</span>; <span class="v">$i</span> &lt; <span class="s">1_000_000</span>; <span class="v">$i</span>++) {
<span class="v">$arr</span>[<span class="v">$i</span>] = <span class="v">$i</span> * <span class="s">2</span>;
}
<span class="c">// SplFixedArray: ~40MB für dasselbe</span>
<span class="v">$arr</span> = <span class="k">new</span> \<span class="t">SplFixedArray</span>(<span class="s">1_000_000</span>);
<span class="k">for</span> (<span class="v">$i</span> = <span class="s">0</span>; <span class="v">$i</span> &lt; <span class="s">1_000_000</span>; <span class="v">$i</span>++) {
<span class="v">$arr</span>[<span class="v">$i</span>] = <span class="v">$i</span> * <span class="s">2</span>;
}</pre>
<div class="callout warn">
<div class="callout-icon">!</div>
<div class="callout-body">
<b>SPL nur wenn nötig</b>
Für die meisten Web-Apps reichen normale Arrays völlig. SPL lohnt sich, wenn du Performance-kritische Pfade mit großen Mengen hast oder spezielle Semantik brauchst (Priority, Object-Keys). Sonst macht es Code unnötig komplex.
</div>
</div>
<div class="recall">
<b>Recall</b>
<ol>
<li>Was ist der Performance-Vorteil von <code style="color:#777BB4">SplQueue</code> gegenüber <code style="color:#777BB4">array_shift</code>?</li>
<li>Wofür nutzt du <code style="color:#777BB4">SplObjectStorage</code>?</li>
<li>Wann lohnt sich <code style="color:#777BB4">SplFixedArray</code>?</li>
</ol>
</div>
</section>
<!-- ===== KAPITEL 3 ===== -->
<section class="chapter">
<div class="chapter-head">
<div class="chapter-num">03</div>
<div class="chapter-title">
<h1>Stream-Wrapper und Filter</h1>
<div class="subtitle">Eigene Protokolle wie file:// schreiben</div>
</div>
</div>
<div class="gap">
<b>Frage zum Einstieg</b>
Du kennst <code>fopen('file:///pfad')</code>, <code>file_get_contents('http://...')</code>, vielleicht <code>fopen('php://memory')</code>. All das funktioniert über <b>Stream-Wrapper</b>. Du kannst eigene schreiben z.B. <code>s3://bucket/key</code> oder <code>db://users/42</code>. Wie geht das?
</div>
<h2>Was sind Stream-Wrapper?</h2>
<p>Ein Stream-Wrapper definiert ein <b>Pseudo-Protokoll</b>, das wie eine Datei aussieht. Alle PHP-Funktionen, die mit Streams arbeiten (<code class="inline">fopen</code>, <code class="inline">fread</code>, <code class="inline">file_get_contents</code>, <code class="inline">file_put_contents</code>), funktionieren transparent damit.</p>
<p>PHP bringt mehrere eingebaute Wrapper mit:</p>
<table>
<tr><th>Wrapper</th><th>Wofür</th></tr>
<tr><td><code>file://</code></td><td>Lokales Dateisystem (Default)</td></tr>
<tr><td><code>http:// / https://</code></td><td>HTTP-Requests</td></tr>
<tr><td><code>php://memory</code></td><td>In-Memory Stream</td></tr>
<tr><td><code>php://stdin / php://stdout</code></td><td>Standard-IO</td></tr>
<tr><td><code>php://input</code></td><td>Raw POST-Body</td></tr>
<tr><td><code>data://</code></td><td>Base64 oder URL-encoded Daten</td></tr>
<tr><td><code>compress.zlib://</code></td><td>gzip-Streams</td></tr>
</table>
<h2>Eigenen Wrapper schreiben</h2>
<p>Ein Stream-Wrapper ist eine Klasse mit bestimmten Methoden. Minimal-Beispiel: ein Wrapper, der Strings aus einem statischen Array liefert:</p>
<pre><span class="k">class</span> <span class="t">DictionaryStreamWrapper</span> {
<span class="k">private static array</span> <span class="v">$dict</span> = [
<span class="s">'hello'</span> =&gt; <span class="s">'Hallo, Welt!'</span>,
<span class="s">'goodbye'</span> =&gt; <span class="s">'Auf Wiedersehen.'</span>,
];
<span class="k">private int</span> <span class="v">$position</span> = <span class="s">0</span>;
<span class="k">private string</span> <span class="v">$data</span> = <span class="s">''</span>;
<span class="k">public function</span> <span class="f">stream_open</span>(<span class="t">string</span> <span class="v">$path</span>, <span class="t">string</span> <span class="v">$mode</span>, <span class="t">int</span> <span class="v">$options</span>, ?<span class="t">string</span> &amp;<span class="v">$openedPath</span>): <span class="t">bool</span> {
<span class="v">$key</span> = <span class="f">parse_url</span>(<span class="v">$path</span>, PHP_URL_HOST);
<span class="k">if</span> (!<span class="f">isset</span>(<span class="t">self</span>::<span class="v">$dict</span>[<span class="v">$key</span>])) <span class="k">return false</span>;
<span class="v">$this</span>-&gt;data = <span class="t">self</span>::<span class="v">$dict</span>[<span class="v">$key</span>];
<span class="k">return true</span>;
}
<span class="k">public function</span> <span class="f">stream_read</span>(<span class="t">int</span> <span class="v">$count</span>): <span class="t">string</span> {
<span class="v">$chunk</span> = <span class="f">substr</span>(<span class="v">$this</span>-&gt;data, <span class="v">$this</span>-&gt;position, <span class="v">$count</span>);
<span class="v">$this</span>-&gt;position += <span class="f">strlen</span>(<span class="v">$chunk</span>);
<span class="k">return</span> <span class="v">$chunk</span>;
}
<span class="k">public function</span> <span class="f">stream_eof</span>(): <span class="t">bool</span> {
<span class="k">return</span> <span class="v">$this</span>-&gt;position &gt;= <span class="f">strlen</span>(<span class="v">$this</span>-&gt;data);
}
}
<span class="c">// Registrieren und nutzen</span>
<span class="f">stream_wrapper_register</span>(<span class="s">'dict'</span>, <span class="t">DictionaryStreamWrapper</span>::<span class="k">class</span>);
<span class="k">echo</span> <span class="f">file_get_contents</span>(<span class="s">'dict://hello'</span>); <span class="c">// 'Hallo, Welt!'</span></pre>
<h2>Praktischer Use Case: S3-Wrapper</h2>
<p>AWS-SDK für PHP registriert einen <code class="inline">s3://</code>-Wrapper. Dein Code arbeitet dann mit S3 wie mit lokalen Dateien:</p>
<pre><span class="k">use</span> Aws\S3\S3Client;
<span class="v">$client</span> = <span class="k">new</span> S3Client([<span class="c">/* config */</span>]);
<span class="v">$client</span>-&gt;<span class="f">registerStreamWrapper</span>();
<span class="c">// Jetzt arbeitet jede File-Funktion mit S3</span>
<span class="v">$data</span> = <span class="f">file_get_contents</span>(<span class="s">'s3://my-bucket/users/42.json'</span>);
<span class="f">file_put_contents</span>(<span class="s">'s3://my-bucket/log.txt'</span>, <span class="s">'eintrag'</span>);
<span class="k">foreach</span> (<span class="f">scandir</span>(<span class="s">'s3://my-bucket/uploads/'</span>) <span class="k">as</span> <span class="v">$file</span>) {
<span class="c">// ...</span>
}</pre>
<h2>Stream-Filter</h2>
<p>Filter transformieren Daten <i>während</i> des Streamings. Eingebaute Filter: <code class="inline">string.rot13</code>, <code class="inline">string.toupper</code>, <code class="inline">zlib.deflate</code>:</p>
<pre><span class="v">$handle</span> = <span class="f">fopen</span>(<span class="s">'large.txt'</span>, <span class="s">'r'</span>);
<span class="f">stream_filter_append</span>(<span class="v">$handle</span>, <span class="s">'string.toupper'</span>);
<span class="k">while</span> (!<span class="f">feof</span>(<span class="v">$handle</span>)) {
<span class="k">echo</span> <span class="f">fread</span>(<span class="v">$handle</span>, <span class="s">8192</span>); <span class="c">// alles UPPERCASE</span>
}</pre>
<div class="callout tip">
<div class="callout-icon">✓</div>
<div class="callout-body">
<b>Flysystem für Cloud-Storage</b>
Für Production: nutze <b>league/flysystem</b> statt eigene Stream-Wrapper zu schreiben. Es abstrahiert S3, Azure, GCS, FTP, local mit einer einheitlichen API stabiler und besser getestet als selbst gestrickte Wrapper.
</div>
</div>
<div class="recall">
<b>Recall</b>
<ol>
<li>Welche Methoden braucht ein Stream-Wrapper mindestens?</li>
<li>Wozu sind Stream-Filter da?</li>
<li>Welche Library nutzt du stattdessen für Cloud-Storage?</li>
</ol>
</div>
</section>
<!-- ===== KAPITEL 4 ===== -->
<section class="chapter">
<div class="chapter-head">
<div class="chapter-num">04</div>
<div class="chapter-title">
<h1>Garbage Collection</h1>
<div class="subtitle">Zirkuläre Referenzen, Memory-Profiling</div>
</div>
</div>
<div class="gap">
<b>Frage zum Einstieg</b>
PHP räumt Speicher automatisch auf meistens. Aber in Long-Running-Prozessen (CLI-Tools, Workers, ReactPHP-Server) sammeln sich plötzlich GB an RAM. Was ist los? Die Antwort liegt in PHPs <b>Garbage Collection</b> und wie sie zirkuläre Referenzen behandelt.
</div>
<h2>Reference Counting</h2>
<p>PHP nutzt primär <b>Reference Counting</b>: jede Variable hat einen Counter, wie oft auf sie verwiesen wird. Fällt der Counter auf 0, wird der Speicher sofort freigegeben:</p>
<pre><span class="v">$a</span> = <span class="s">'hallo'</span>; <span class="c">// refcount = 1</span>
<span class="v">$b</span> = <span class="v">$a</span>; <span class="c">// refcount = 2</span>
<span class="k">unset</span>(<span class="v">$a</span>); <span class="c">// refcount = 1</span>
<span class="k">unset</span>(<span class="v">$b</span>); <span class="c">// refcount = 0 → Speicher frei</span></pre>
<p>Das ist schnell und deterministisch. Aber es hat ein Problem: <b>zirkuläre Referenzen</b>.</p>
<h2>Das Problem zirkulärer Referenzen</h2>
<pre><span class="k">class</span> <span class="t">Node</span> {
<span class="k">public</span> ?<span class="t">Node</span> <span class="v">$next</span> = <span class="k">null</span>;
}
<span class="v">$a</span> = <span class="k">new</span> <span class="t">Node</span>();
<span class="v">$b</span> = <span class="k">new</span> <span class="t">Node</span>();
<span class="v">$a</span>-&gt;next = <span class="v">$b</span>; <span class="c">// $b refcount = 2</span>
<span class="v">$b</span>-&gt;next = <span class="v">$a</span>; <span class="c">// $a refcount = 2</span>
<span class="k">unset</span>(<span class="v">$a</span>); <span class="c">// $a refcount = 1 (von $b-&gt;next)</span>
<span class="k">unset</span>(<span class="v">$b</span>); <span class="c">// $b refcount = 1 (von $a-&gt;next)</span>
<span class="c">// Beide Counter sind 1, aber nichts greift mehr drauf zu</span>
<span class="c">// → Memory Leak!</span></pre>
<p>Reference Counting allein erkennt das nicht. PHP hat dafür einen zusätzlichen <b>Cycle Collector</b>, der periodisch durchläuft und solche Zyklen findet.</p>
<h2>Cycle Collector steuern</h2>
<p>Der Collector läuft automatisch, wenn ein interner Buffer voll ist (Default 10.000 Roots). Du kannst ihn manuell triggern:</p>
<pre><span class="c">// Aktuellen Status prüfen</span>
<span class="f">gc_status</span>(); <span class="c">// runs, collected, threshold, roots</span>
<span class="c">// Manuell laufen lassen</span>
<span class="v">$collected</span> = <span class="f">gc_collect_cycles</span>(); <span class="c">// Anzahl entsorgter Objekte</span>
<span class="c">// Deaktivieren (Performance-kritische Sektion)</span>
<span class="f">gc_disable</span>();
<span class="c">// ... Hot-Code ...</span>
<span class="f">gc_enable</span>();</pre>
<h2>Long-Running-Prozesse</h2>
<p>In CLI-Workern oder ReactPHP-Servern ist Memory-Management kritisch PHP-Scripts sterben sonst nach Stunden. Pattern:</p>
<pre><span class="k">while</span> (<span class="v">$message</span> = <span class="v">$queue</span>-&gt;<span class="f">consume</span>()) {
<span class="f">processMessage</span>(<span class="v">$message</span>);
<span class="c">// Memory-Check</span>
<span class="k">if</span> (<span class="f">memory_get_usage</span>() &gt; <span class="s">100</span> * <span class="s">1024</span> * <span class="s">1024</span>) {
<span class="f">gc_collect_cycles</span>();
<span class="k">if</span> (<span class="f">memory_get_usage</span>() &gt; <span class="s">200</span> * <span class="s">1024</span> * <span class="s">1024</span>) {
<span class="k">exit</span>(<span class="s">0</span>); <span class="c">// Supervisor startet neu</span>
}
}
}</pre>
<p>Symfony Messenger und Laravel Horizon nutzen genau dieses Pattern sie killen Worker nach bestimmter Memory-Schwelle und lassen sie neu starten.</p>
<h2>WeakReference und WeakMap</h2>
<p>Seit PHP 7.4 (WeakReference) und 8.0 (WeakMap) gibt es einen offiziellen Mechanismus für Referenzen, die <i>nicht</i> in den Refcount zählen:</p>
<pre><span class="v">$user</span> = <span class="k">new</span> <span class="t">User</span>(<span class="s">'Marek'</span>);
<span class="v">$weak</span> = \<span class="t">WeakReference</span>::<span class="f">create</span>(<span class="v">$user</span>);
<span class="v">$weak</span>-&gt;<span class="f">get</span>(); <span class="c">// User-Objekt</span>
<span class="k">unset</span>(<span class="v">$user</span>); <span class="c">// User wird sofort freigegeben</span>
<span class="v">$weak</span>-&gt;<span class="f">get</span>(); <span class="c">// null</span>
<span class="c">// WeakMap: nützlich für Caches, die nicht "festhalten"</span>
<span class="v">$cache</span> = <span class="k">new</span> \<span class="t">WeakMap</span>();
<span class="v">$cache</span>[<span class="v">$user</span>] = <span class="s">'cached-value'</span>;
<span class="c">// Sobald $user weg ist, ist auch der Cache-Eintrag weg</span></pre>
<div class="recall">
<b>Recall</b>
<ol>
<li>Was ist PHPs primäre GC-Strategie?</li>
<li>Warum reicht Reference Counting bei zirkulären Referenzen nicht?</li>
<li>Wofür nutzt du <code style="color:#777BB4">WeakReference</code>?</li>
</ol>
</div>
</section>
<!-- ===== KAPITEL 5 ===== -->
<section class="chapter">
<div class="chapter-head">
<div class="chapter-num">05</div>
<div class="chapter-title">
<h1>FFI - C-Code direkt aufrufen</h1>
<div class="subtitle">Foreign Function Interface</div>
</div>
</div>
<div class="gap">
<b>Frage zum Einstieg</b>
Du brauchst eine C-Library etwa für GPU-Berechnungen, Bildverarbeitung oder spezielle Algorithmen. Klassisch: PHP-Extension in C schreiben (großer Aufwand). Mit <b>FFI</b> seit PHP 7.4 rufst du C-Funktionen direkt aus PHP-Code auf, ohne Kompilieren.
</div>
<h2>FFI aktivieren</h2>
<p>FFI ist standardmäßig installiert, muss aber in der <code class="inline">php.ini</code> aktiviert werden:</p>
<pre><span class="c"># php.ini</span>
extension=ffi
ffi.enable=preload <span class="c"># sicher (nur für preloaded scripts)</span>
<span class="c"># oder: ffi.enable=true # offen, unsicher in Web-Context</span></pre>
<h2>C-Funktion aufrufen</h2>
<p>FFI braucht zwei Dinge: die Signatur der C-Funktion und die Library-Datei. Klassisches Beispiel: die libc-Funktion <code class="inline">getpid</code>:</p>
<pre><span class="v">$ffi</span> = \<span class="t">FFI</span>::<span class="f">cdef</span>(<span class="s">"
int getpid(void);
unsigned int sleep(unsigned int seconds);
"</span>, <span class="s">"libc.so.6"</span>);
<span class="k">echo</span> <span class="v">$ffi</span>-&gt;<span class="f">getpid</span>(); <span class="c">// 12345 (Process-ID)</span>
<span class="v">$ffi</span>-&gt;<span class="f">sleep</span>(<span class="s">2</span>); <span class="c">// 2 Sekunden schlafen</span></pre>
<h2>Header-Datei laden</h2>
<p>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:</p>
<pre><span class="v">$ffi</span> = \<span class="t">FFI</span>::<span class="f">load</span>(<span class="s">'/usr/include/sdl2/SDL.h'</span>);
<span class="c">// Jetzt sind alle SDL-Funktionen verfügbar</span>
<span class="v">$ffi</span>-&gt;<span class="f">SDL_Init</span>(SDL_INIT_VIDEO);
<span class="v">$window</span> = <span class="v">$ffi</span>-&gt;<span class="f">SDL_CreateWindow</span>(...);</pre>
<h2>Structs und Pointer</h2>
<p>C-Strukturen werden zu PHP-Objekten. Pointer und Memory-Allokation funktionieren auch:</p>
<pre><span class="v">$ffi</span> = \<span class="t">FFI</span>::<span class="f">cdef</span>(<span class="s">"
typedef struct {
int x;
int y;
} Point;
"</span>);
<span class="c">// Struct allokieren</span>
<span class="v">$point</span> = <span class="v">$ffi</span>-&gt;<span class="f">new</span>(<span class="s">'Point'</span>);
<span class="v">$point</span>-&gt;x = <span class="s">42</span>;
<span class="v">$point</span>-&gt;y = <span class="s">17</span>;
<span class="c">// Array von Structs</span>
<span class="v">$points</span> = <span class="v">$ffi</span>-&gt;<span class="f">new</span>(<span class="s">'Point[100]'</span>);
<span class="k">for</span> (<span class="v">$i</span> = <span class="s">0</span>; <span class="v">$i</span> &lt; <span class="s">100</span>; <span class="v">$i</span>++) {
<span class="v">$points</span>[<span class="v">$i</span>]-&gt;x = <span class="v">$i</span>;
}</pre>
<h2>Praxis-Use-Cases</h2>
<table>
<tr><th>Use Case</th><th>Beispiel-Library</th></tr>
<tr><td>Bildverarbeitung</td><td>libvips, OpenCV</td></tr>
<tr><td>Kompression</td><td>libzstd, brotli</td></tr>
<tr><td>Crypto</td><td>libsodium (auch nativ in PHP)</td></tr>
<tr><td>ML-Inferenz</td><td>libtorch, onnxruntime</td></tr>
<tr><td>Grafik/UI</td><td>SDL, GTK</td></tr>
<tr><td>System-Calls</td><td>libc direkt</td></tr>
</table>
<h2>Performance-Trade-Off</h2>
<p>FFI-Calls sind nicht gratis: jeder Aufruf hat Overhead durch Type-Conversion. Faustregel:</p>
<ul>
<li><b>Lohnt sich</b>: wenige Aufrufe mit viel C-Arbeit pro Call (Bildverarbeitung)</li>
<li><b>Lohnt sich nicht</b>: viele kleine Aufrufe in Loops (klassische PHP-Logik schneller)</li>
</ul>
<div class="callout warn">
<div class="callout-icon">!</div>
<div class="callout-body">
<b>FFI ist nicht für Web-Apps gedacht</b>
Mit <code class="inline">ffi.enable=preload</code> 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.
</div>
</div>
<div class="recall">
<b>Recall</b>
<ol>
<li>Welche zwei Dinge braucht <code style="color:#777BB4">FFI::cdef</code>?</li>
<li>Wann lohnt sich FFI, wann nicht?</li>
<li>Warum solltest du FFI nicht in Web-Requests nutzen?</li>
</ol>
</div>
</section>
<!-- ===== KAPITEL 6 ===== -->
<section class="chapter">
<div class="chapter-head">
<div class="chapter-num">06</div>
<div class="chapter-title">
<h1>OpCache und Preloading</h1>
<div class="subtitle">Code-Compilation einmal, nicht pro Request</div>
</div>
</div>
<div class="gap">
<b>Frage zum Einstieg</b>
PHP parst und kompiliert standardmäßig deinen Code <i>bei jedem Request</i> neu. Bei einer Laravel-App mit hunderten Dateien sind das viele Millisekunden Overhead pro Request. <b>OpCache</b> und <b>Preloading</b> eliminieren das.
</div>
<h2>OpCache aktivieren</h2>
<p>OpCache ist ab PHP 5.5 dabei, muss aber in <code class="inline">php.ini</code> aktiviert werden:</p>
<pre><span class="c"># php.ini</span>
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1 <span class="c"># Dev: 1 (prüft Änderungen)</span>
opcache.revalidate_freq=0 <span class="c"># Prod: 0 (kein Re-Check)</span>
opcache.jit_buffer_size=128M <span class="c"># PHP 8 JIT</span>
opcache.jit=tracing</pre>
<p>OpCache cached die kompilierten <b>OpCodes</b> im Shared Memory. Beim nächsten Request wird der gecachte OpCode direkt ausgeführt ohne Parse und Compile.</p>
<h2>Validate Timestamps in Production</h2>
<p>In Development willst du Code-Änderungen sofort sehen (<code class="inline">validate_timestamps=1</code>). In Production sind File-Stat-Calls bei jedem Request unnötiger Overhead deshalb <code class="inline">validate_timestamps=0</code>:</p>
<pre><span class="c"># Bei Deployment manuell reload</span>
service php-fpm reload
<span class="c"># oder</span>
opcache_reset() <span class="c"># im PHP-Code</span>
<span class="c"># oder cache file/api</span>
curl http://localhost/opcache-reset.php</pre>
<h2>OpCache-Status zur Laufzeit</h2>
<pre><span class="v">$status</span> = <span class="f">opcache_get_status</span>();
<span class="v">$status</span>[<span class="s">'opcache_statistics'</span>][<span class="s">'hits'</span>]; <span class="c">// Cache Hits</span>
<span class="v">$status</span>[<span class="s">'opcache_statistics'</span>][<span class="s">'misses'</span>]; <span class="c">// Misses</span>
<span class="v">$status</span>[<span class="s">'memory_usage'</span>][<span class="s">'used_memory'</span>]; <span class="c">// Speicher</span>
<span class="v">$status</span>[<span class="s">'memory_usage'</span>][<span class="s">'free_memory'</span>]; <span class="c">// frei</span>
<span class="c">// Bei voller Speicher-Auslastung: opcache.memory_consumption erhöhen</span></pre>
<h2>Preloading: noch eine Stufe weiter</h2>
<p>Seit PHP 7.4 gibt es <b>Preloading</b>: beim Start des PHP-Prozesses werden Klassen einmal komplett geladen und stehen dann in <i>jedem</i> Request sofort zur Verfügung ohne OpCache-Lookup:</p>
<pre><span class="c"># php.ini</span>
opcache.preload=/var/www/preload.php
opcache.preload_user=www-data</pre>
<pre><span class="c">// preload.php</span>
<span class="t">&lt;?php</span>
<span class="k">require</span> <span class="s">'/var/www/vendor/autoload.php'</span>;
<span class="c">// Lade alle Klassen aus src/</span>
<span class="k">foreach</span> (<span class="k">new</span> \<span class="t">RecursiveIteratorIterator</span>(
<span class="k">new</span> \<span class="t">RecursiveDirectoryIterator</span>(<span class="s">'/var/www/src'</span>)
) <span class="k">as</span> <span class="v">$file</span>) {
<span class="k">if</span> (<span class="v">$file</span>-&gt;<span class="f">getExtension</span>() === <span class="s">'php'</span>) {
<span class="f">opcache_compile_file</span>(<span class="v">$file</span>-&gt;<span class="f">getRealPath</span>());
}
}</pre>
<p>Effekt: in Laravel- und Symfony-Apps oft 20-30% schnellere Requests, vor allem bei kleinen Aktionen.</p>
<h2>JIT (Just-In-Time)</h2>
<p>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:</p>
<table>
<tr><th>Use Case</th><th>Speedup mit JIT</th></tr>
<tr><td>Web (Database/Network-bound)</td><td>~5-10%</td></tr>
<tr><td>CLI-Tools, Compute-bound</td><td>~30-50%</td></tr>
<tr><td>Mandelbrot/Mathematik</td><td>~2-3x</td></tr>
</table>
<div class="callout note">
<div class="callout-icon">i</div>
<div class="callout-body">
<b>Preloading-Limitationen</b>
Geladene Klassen können zur Laufzeit <b>nicht mehr geändert</b> werden. Wenn dein Framework Klassen dynamisch erweitert (z.B. Symfony Cache-Container), führt das zu Konflikten. Reload bei Deployment ist Pflicht.
</div>
</div>
<div class="recall">
<b>Recall</b>
<ol>
<li>Was cached OpCache genau?</li>
<li>Was ist der Unterschied zwischen <code style="color:#777BB4">validate_timestamps=1</code> und <code style="color:#777BB4">0</code>?</li>
<li>Wann lohnt sich JIT besonders?</li>
</ol>
</div>
</section>
<!-- ===== KAPITEL 7 ===== -->
<section class="chapter">
<div class="chapter-head">
<div class="chapter-num">07</div>
<div class="chapter-title">
<h1>Fibers - Cooperative Concurrency</h1>
<div class="subtitle">Async ohne Threading (PHP 8.1+)</div>
</div>
</div>
<div class="gap">
<b>Frage zum Einstieg</b>
PHP ist klassisch single-threaded und blockierend. Aber moderne Apps brauchen oft parallel laufende HTTP-Calls, gleichzeitige DB-Queries, Streaming. <b>Fibers</b> in PHP 8.1 bringen kooperative Concurrency die Basis für moderne async Libraries.
</div>
<h2>Was ist eine Fiber?</h2>
<p>Eine <b>Fiber</b> ist eine "leichtgewichtige" Coroutine: sie hat ihren eigenen Stack, kann pausieren und später fortgesetzt werden. Anders als Threads laufen sie nicht parallel, sondern <b>kooperativ</b> sie geben Kontrolle freiwillig ab:</p>
<pre><span class="v">$fiber</span> = <span class="k">new</span> \<span class="t">Fiber</span>(<span class="k">function</span>(): <span class="t">void</span> {
<span class="k">echo</span> <span class="s">"Fiber gestartet\n"</span>;
\<span class="t">Fiber</span>::<span class="f">suspend</span>(<span class="s">'pause-wert'</span>); <span class="c">// pausieren</span>
<span class="k">echo</span> <span class="s">"Fiber fortgesetzt\n"</span>;
});
<span class="v">$value</span> = <span class="v">$fiber</span>-&gt;<span class="f">start</span>(); <span class="c">// 'pause-wert'</span>
<span class="k">echo</span> <span class="s">"In Main: </span><span class="v">$value</span><span class="s">\n"</span>;
<span class="v">$fiber</span>-&gt;<span class="f">resume</span>(<span class="s">'antwort'</span>); <span class="c">// Fiber läuft weiter</span></pre>
<p>Ausgabe:</p>
<pre>Fiber gestartet
In Main: pause-wert
Fiber fortgesetzt</pre>
<h2>Async-I/O mit Fibers</h2>
<p>Klassischer Use Case: HTTP-Requests parallel. Statt zu blockieren, suspendiert die Fiber während sie wartet:</p>
<pre><span class="k">function</span> <span class="f">fetchUrl</span>(<span class="t">string</span> <span class="v">$url</span>): <span class="t">string</span> {
<span class="k">return</span> <span class="k">new</span> \<span class="t">Fiber</span>(<span class="k">function</span>() <span class="k">use</span> (<span class="v">$url</span>) {
<span class="v">$ch</span> = <span class="f">curl_init</span>(<span class="v">$url</span>);
<span class="f">curl_setopt</span>(<span class="v">$ch</span>, CURLOPT_RETURNTRANSFER, <span class="k">true</span>);
<span class="c">// Während wir warten: andere Fibers können laufen</span>
\<span class="t">Fiber</span>::<span class="f">suspend</span>();
<span class="k">return</span> <span class="f">curl_exec</span>(<span class="v">$ch</span>);
});
}</pre>
<p>Das einfache Beispiel hier zeigt nur das Konzept in der Praxis brauchst du eine <b>Scheduler/Event-Loop</b>, die mehrere Fibers verwaltet. Frameworks wie ReactPHP und Amp bieten das.</p>
<h2>Amp 3.0 mit Fibers</h2>
<p>Amp (revierschiff in PHP-Async-Welt) nutzt Fibers in Version 3.0 für eine viel saubere API als die alten Promise-basierten Patterns:</p>
<pre><span class="k">use function</span> Amp\<span class="f">async</span>;
<span class="k">use function</span> Amp\Future\<span class="f">await</span>;
<span class="c">// Drei parallele HTTP-Requests</span>
<span class="v">$results</span> = <span class="f">await</span>([
<span class="f">async</span>(<span class="f">fetchUrl</span>(...), <span class="s">'https://api.a.com'</span>),
<span class="f">async</span>(<span class="f">fetchUrl</span>(...), <span class="s">'https://api.b.com'</span>),
<span class="f">async</span>(<span class="f">fetchUrl</span>(...), <span class="s">'https://api.c.com'</span>),
]);
<span class="c">// Hier sind alle drei Responses verfügbar</span>
<span class="k">foreach</span> (<span class="v">$results</span> <span class="k">as</span> <span class="v">$response</span>) {
<span class="c">// ...</span>
}</pre>
<p>Im Hintergrund läuft eine Event-Loop, die alle Fibers verwaltet und sie aufweckt, wenn ihre I/O fertig ist.</p>
<h2>Fibers vs. echte Threads</h2>
<table>
<tr><th>Aspekt</th><th>Fibers</th><th>Threads (parallel)</th></tr>
<tr><td>Parallelität</td><td>Nein, kooperativ</td><td>Ja, true parallel</td></tr>
<tr><td>Memory pro Unit</td><td>~8 KB</td><td>~MB</td></tr>
<tr><td>Context-Switch</td><td>Sehr schnell</td><td>OS-Overhead</td></tr>
<tr><td>Shared State</td><td>Direkt zugreifbar</td><td>Locking nötig</td></tr>
<tr><td>Use Case</td><td>I/O-bound</td><td>CPU-bound</td></tr>
</table>
<p>Für klassische Web-Apps (viele I/O-Calls, wenig CPU) sind Fibers fast immer die richtige Wahl. Wenn du <i>echtes</i> Parallel-Processing brauchst (Bildverarbeitung, Berechnungen), nutzt du PHP <code class="inline">parallel</code>-Extension oder lagerst es in separate Prozesse aus.</p>
<div class="callout tip">
<div class="callout-icon">✓</div>
<div class="callout-body">
<b>Symfony 7 mit Fibers</b>
Symfony 7+ nutzt intern Fibers für HttpClient (Multi-Request) und Messenger (parallele Message-Verarbeitung). Auch ohne explizit Fibers zu schreiben profitierst du dadurch.
</div>
</div>
<div class="recall">
<b>Recall</b>
<ol>
<li>Was ist der Hauptunterschied zwischen Fiber und Thread?</li>
<li>Wann gibt eine Fiber Kontrolle ab?</li>
<li>Wofür braucht es zusätzlich eine Event-Loop wie in Amp?</li>
</ol>
</div>
</section>
<!-- ===== KAPITEL 8 ===== -->
<section class="chapter">
<div class="chapter-head">
<div class="chapter-num">08</div>
<div class="chapter-title">
<h1>ReactPHP &amp; Event-Loops</h1>
<div class="subtitle">Long-Running Server, non-blocking I/O</div>
</div>
</div>
<div class="gap">
<b>Frage zum Einstieg</b>
PHP klassisch: ein Request, ein Prozess, Sterben am Ende. Aber WebSocket-Server, Pub/Sub-Workers oder Streaming-APIs brauchen <b>Long-Running-Prozesse</b>. <b>ReactPHP</b> bringt das Event-Loop-Modell (wie Node.js) in die PHP-Welt.
</div>
<h2>Event-Loop-Grundprinzip</h2>
<p>Eine Event-Loop ist eine Endlos-Schleife, die <b>Events</b> aus verschiedenen Quellen (Sockets, Timer, Streams) liest und Handler aufruft. I/O-Operationen blockieren nicht sie geben Promises/Callbacks zurück.</p>
<pre><span class="k">use</span> React\EventLoop\Loop;
<span class="c">// Timer: nach 2 Sekunden, einmal</span>
Loop::<span class="f">addTimer</span>(<span class="s">2.0</span>, <span class="k">function</span>() {
<span class="k">echo</span> <span class="s">"Nach 2 Sekunden\n"</span>;
});
<span class="c">// Periodisch alle 5 Sekunden</span>
Loop::<span class="f">addPeriodicTimer</span>(<span class="s">5.0</span>, <span class="k">function</span>() {
<span class="k">echo</span> <span class="s">"Alle 5 Sekunden\n"</span>;
});
<span class="c">// Event-Loop starten (blockierend bis stop)</span>
Loop::<span class="f">run</span>();</pre>
<h2>HTTP-Server mit ReactPHP</h2>
<p>Ein vollständiger HTTP-Server in wenigen Zeilen ohne Apache/nginx davor:</p>
<pre><span class="k">use</span> React\Http\HttpServer;
<span class="k">use</span> React\Http\Message\Response;
<span class="k">use</span> React\Socket\SocketServer;
<span class="k">use</span> Psr\Http\Message\ServerRequestInterface;
<span class="v">$http</span> = <span class="k">new</span> HttpServer(<span class="k">function</span>(ServerRequestInterface <span class="v">$request</span>) {
<span class="k">return</span> Response::<span class="f">plaintext</span>(<span class="s">"Hello, "</span> . <span class="v">$request</span>-&gt;<span class="f">getUri</span>()-&gt;<span class="f">getPath</span>());
});
<span class="v">$socket</span> = <span class="k">new</span> SocketServer(<span class="s">'0.0.0.0:8080'</span>);
<span class="v">$http</span>-&gt;<span class="f">listen</span>(<span class="v">$socket</span>);
<span class="k">echo</span> <span class="s">"Server läuft auf http://localhost:8080\n"</span>;
<span class="c">// Event-Loop läuft implizit weiter</span></pre>
<p>Vorteil gegenüber php-fpm: <b>State bleibt zwischen Requests</b> erhalten. Datenbankverbindungen, Caches, geladene Klassen werden einmal aufgebaut. Bei vielen kleinen Requests ist das deutlich schneller.</p>
<h2>Streams und Pipes</h2>
<pre><span class="k">use</span> React\Stream\ReadableResourceStream;
<span class="k">use</span> React\Stream\WritableResourceStream;
<span class="v">$stdin</span> = <span class="k">new</span> ReadableResourceStream(STDIN);
<span class="v">$stdout</span> = <span class="k">new</span> WritableResourceStream(STDOUT);
<span class="v">$stdin</span>-&gt;<span class="f">on</span>(<span class="s">'data'</span>, <span class="k">function</span>(<span class="v">$chunk</span>) <span class="k">use</span> (<span class="v">$stdout</span>) {
<span class="v">$stdout</span>-&gt;<span class="f">write</span>(<span class="f">strtoupper</span>(<span class="v">$chunk</span>));
});
<span class="v">$stdin</span>-&gt;<span class="f">on</span>(<span class="s">'end'</span>, <span class="k">function</span>() {
<span class="k">echo</span> <span class="s">"\nInput beendet\n"</span>;
});</pre>
<h2>WebSocket-Server mit Ratchet</h2>
<p><b>Ratchet</b> baut auf ReactPHP auf und bringt WebSocket-Support:</p>
<pre><span class="k">use</span> Ratchet\Server\IoServer;
<span class="k">use</span> Ratchet\WebSocket\WsServer;
<span class="k">use</span> Ratchet\MessageComponentInterface;
<span class="k">use</span> Ratchet\ConnectionInterface;
<span class="k">class</span> <span class="t">ChatServer</span> <span class="k">implements</span> MessageComponentInterface {
<span class="k">protected</span> \<span class="t">SplObjectStorage</span> <span class="v">$clients</span>;
<span class="k">public function</span> <span class="f">__construct</span>() {
<span class="v">$this</span>-&gt;clients = <span class="k">new</span> \<span class="t">SplObjectStorage</span>();
}
<span class="k">public function</span> <span class="f">onOpen</span>(ConnectionInterface <span class="v">$conn</span>) {
<span class="v">$this</span>-&gt;clients-&gt;<span class="f">attach</span>(<span class="v">$conn</span>);
}
<span class="k">public function</span> <span class="f">onMessage</span>(ConnectionInterface <span class="v">$from</span>, <span class="v">$msg</span>) {
<span class="k">foreach</span> (<span class="v">$this</span>-&gt;clients <span class="k">as</span> <span class="v">$client</span>) {
<span class="k">if</span> (<span class="v">$client</span> !== <span class="v">$from</span>) <span class="v">$client</span>-&gt;<span class="f">send</span>(<span class="v">$msg</span>);
}
}
<span class="c">// onClose, onError ...</span>
}
<span class="v">$server</span> = IoServer::<span class="f">factory</span>(<span class="k">new</span> WsServer(<span class="k">new</span> <span class="t">ChatServer</span>()), <span class="s">8081</span>);
<span class="v">$server</span>-&gt;<span class="f">run</span>();</pre>
<h2>Wann ReactPHP, wann klassisch?</h2>
<table>
<tr><th>Use Case</th><th>Wähle</th></tr>
<tr><td>Klassische CRUD-App</td><td>php-fpm + Symfony/Laravel</td></tr>
<tr><td>WebSocket-Server</td><td>ReactPHP + Ratchet</td></tr>
<tr><td>Pub/Sub-Worker</td><td>ReactPHP oder Symfony Messenger</td></tr>
<tr><td>Streaming-API</td><td>ReactPHP</td></tr>
<tr><td>Tausende kleine Microservice-Calls</td><td>ReactPHP (Connection-Reuse)</td></tr>
</table>
<div class="callout warn">
<div class="callout-icon">!</div>
<div class="callout-body">
<b>Blockierender Code killt die Event-Loop</b>
Ein einziger <code class="inline">sleep(10)</code>, <code class="inline">file_get_contents()</code> auf eine langsame URL, oder ein synchrones <code class="inline">PDO::query()</code> blockiert die <b>gesamte Event-Loop</b>. Alle Connections frieren ein. In ReactPHP musst du konsequent non-blocking I/O nutzen.
</div>
</div>
<div class="recall">
<b>Recall</b>
<ol>
<li>Was ist der Hauptunterschied zwischen php-fpm und ReactPHP?</li>
<li>Welches Pattern wäre für einen WebSocket-Chat geeignet?</li>
<li>Warum sind blockierende Calls in ReactPHP tabu?</li>
</ol>
</div>
</section>
<!-- ===== KAPITEL 9 ===== -->
<section class="chapter">
<div class="chapter-head">
<div class="chapter-num">09</div>
<div class="chapter-title">
<h1>Profiling mit Blackfire</h1>
<div class="subtitle">CPU- und Memory-Hotspots finden</div>
</div>
</div>
<div class="gap">
<b>Frage zum Einstieg</b>
Deine App ist langsam aber wo genau? Der Controller? Die DB-Query? Die View-Rendering? Raten endet im Polieren der falschen Stellen. <b>Blackfire</b> und <b>Xdebug Profiler</b> zeigen dir Call-Graphs mit echten Messzahlen.
</div>
<h2>Xdebug-Profiler (gratis)</h2>
<p>Xdebug bringt einen einfachen Profiler mit. In <code class="inline">php.ini</code>:</p>
<pre>xdebug.mode=profile
xdebug.start_with_request=trigger
xdebug.output_dir=/tmp/xdebug</pre>
<p>Mit <code class="inline">?XDEBUG_TRIGGER=1</code> als Query-Parameter aktivierst du das Profiling für einen Request. Ergebnis: eine Cachegrind-Datei in <code class="inline">/tmp/xdebug/</code>, die du mit <b>KCacheGrind</b> (Linux) oder <b>QCacheGrind</b> (macOS) öffnest.</p>
<h2>Blackfire (professionell)</h2>
<p><b>Blackfire</b> ist ein kommerzieller Profiler mit Web-UI, Vergleichen, Regressions-Detection und Production-Profiling. Setup: Agent installieren, Probe als PHP-Extension, dann profilen:</p>
<pre><span class="c"># CLI-Script profilen</span>
blackfire run php script.php
<span class="c"># Eine bestimmte URL</span>
blackfire curl https://example.com/slow-page
<span class="c"># Mit Iterationen (Mittelwert)</span>
blackfire --samples=10 curl https://example.com/page</pre>
<p>Das Ergebnis ist ein <b>Call-Graph</b>: jede Funktion mit ihrer Zeit, wer sie aufruft, wie oft, welcher Anteil an der Gesamtzeit.</p>
<h2>Im Code instrumentieren</h2>
<pre><span class="k">use</span> Blackfire\Client;
<span class="k">use</span> Blackfire\Profile\Configuration;
<span class="v">$blackfire</span> = <span class="k">new</span> Client();
<span class="v">$config</span> = (<span class="k">new</span> Configuration())-&gt;<span class="f">setTitle</span>(<span class="s">'Order Processing'</span>);
<span class="v">$probe</span> = <span class="v">$blackfire</span>-&gt;<span class="f">createProbe</span>(<span class="v">$config</span>);
<span class="c">// Code, der gemessen werden soll</span>
<span class="v">$service</span>-&gt;<span class="f">processOrders</span>(<span class="v">$orders</span>);
<span class="v">$blackfire</span>-&gt;<span class="f">endProbe</span>(<span class="v">$probe</span>);</pre>
<h2>Typische Findings</h2>
<table>
<tr><th>Pattern</th><th>Wie oft</th><th>Lösung</th></tr>
<tr><td>N+1 Query</td><td>fast jede App</td><td>Eager-Loading</td></tr>
<tr><td>JSON-Encoding mehrfach</td><td>oft</td><td>Cache</td></tr>
<tr><td>Twig ohne Cache</td><td>häufig</td><td>opcache.preload</td></tr>
<tr><td>Doctrine: gleiche Entity 100x geladen</td><td>häufig</td><td>Identity Map</td></tr>
<tr><td>Composer Autoload langsam</td><td>oft</td><td>composer dump-autoload -o</td></tr>
</table>
<h2>Memory-Profiling</h2>
<p>Memory-Leaks sind oft schlimmer als CPU-Probleme. <code class="inline">memory_get_peak_usage()</code> gibt dir die maximale Speichernutzung eines Requests:</p>
<pre><span class="k">$start</span> = <span class="f">memory_get_usage</span>();
<span class="f">processLargeData</span>();
<span class="k">$end</span> = <span class="f">memory_get_usage</span>();
<span class="k">$peak</span> = <span class="f">memory_get_peak_usage</span>();
<span class="k">echo</span> <span class="s">"Used: "</span> . (<span class="v">$end</span> - <span class="v">$start</span>) . <span class="s">" bytes\n"</span>;
<span class="k">echo</span> <span class="s">"Peak: </span><span class="v">$peak</span><span class="s"> bytes\n"</span>;</pre>
<p>Für tieferes Memory-Profiling: <code class="inline">php-meminfo</code> (Extension) oder Blackfire's Memory-Profile.</p>
<div class="callout tip">
<div class="callout-icon">✓</div>
<div class="callout-body">
<b>Misst, was zählt nicht alles</b>
Optimiere nicht blind nach Profiler-Ausgabe. Eine Funktion, die 30% der Zeit braucht, aber nur einmal pro Tag läuft, ist weniger wichtig als eine, die 5% braucht, aber 1000 mal pro Minute. Schau dir <b>absolute Zahlen × Frequenz</b> an, nicht nur Prozent.
</div>
</div>
<div class="recall">
<b>Recall</b>
<ol>
<li>Welcher Profiler ist gratis dabei?</li>
<li>Was ist ein N+1 Query und warum häufig?</li>
<li>Wofür ist <code style="color:#777BB4">memory_get_peak_usage()</code>?</li>
</ol>
</div>
</section>
<!-- ===== KAPITEL 10 ===== -->
<section class="chapter">
<div class="chapter-head">
<div class="chapter-num">10</div>
<div class="chapter-title">
<h1>Caching-Strategien</h1>
<div class="subtitle">APCu, Redis, HTTP-Cache, ESI</div>
</div>
</div>
<div class="gap">
<b>Frage zum Einstieg</b>
Eine Datenbank-Query, die 200ms dauert. Eine Berechnung, die immer wieder dasselbe Ergebnis liefert. Ein externer API-Call, der teuer ist. <b>Caching</b> ist meist die größte Performance-Optimierung aber wo cachen und wie invalidieren?
</div>
<h2>Die Caching-Schichten</h2>
<table>
<tr><th>Schicht</th><th>Latenz</th><th>Tech</th></tr>
<tr><td>OpCache (kompilierter Code)</td><td>~0ms</td><td>eingebaut</td></tr>
<tr><td>Request-Cache (in-memory)</td><td>~0ms</td><td>Arrays</td></tr>
<tr><td>APCu (shared memory)</td><td>~0.01ms</td><td>APCu Extension</td></tr>
<tr><td>Redis (lokal)</td><td>~0.5ms</td><td>Redis-Server</td></tr>
<tr><td>Redis (Netzwerk)</td><td>~1-5ms</td><td>remote Redis</td></tr>
<tr><td>Datenbank-Query</td><td>~5-100ms</td><td>MySQL/Postgres</td></tr>
<tr><td>Externe API</td><td>~50-1000ms</td><td>HTTP</td></tr>
</table>
<p>Jede Schicht hinunter ist 10-100x langsamer. Die Kunst: so weit oben wie möglich cachen, ohne stale Daten zu liefern.</p>
<h2>PSR-6 / PSR-16 Cache</h2>
<p>Die PHP-FIG hat Cache-Interfaces standardisiert. Sym­fo­ny Cache, Laravel Cache und Doctrine Cache implementieren sie. Dein Code bleibt agnostisch:</p>
<pre><span class="k">use</span> Symfony\Contracts\Cache\CacheInterface;
<span class="k">use</span> Symfony\Contracts\Cache\ItemInterface;
<span class="k">class</span> <span class="t">UserRepository</span> {
<span class="k">public function</span> <span class="f">__construct</span>(<span class="k">private</span> CacheInterface <span class="v">$cache</span>) {}
<span class="k">public function</span> <span class="f">find</span>(<span class="t">int</span> <span class="v">$id</span>): ?<span class="t">User</span> {
<span class="k">return</span> <span class="v">$this</span>-&gt;cache-&gt;<span class="f">get</span>(<span class="s">"user.</span><span class="v">$id</span><span class="s">"</span>, <span class="k">function</span>(ItemInterface <span class="v">$item</span>) <span class="k">use</span> (<span class="v">$id</span>) {
<span class="v">$item</span>-&gt;<span class="f">expiresAfter</span>(<span class="s">3600</span>); <span class="c">// 1 Stunde</span>
<span class="k">return</span> <span class="v">$this</span>-&gt;<span class="f">loadFromDb</span>(<span class="v">$id</span>);
});
}
}</pre>
<h2>APCu für Request-übergreifende Caches</h2>
<p><b>APCu</b> ist Shared Memory pro PHP-Server-Prozess. Sehr schnell, aber:</p>
<ul>
<li>Pro Server-Maschine nicht über mehrere Server verteilt</li>
<li>Daten gehen bei PHP-FPM-Reload verloren</li>
<li>Begrenzte Größe (Standard: 32MB)</li>
</ul>
<pre><span class="c">// Direkte API</span>
<span class="f">apcu_store</span>(<span class="s">'key'</span>, <span class="v">$data</span>, <span class="s">300</span>); <span class="c">// 5 Min TTL</span>
<span class="v">$data</span> = <span class="f">apcu_fetch</span>(<span class="s">'key'</span>);
<span class="c">// Atomic increment für Counter</span>
<span class="v">$hits</span> = <span class="f">apcu_inc</span>(<span class="s">'hits:home'</span>);</pre>
<h2>Redis für verteilte Caches</h2>
<pre><span class="v">$redis</span> = <span class="k">new</span> \<span class="t">Redis</span>();
<span class="v">$redis</span>-&gt;<span class="f">connect</span>(<span class="s">'redis-server'</span>, <span class="s">6379</span>);
<span class="c">// Einfacher Key-Value</span>
<span class="v">$redis</span>-&gt;<span class="f">set</span>(<span class="s">'user:42'</span>, <span class="f">json_encode</span>(<span class="v">$user</span>), <span class="s">3600</span>);
<span class="v">$cached</span> = <span class="f">json_decode</span>(<span class="v">$redis</span>-&gt;<span class="f">get</span>(<span class="s">'user:42'</span>), <span class="k">true</span>);
<span class="c">// Pipeline für Batch-Operationen</span>
<span class="v">$pipe</span> = <span class="v">$redis</span>-&gt;<span class="f">multi</span>(<span class="t">Redis</span>::PIPELINE);
<span class="k">foreach</span> (<span class="v">$ids</span> <span class="k">as</span> <span class="v">$id</span>) {
<span class="v">$pipe</span>-&gt;<span class="f">get</span>(<span class="s">"user:</span><span class="v">$id</span><span class="s">"</span>);
}
<span class="v">$results</span> = <span class="v">$pipe</span>-&gt;<span class="f">exec</span>(); <span class="c">// alle in einem Roundtrip</span></pre>
<h2>HTTP-Cache und ESI</h2>
<p>Für komplette Seiten oder Fragmente bietet sich HTTP-Caching an. <code class="inline">Cache-Control</code> und <code class="inline">ETag</code> sagen Browser und CDNs, wann sie nicht neu fragen müssen:</p>
<pre><span class="c">// Symfony Response</span>
<span class="v">$response</span>-&gt;<span class="f">setPublic</span>();
<span class="v">$response</span>-&gt;<span class="f">setMaxAge</span>(<span class="s">3600</span>);
<span class="v">$response</span>-&gt;<span class="f">setSharedMaxAge</span>(<span class="s">86400</span>); <span class="c">// CDN cached 1 Tag</span>
<span class="v">$response</span>-&gt;<span class="f">setEtag</span>(<span class="f">md5</span>(<span class="v">$content</span>));</pre>
<p><b>ESI (Edge Side Includes)</b> 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.</p>
<h2>Cache-Invalidierung</h2>
<p>Das schwerste Problem. Strategien:</p>
<ul>
<li><b>TTL</b> einfach, aber kann stale sein</li>
<li><b>Manuell</b> bei Schreib-Operationen fehlt leicht eine Stelle</li>
<li><b>Tags</b> Cache-Items haben Labels, du invalidierst per Label</li>
<li><b>Event-driven</b> Listener auf Domain-Events räumen Cache auf</li>
</ul>
<div class="callout note">
<div class="callout-icon">i</div>
<div class="callout-body">
<b>"Es gibt zwei schwere Probleme in der Informatik..."</b>
Cache-Invalidierung und Naming. Beide passieren immer. Akzeptiere, dass dein Cache manchmal stale ist, baue Monitoring ein, und plane Strategien für Invalidierung von Anfang an mit.
</div>
</div>
<div class="recall">
<b>Recall</b>
<ol>
<li>Wann nutzt du APCu, wann Redis?</li>
<li>Was macht ESI in Varnish?</li>
<li>Welche Strategien für Cache-Invalidierung kennst du?</li>
</ol>
</div>
</section>
<!-- ===== KAPITEL 11 ===== -->
<section class="chapter">
<div class="chapter-head">
<div class="chapter-num">11</div>
<div class="chapter-title">
<h1>Dependency Injection Container</h1>
<div class="subtitle">Eigener Container in 100 Zeilen</div>
</div>
</div>
<div class="gap">
<b>Frage zum Einstieg</b>
Symfony Container, Laravel Container, PHP-DI alle lösen dasselbe Problem: Klassen automatisch instanziieren mit den richtigen Abhängigkeiten. Wie funktioniert das intern? Ein eigener DI-Container in 100 Zeilen Code zeigt das Prinzip.
</div>
<h2>Was ein DI-Container tut</h2>
<p>Du sagst dem Container: "Gib mir eine Instanz von <code class="inline">OrderService</code>". Er liest die Konstruktor-Signatur, sieht "braucht <code class="inline">PaymentGateway</code> und <code class="inline">EmailSender</code>", instanziiert die <i>auch</i> automatisch und übergibt sie:</p>
<pre><span class="k">class</span> <span class="t">OrderService</span> {
<span class="k">public function</span> <span class="f">__construct</span>(
<span class="k">private</span> PaymentGateway <span class="v">$gateway</span>,
<span class="k">private</span> EmailSender <span class="v">$mailer</span>,
) {}
}
<span class="c">// Container kümmert sich um alles</span>
<span class="v">$container</span> = <span class="k">new</span> <span class="t">Container</span>();
<span class="v">$service</span> = <span class="v">$container</span>-&gt;<span class="f">get</span>(OrderService::<span class="k">class</span>);</pre>
<h2>Minimaler Container</h2>
<p>Hier ein vereinfachter Container, der Klassen über Reflection auflöst. Production-Container haben mehr Features, das Kern-Prinzip bleibt:</p>
<pre><span class="k">class</span> <span class="t">Container</span> {
<span class="k">private array</span> <span class="v">$bindings</span> = [];
<span class="k">private array</span> <span class="v">$instances</span> = [];
<span class="k">public function</span> <span class="f">bind</span>(<span class="t">string</span> <span class="v">$abstract</span>, callable|<span class="t">string</span> <span class="v">$concrete</span>): <span class="t">void</span> {
<span class="v">$this</span>-&gt;bindings[<span class="v">$abstract</span>] = <span class="v">$concrete</span>;
}
<span class="k">public function</span> <span class="f">singleton</span>(<span class="t">string</span> <span class="v">$abstract</span>, callable|<span class="t">string</span> <span class="v">$concrete</span>): <span class="t">void</span> {
<span class="v">$this</span>-&gt;<span class="f">bind</span>(<span class="v">$abstract</span>, <span class="v">$concrete</span>);
<span class="v">$this</span>-&gt;instances[<span class="v">$abstract</span>] = <span class="k">null</span>;
}
<span class="k">public function</span> <span class="f">get</span>(<span class="t">string</span> <span class="v">$id</span>): <span class="t">object</span> {
<span class="c">// Singleton-Cache</span>
<span class="k">if</span> (<span class="f">array_key_exists</span>(<span class="v">$id</span>, <span class="v">$this</span>-&gt;instances) &amp;&amp; <span class="v">$this</span>-&gt;instances[<span class="v">$id</span>]) {
<span class="k">return</span> <span class="v">$this</span>-&gt;instances[<span class="v">$id</span>];
}
<span class="v">$instance</span> = <span class="v">$this</span>-&gt;<span class="f">resolve</span>(<span class="v">$id</span>);
<span class="k">if</span> (<span class="f">array_key_exists</span>(<span class="v">$id</span>, <span class="v">$this</span>-&gt;instances)) {
<span class="v">$this</span>-&gt;instances[<span class="v">$id</span>] = <span class="v">$instance</span>;
}
<span class="k">return</span> <span class="v">$instance</span>;
}
<span class="k">private function</span> <span class="f">resolve</span>(<span class="t">string</span> <span class="v">$id</span>): <span class="t">object</span> {
<span class="v">$concrete</span> = <span class="v">$this</span>-&gt;bindings[<span class="v">$id</span>] ?? <span class="v">$id</span>;
<span class="k">if</span> (<span class="f">is_callable</span>(<span class="v">$concrete</span>)) {
<span class="k">return</span> <span class="v">$concrete</span>(<span class="v">$this</span>);
}
<span class="v">$reflection</span> = <span class="k">new</span> \<span class="t">ReflectionClass</span>(<span class="v">$concrete</span>);
<span class="v">$constructor</span> = <span class="v">$reflection</span>-&gt;<span class="f">getConstructor</span>();
<span class="k">if</span> (!<span class="v">$constructor</span>) {
<span class="k">return new</span> <span class="v">$concrete</span>();
}
<span class="v">$args</span> = [];
<span class="k">foreach</span> (<span class="v">$constructor</span>-&gt;<span class="f">getParameters</span>() <span class="k">as</span> <span class="v">$param</span>) {
<span class="v">$type</span> = <span class="v">$param</span>-&gt;<span class="f">getType</span>();
<span class="k">if</span> (<span class="v">$type</span> &amp;&amp; !<span class="v">$type</span>-&gt;<span class="f">isBuiltin</span>()) {
<span class="v">$args</span>[] = <span class="v">$this</span>-&gt;<span class="f">get</span>(<span class="v">$type</span>-&gt;<span class="f">getName</span>());
} <span class="k">elseif</span> (<span class="v">$param</span>-&gt;<span class="f">isDefaultValueAvailable</span>()) {
<span class="v">$args</span>[] = <span class="v">$param</span>-&gt;<span class="f">getDefaultValue</span>();
} <span class="k">else</span> {
<span class="k">throw new</span> \<span class="t">Exception</span>(<span class="s">"Cannot resolve </span><span class="v">$id</span><span class="s">"</span>);
}
}
<span class="k">return</span> <span class="v">$reflection</span>-&gt;<span class="f">newInstanceArgs</span>(<span class="v">$args</span>);
}
}</pre>
<h2>Bindings konfigurieren</h2>
<pre><span class="v">$container</span> = <span class="k">new</span> <span class="t">Container</span>();
<span class="c">// Interface → konkrete Klasse</span>
<span class="v">$container</span>-&gt;<span class="f">bind</span>(LoggerInterface::<span class="k">class</span>, FileLogger::<span class="k">class</span>);
<span class="c">// Factory-Closure</span>
<span class="v">$container</span>-&gt;<span class="f">bind</span>(\<span class="t">PDO</span>::<span class="k">class</span>, <span class="k">fn</span>() =&gt;
<span class="k">new</span> \<span class="t">PDO</span>(<span class="s">'mysql:host=localhost'</span>, <span class="s">'user'</span>, <span class="s">'pass'</span>)
);
<span class="c">// Singleton (gleiche Instanz für alle Calls)</span>
<span class="v">$container</span>-&gt;<span class="f">singleton</span>(CacheInterface::<span class="k">class</span>, RedisCache::<span class="k">class</span>);
<span class="c">// Auflösen</span>
<span class="v">$service</span> = <span class="v">$container</span>-&gt;<span class="f">get</span>(OrderService::<span class="k">class</span>);
<span class="c">// Container baut automatisch: PaymentGateway, EmailSender, deren Dependencies</span></pre>
<h2>Production-Container-Features</h2>
<p>Echte Container haben noch viel mehr:</p>
<ul>
<li><b>Compile-Time-Optimierung</b> Bindings werden zu generiertem PHP-Code (Symfony)</li>
<li><b>Auto-Wiring</b> kein explizites Binding nötig, wenn Class-Name = Interface</li>
<li><b>Tags</b> Services mit Tags wie "event_listener" automatisch sammeln</li>
<li><b>Lazy-Loading</b> Services werden erst beim ersten Zugriff erzeugt</li>
<li><b>Parameter</b> Konfigurations-Werte (Strings, Arrays) als Konstanten injizieren</li>
</ul>
<div class="callout tip">
<div class="callout-icon">✓</div>
<div class="callout-body">
<b>Nutze einen ausgereiften Container</b>
Für eigene Projekte: <b>PHP-DI</b> oder <b>Symfony DependencyInjection</b>. Eigene Container bauen ist lehrreich, aber Production-Container haben Edge Cases und Performance-Optimierung, die du selbst nicht erreichst.
</div>
</div>
<div class="recall">
<b>Recall</b>
<ol>
<li>Was ist die Hauptaufgabe eines DI-Containers?</li>
<li>Welcher Reflection-Mechanismus ist Basis dafür?</li>
<li>Was machen "Tags" in Symfony's Container?</li>
</ol>
</div>
</section>
<!-- ===== KAPITEL 12 ===== -->
<section class="chapter">
<div class="chapter-head">
<div class="chapter-num">12</div>
<div class="chapter-title">
<h1>Event-Dispatcher Pattern</h1>
<div class="subtitle">Lose Kopplung zwischen Bounded Contexts</div>
</div>
</div>
<div class="gap">
<b>Frage zum Einstieg</b>
Eine Bestellung wird platziert. Folge: E-Mail an Kunde, Lagerplatz reservieren, Statistik aktualisieren, Webhook an Partner senden. Sollst du das alles in <code>OrderService::place()</code> reincoden? Das Event-Dispatcher-Pattern trennt das sauber.
</div>
<h2>Das Problem direkter Aufrufe</h2>
<pre><span class="k">class</span> <span class="t">OrderService</span> {
<span class="k">public function</span> <span class="f">__construct</span>(
<span class="k">private</span> EmailService <span class="v">$mailer</span>,
<span class="k">private</span> InventoryService <span class="v">$inventory</span>,
<span class="k">private</span> AnalyticsService <span class="v">$analytics</span>,
<span class="k">private</span> WebhookService <span class="v">$webhooks</span>,
<span class="c">// noch 5 weitere ...</span>
) {}
<span class="k">public function</span> <span class="f">place</span>(<span class="t">Order</span> <span class="v">$order</span>): <span class="t">void</span> {
<span class="v">$this</span>-&gt;<span class="f">save</span>(<span class="v">$order</span>);
<span class="v">$this</span>-&gt;mailer-&gt;<span class="f">sendConfirmation</span>(<span class="v">$order</span>);
<span class="v">$this</span>-&gt;inventory-&gt;<span class="f">reserve</span>(<span class="v">$order</span>);
<span class="v">$this</span>-&gt;analytics-&gt;<span class="f">track</span>(<span class="v">$order</span>);
<span class="v">$this</span>-&gt;webhooks-&gt;<span class="f">notify</span>(<span class="v">$order</span>);
<span class="c">// ...</span>
}
}</pre>
<p><code class="inline">OrderService</code> kennt alle anderen Services. Jeder neue Listener bedeutet eine Code-Änderung. Tests brauchen alle Mocks. <b>Tight Coupling</b>.</p>
<h2>Mit Event-Dispatcher</h2>
<p>Der Service publiziert nur ein <b>Event</b>. Wer darauf reagiert, ist ihm egal. Andere Bounded Contexts subscriben sich:</p>
<pre><span class="k">class</span> <span class="t">OrderPlaced</span> {
<span class="k">public function</span> <span class="f">__construct</span>(<span class="k">public readonly</span> <span class="t">Order</span> <span class="v">$order</span>) {}
}
<span class="k">class</span> <span class="t">OrderService</span> {
<span class="k">public function</span> <span class="f">__construct</span>(<span class="k">private</span> EventDispatcherInterface <span class="v">$events</span>) {}
<span class="k">public function</span> <span class="f">place</span>(<span class="t">Order</span> <span class="v">$order</span>): <span class="t">void</span> {
<span class="v">$this</span>-&gt;<span class="f">save</span>(<span class="v">$order</span>);
<span class="v">$this</span>-&gt;events-&gt;<span class="f">dispatch</span>(<span class="k">new</span> <span class="t">OrderPlaced</span>(<span class="v">$order</span>));
}
}
<span class="c">// Listener leben in ihren eigenen Modulen</span>
<span class="k">class</span> <span class="t">SendConfirmationListener</span> {
<span class="k">public function</span> <span class="f">__invoke</span>(<span class="t">OrderPlaced</span> <span class="v">$event</span>): <span class="t">void</span> {
<span class="v">$this</span>-&gt;mailer-&gt;<span class="f">send</span>(<span class="v">$event</span>-&gt;order-&gt;customer, <span class="s">'Bestätigung'</span>);
}
}
<span class="k">class</span> <span class="t">ReserveInventoryListener</span> {
<span class="k">public function</span> <span class="f">__invoke</span>(<span class="t">OrderPlaced</span> <span class="v">$event</span>): <span class="t">void</span> {
<span class="v">$this</span>-&gt;inventory-&gt;<span class="f">reserve</span>(<span class="v">$event</span>-&gt;order);
}
}</pre>
<p>Jetzt kann ein neues Team einen neuen Listener hinzufügen, ohne <code class="inline">OrderService</code> anzufassen. Tests des Services brauchen nur einen Mock-Dispatcher.</p>
<h2>Symfony EventDispatcher</h2>
<p>Symfony hat einen ausgereiften Dispatcher (PSR-14-konform). Listener werden über Tags oder Attribute registriert:</p>
<pre><span class="k">use</span> Symfony\Component\EventDispatcher\Attribute\AsEventListener;
<span class="k">#[<span class="t">AsEventListener</span>(event: <span class="t">OrderPlaced</span>::<span class="k">class</span>)]</span>
<span class="k">class</span> <span class="t">SendConfirmationListener</span> {
<span class="k">public function</span> <span class="f">__invoke</span>(<span class="t">OrderPlaced</span> <span class="v">$event</span>): <span class="t">void</span> {
<span class="c">// ...</span>
}
}</pre>
<p>Symfony scannt beim Container-Build alle Klassen mit dem Attribut und verdrahtet sie automatisch.</p>
<h2>Synchron vs. Asynchron</h2>
<p>Standardmäßig laufen Listener <b>synchron</b> im selben Request, hintereinander. Für langsame Operationen (E-Mail, Webhook) willst du sie async machen sonst friert der User-Request ein:</p>
<pre><span class="k">use</span> Symfony\Component\Messenger\Attribute\AsMessageHandler;
<span class="c">// Statt EventListener: MessageHandler in Symfony Messenger</span>
<span class="k">#[<span class="t">AsMessageHandler</span>]</span>
<span class="k">class</span> <span class="t">SendConfirmationHandler</span> {
<span class="k">public function</span> <span class="f">__invoke</span>(<span class="t">OrderPlaced</span> <span class="v">$message</span>): <span class="t">void</span> {
<span class="v">$this</span>-&gt;mailer-&gt;<span class="f">send</span>(...);
}
}
<span class="c"># config/messenger.yaml</span>
framework:
messenger:
routing:
App\Event\OrderPlaced: <span class="s">'async'</span> <span class="c"># landet in Queue</span></pre>
<p>Der User-Request kehrt sofort zurück, der Mail-Versand passiert im Worker-Prozess. Wichtig für UX und Resilienz.</p>
<div class="callout note">
<div class="callout-icon">i</div>
<div class="callout-body">
<b>Domain Events vs. Application Events</b>
<b>Domain Events</b> (<code class="inline">OrderPlaced</code>) sind Teil der Geschäftslogik, beschreiben "was passiert ist". <b>Application Events</b> sind technisch (<code class="inline">kernel.request</code> in Symfony). Mische sie nicht Domain Events leben im Domain-Layer, Application Events im Framework.
</div>
</div>
<div class="recall">
<b>Recall</b>
<ol>
<li>Was ist das Hauptproblem von direkten Service-Aufrufen ohne Events?</li>
<li>Wann nutzt du sync, wann async Event-Handling?</li>
<li>Was ist der Unterschied zwischen Domain Event und Application Event?</li>
</ol>
</div>
</section>
<!-- ===== KAPITEL 13 ===== -->
<section class="chapter">
<div class="chapter-head">
<div class="chapter-num">13</div>
<div class="chapter-title">
<h1>CQRS und Command Bus</h1>
<div class="subtitle">Lese- und Schreibmodelle trennen</div>
</div>
</div>
<div class="gap">
<b>Frage zum Einstieg</b>
Deine User-Liste braucht 12 verschiedene Filter, Pagination, Aggregate. Deine User-Updates haben komplexe Business-Regeln. <i>Beide</i> in einer Klasse abzubilden wird unhandlich. <b>CQRS</b> trennt Reads und Writes radikal.
</div>
<h2>Was ist CQRS?</h2>
<p><b>Command Query Responsibility Segregation</b> ein Pattern, das Lese-Operationen (Queries) und Schreib-Operationen (Commands) auf separate Modelle teilt:</p>
<ul>
<li><b>Commands</b> ändern State, geben <i>nichts</i> zurück (oder nur ID)</li>
<li><b>Queries</b> lesen State, ändern <i>nichts</i></li>
</ul>
<pre><span class="c">// Command: Intention "tu das"</span>
<span class="k">class</span> <span class="t">CreateUserCommand</span> {
<span class="k">public function</span> <span class="f">__construct</span>(
<span class="k">public readonly</span> <span class="t">string</span> <span class="v">$name</span>,
<span class="k">public readonly</span> <span class="t">string</span> <span class="v">$email</span>,
) {}
}
<span class="c">// Query: Frage "wie ist das"</span>
<span class="k">class</span> <span class="t">FindUsersByCountryQuery</span> {
<span class="k">public function</span> <span class="f">__construct</span>(
<span class="k">public readonly</span> <span class="t">string</span> <span class="v">$country</span>,
<span class="k">public readonly</span> <span class="t">int</span> <span class="v">$page</span> = <span class="s">1</span>,
) {}
}</pre>
<h2>Command Bus</h2>
<p>Statt Controller-Code, der direkt Repository-Aufrufe macht, dispatched er einen Command an den <b>Bus</b>. Der Bus findet den richtigen Handler:</p>
<pre><span class="k">class</span> <span class="t">UserController</span> {
<span class="k">public function</span> <span class="f">create</span>(<span class="t">Request</span> <span class="v">$request</span>, CommandBus <span class="v">$bus</span>): Response {
<span class="v">$command</span> = <span class="k">new</span> <span class="t">CreateUserCommand</span>(
name: <span class="v">$request</span>-&gt;<span class="f">getString</span>(<span class="s">'name'</span>),
email: <span class="v">$request</span>-&gt;<span class="f">getString</span>(<span class="s">'email'</span>),
);
<span class="v">$bus</span>-&gt;<span class="f">dispatch</span>(<span class="v">$command</span>);
<span class="k">return new</span> Response(<span class="s">'Created'</span>, <span class="s">201</span>);
}
}
<span class="c">// Handler die einzige Stelle mit Business-Logik</span>
<span class="k">class</span> <span class="t">CreateUserCommandHandler</span> {
<span class="k">public function</span> <span class="f">__construct</span>(<span class="k">private</span> UserRepository <span class="v">$repo</span>) {}
<span class="k">public function</span> <span class="f">__invoke</span>(<span class="t">CreateUserCommand</span> <span class="v">$cmd</span>): <span class="t">void</span> {
<span class="v">$user</span> = <span class="k">new</span> <span class="t">User</span>(<span class="v">$cmd</span>-&gt;name, <span class="v">$cmd</span>-&gt;email);
<span class="v">$this</span>-&gt;repo-&gt;<span class="f">save</span>(<span class="v">$user</span>);
}
}</pre>
<h2>Query Bus</h2>
<p>Analog für Reads aber mit Rückgabewert. Queries lesen oft aus optimierten Read-Modellen (denormalisierte Views, Search-Index):</p>
<pre><span class="k">class</span> <span class="t">FindUsersByCountryQueryHandler</span> {
<span class="k">public function</span> <span class="f">__construct</span>(<span class="k">private</span> \<span class="t">PDO</span> <span class="v">$db</span>) {}
<span class="k">public function</span> <span class="f">__invoke</span>(<span class="t">FindUsersByCountryQuery</span> <span class="v">$q</span>): <span class="t">array</span> {
<span class="c">// Direktes SQL, optimiert für die View</span>
<span class="v">$stmt</span> = <span class="v">$this</span>-&gt;db-&gt;<span class="f">prepare</span>(<span class="s">'
SELECT id, name, email, created_at, order_count
FROM users_with_stats
WHERE country = ?
LIMIT ? OFFSET ?
'</span>);
<span class="v">$stmt</span>-&gt;<span class="f">execute</span>([<span class="v">$q</span>-&gt;country, <span class="s">50</span>, (<span class="v">$q</span>-&gt;page - <span class="s">1</span>) * <span class="s">50</span>]);
<span class="k">return</span> <span class="v">$stmt</span>-&gt;<span class="f">fetchAll</span>();
}
}</pre>
<p>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.</p>
<h2>Symfony Messenger als Bus</h2>
<p>Symfony Messenger ist nicht nur für async auch als Command/Query Bus nutzbar:</p>
<pre><span class="c"># config/messenger.yaml</span>
framework:
messenger:
buses:
command.bus:
middleware:
- validation
- doctrine_transaction
query.bus:
middleware:
- validation</pre>
<pre><span class="k">class</span> <span class="t">UserController</span> {
<span class="k">public function</span> <span class="f">__construct</span>(
<span class="k">private</span> MessageBusInterface <span class="v">$commandBus</span>,
<span class="k">private</span> MessageBusInterface <span class="v">$queryBus</span>,
) {}
<span class="k">public function</span> <span class="f">list</span>(<span class="t">string</span> <span class="v">$country</span>): Response {
<span class="v">$users</span> = <span class="v">$this</span>-&gt;queryBus-&gt;<span class="f">dispatch</span>(<span class="k">new</span> <span class="t">FindUsersByCountryQuery</span>(<span class="v">$country</span>));
<span class="k">return</span> Response::<span class="f">json</span>(<span class="v">$users</span>);
}
}</pre>
<h2>Wann lohnt sich CQRS?</h2>
<table>
<tr><th>Use Case</th><th>CQRS sinnvoll?</th></tr>
<tr><td>Simple CRUD-App</td><td>nein, Overkill</td></tr>
<tr><td>Komplexe Business-Logik bei Writes</td><td>ja</td></tr>
<tr><td>Lese-Performance ist kritisch</td><td>ja</td></tr>
<tr><td>Sehr unterschiedliche Read- und Write-Modelle</td><td>ja</td></tr>
<tr><td>Mehrere Teams an einem Bounded Context</td><td>ja</td></tr>
</table>
<div class="callout warn">
<div class="callout-icon">!</div>
<div class="callout-body">
<b>CQRS ist keine Silver Bullet</b>
Es bringt mehr Code, mehr Klassen, mehr Indirection. Bei einer Standard-CRUD-App ist es Overkill. Wende es gezielt an: nur dort wo Komplexität es rechtfertigt. Innerhalb einer App kannst du auch nur einen Bounded Context mit CQRS bauen, den Rest mit klassischen Services.
</div>
</div>
<div class="recall">
<b>Recall</b>
<ol>
<li>Was ist der Hauptunterschied zwischen Command und Query?</li>
<li>Warum dürfen Query-Handler "denormalisiert" lesen?</li>
<li>Wann ist CQRS Overkill?</li>
</ol>
</div>
</section>
<!-- ===== KAPITEL 14 ===== -->
<section class="chapter">
<div class="chapter-head">
<div class="chapter-num">14</div>
<div class="chapter-title">
<h1>Domain-Driven Design in PHP</h1>
<div class="subtitle">Value Objects, Entities, Aggregates</div>
</div>
</div>
<div class="gap">
<b>Frage zum Einstieg</b>
Anämische Entities sind in PHP weit verbreitet: Klassen mit nur Properties und Getter/Setter, alle Business-Logik in Service-Klassen verstreut. <b>Domain-Driven Design</b> dreht das um Logik lebt im Domain-Modell, nicht außerhalb.
</div>
<h2>Value Objects</h2>
<p>Ein <b>Value Object</b> ist ein Wert ohne eigene Identität, unveränderlich. Statt <code class="inline">string $email</code> nimm <code class="inline">Email $email</code> mit eingebauter Validierung:</p>
<pre><span class="k">final class</span> <span class="t">Email</span> {
<span class="k">public function</span> <span class="f">__construct</span>(<span class="k">public readonly</span> <span class="t">string</span> <span class="v">$value</span>) {
<span class="k">if</span> (!<span class="f">filter_var</span>(<span class="v">$value</span>, FILTER_VALIDATE_EMAIL)) {
<span class="k">throw new</span> \<span class="t">InvalidArgumentException</span>(<span class="s">"Ungültige Email: </span><span class="v">$value</span><span class="s">"</span>);
}
}
<span class="k">public function</span> <span class="f">domain</span>(): <span class="t">string</span> {
<span class="k">return</span> <span class="f">substr</span>(<span class="v">$this</span>-&gt;value, <span class="f">strpos</span>(<span class="v">$this</span>-&gt;value, <span class="s">'@'</span>) + <span class="s">1</span>);
}
<span class="k">public function</span> <span class="f">equals</span>(<span class="t">Email</span> <span class="v">$other</span>): <span class="t">bool</span> {
<span class="k">return</span> <span class="f">strtolower</span>(<span class="v">$this</span>-&gt;value) === <span class="f">strtolower</span>(<span class="v">$other</span>-&gt;value);
}
}
<span class="v">$email</span> = <span class="k">new</span> <span class="t">Email</span>(<span class="s">'marek@example.com'</span>); <span class="c">// validiert automatisch</span>
<span class="v">$email</span>-&gt;<span class="f">domain</span>(); <span class="c">// 'example.com'</span></pre>
<p>Wo immer eine Email durch deinen Code wandert, ist sie <b>garantiert valide</b>. Keine "ist das ein gültiges Format?"-Checks mehr überall.</p>
<h2>Entities mit Geschäftslogik</h2>
<p>Eine <b>Entity</b> 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 <i>in</i> der Entity:</p>
<pre><span class="k">class</span> <span class="t">Order</span> {
<span class="k">private array</span> <span class="v">$items</span> = [];
<span class="k">private</span> OrderStatus <span class="v">$status</span>;
<span class="k">public function</span> <span class="f">__construct</span>(<span class="k">public readonly</span> OrderId <span class="v">$id</span>, <span class="k">public readonly</span> CustomerId <span class="v">$customerId</span>) {
<span class="v">$this</span>-&gt;status = OrderStatus::<span class="t">Draft</span>;
}
<span class="k">public function</span> <span class="f">addItem</span>(Product <span class="v">$product</span>, <span class="t">int</span> <span class="v">$quantity</span>): <span class="t">void</span> {
<span class="k">if</span> (<span class="v">$this</span>-&gt;status !== OrderStatus::<span class="t">Draft</span>) {
<span class="k">throw new</span> CannotModifyConfirmedOrder();
}
<span class="v">$this</span>-&gt;items[] = <span class="k">new</span> OrderItem(<span class="v">$product</span>, <span class="v">$quantity</span>);
}
<span class="k">public function</span> <span class="f">confirm</span>(): <span class="t">void</span> {
<span class="k">if</span> (<span class="f">empty</span>(<span class="v">$this</span>-&gt;items)) <span class="k">throw new</span> CannotConfirmEmptyOrder();
<span class="v">$this</span>-&gt;status = OrderStatus::<span class="t">Confirmed</span>;
}
<span class="k">public function</span> <span class="f">total</span>(): Money {
<span class="k">return</span> <span class="f">array_reduce</span>(
<span class="v">$this</span>-&gt;items,
<span class="k">fn</span>(Money <span class="v">$sum</span>, OrderItem <span class="v">$item</span>) =&gt; <span class="v">$sum</span>-&gt;<span class="f">add</span>(<span class="v">$item</span>-&gt;<span class="f">subtotal</span>()),
Money::<span class="f">zero</span>()
);
}
}</pre>
<p>Die Order schützt ihre eigenen Invarianten sie lässt nicht zu, dass jemand sie in einen inkonsistenten Zustand bringt.</p>
<h2>Aggregates und Aggregate Root</h2>
<p>Ein <b>Aggregate</b> ist eine Gruppe von Objekten, die <i>zusammen</i> konsistent bleiben müssen. Der <b>Aggregate Root</b> ist der einzige Einstiegspunkt:</p>
<pre><span class="c">// Order ist Aggregate Root</span>
<span class="c">// OrderItem ist Teil des Aggregates, NICHT direkt zugreifbar</span>
<span class="c">// Schlecht: OrderItem direkt aus Repository holen</span>
<span class="v">$item</span> = <span class="v">$itemRepo</span>-&gt;<span class="f">find</span>(<span class="s">123</span>);
<span class="v">$item</span>-&gt;quantity = <span class="s">5</span>;
<span class="v">$itemRepo</span>-&gt;<span class="f">save</span>(<span class="v">$item</span>); <span class="c">// Order weiß nichts davon → Inkonsistenz</span>
<span class="c">// Gut: Immer durch Aggregate Root</span>
<span class="v">$order</span> = <span class="v">$orderRepo</span>-&gt;<span class="f">find</span>(<span class="v">$orderId</span>);
<span class="v">$order</span>-&gt;<span class="f">changeItemQuantity</span>(<span class="v">$itemId</span>, <span class="s">5</span>); <span class="c">// Order prüft Invarianten</span>
<span class="v">$orderRepo</span>-&gt;<span class="f">save</span>(<span class="v">$order</span>);</pre>
<h2>Repositories</h2>
<p>Ein <b>Repository</b> ist die Schnittstelle zur Persistenz für ein Aggregate. Er sieht aus wie eine Collection:</p>
<pre><span class="k">interface</span> <span class="t">OrderRepository</span> {
<span class="k">public function</span> <span class="f">find</span>(OrderId <span class="v">$id</span>): ?<span class="t">Order</span>;
<span class="k">public function</span> <span class="f">save</span>(<span class="t">Order</span> <span class="v">$order</span>): <span class="t">void</span>;
<span class="k">public function</span> <span class="f">remove</span>(<span class="t">Order</span> <span class="v">$order</span>): <span class="t">void</span>;
}
<span class="c">// Implementierung mit Doctrine</span>
<span class="k">class</span> <span class="t">DoctrineOrderRepository</span> <span class="k">implements</span> <span class="t">OrderRepository</span> {
<span class="k">public function</span> <span class="f">__construct</span>(<span class="k">private</span> EntityManagerInterface <span class="v">$em</span>) {}
<span class="k">public function</span> <span class="f">find</span>(OrderId <span class="v">$id</span>): ?<span class="t">Order</span> {
<span class="k">return</span> <span class="v">$this</span>-&gt;em-&gt;<span class="f">find</span>(<span class="t">Order</span>::<span class="k">class</span>, <span class="v">$id</span>-&gt;value);
}
<span class="k">public function</span> <span class="f">save</span>(<span class="t">Order</span> <span class="v">$order</span>): <span class="t">void</span> {
<span class="v">$this</span>-&gt;em-&gt;<span class="f">persist</span>(<span class="v">$order</span>);
<span class="v">$this</span>-&gt;em-&gt;<span class="f">flush</span>();
}
}</pre>
<div class="callout note">
<div class="callout-icon">i</div>
<div class="callout-body">
<b>DDD ist mehr als nur Klassen-Struktur</b>
DDD beinhaltet auch <b>strategisches Design</b>: Bounded Contexts, Ubiquitous Language, Context Maps. Die taktischen Patterns (Value Object, Entity, Aggregate) sind nur das Werkzeug für die strategische Trennung von Geschäftsbereichen.
</div>
</div>
<div class="recall">
<b>Recall</b>
<ol>
<li>Was unterscheidet Value Object von Entity?</li>
<li>Was ist die Regel für Zugriff auf Aggregate-Member?</li>
<li>Wofür ist ein Repository da?</li>
</ol>
</div>
</section>
<!-- ===== KAPITEL 15 ===== -->
<section class="chapter">
<div class="chapter-head">
<div class="chapter-num">15</div>
<div class="chapter-title">
<h1>Hexagonal Architecture</h1>
<div class="subtitle">Ports und Adapter, Domain im Zentrum</div>
</div>
</div>
<div class="gap">
<b>Frage zum Einstieg</b>
Deine App hängt überall an Symfony, an Doctrine, an Redis. Refactor auf ein anderes Framework? Praktisch unmöglich. Tests, die echte DB brauchen? Langsam. <b>Hexagonal Architecture</b> (auch "Ports und Adapter") löst das mit konsequenter Inversion of Control.
</div>
<h2>Das Prinzip</h2>
<p>Im Zentrum steht die <b>Domain</b> (Geschäftslogik). Drumherum sind <b>Ports</b> (Interfaces) und <b>Adapter</b> (Implementierungen). Die Domain weiß nichts von Symfony, Doctrine oder HTTP sie kennt nur ihre eigenen Interfaces:</p>
<pre> ┌─────────────────┐
HTTP → │ │
│ │
CLI → │ DOMAIN │ → Database
│ (Geschäfts- │ → File-Storage
Worker → │ logik) │ → External API
│ │
└─────────────────┘
↑ ↓
Adapter Adapter
(Inbound) (Outbound)</pre>
<h2>Inbound Port: was die Domain anbietet</h2>
<pre><span class="c">// Inbound Port: definiert, was die Domain anbietet</span>
<span class="c">// Liegt im src/Application/</span>
<span class="k">interface</span> <span class="t">PlaceOrderUseCase</span> {
<span class="k">public function</span> <span class="f">execute</span>(<span class="t">PlaceOrderInput</span> <span class="v">$input</span>): <span class="t">PlaceOrderOutput</span>;
}
<span class="c">// Implementierung in der Domain (kennt keine Framework-Klassen)</span>
<span class="k">class</span> <span class="t">PlaceOrderService</span> <span class="k">implements</span> <span class="t">PlaceOrderUseCase</span> {
<span class="k">public function</span> <span class="f">__construct</span>(
<span class="k">private</span> OrderRepository <span class="v">$orders</span>,
<span class="k">private</span> PaymentGateway <span class="v">$payment</span>,
<span class="k">private</span> EventDispatcher <span class="v">$events</span>,
) {}
<span class="k">public function</span> <span class="f">execute</span>(<span class="t">PlaceOrderInput</span> <span class="v">$input</span>): <span class="t">PlaceOrderOutput</span> {
<span class="v">$order</span> = <span class="k">new</span> <span class="t">Order</span>(...);
<span class="v">$this</span>-&gt;payment-&gt;<span class="f">charge</span>(<span class="v">$order</span>);
<span class="v">$this</span>-&gt;orders-&gt;<span class="f">save</span>(<span class="v">$order</span>);
<span class="v">$this</span>-&gt;events-&gt;<span class="f">dispatch</span>(<span class="k">new</span> <span class="t">OrderPlaced</span>(<span class="v">$order</span>));
<span class="k">return new</span> <span class="t">PlaceOrderOutput</span>(<span class="v">$order</span>-&gt;id);
}
}</pre>
<h2>Inbound Adapter: HTTP, CLI, etc.</h2>
<p>Der HTTP-Controller ist ein <b>Adapter</b> er übersetzt zwischen HTTP-Request und Domain-Input:</p>
<pre><span class="c">// Inbound Adapter (Symfony Controller)</span>
<span class="c">// Liegt im src/Infrastructure/Http/</span>
<span class="k">class</span> <span class="t">PlaceOrderController</span> {
<span class="k">public function</span> <span class="f">__construct</span>(<span class="k">private</span> <span class="t">PlaceOrderUseCase</span> <span class="v">$useCase</span>) {}
<span class="k">#[Route(<span class="s">'/orders'</span>, methods: [<span class="s">'POST'</span>])]</span>
<span class="k">public function</span> <span class="f">__invoke</span>(<span class="t">Request</span> <span class="v">$request</span>): <span class="t">JsonResponse</span> {
<span class="v">$input</span> = <span class="k">new</span> <span class="t">PlaceOrderInput</span>(
customerId: <span class="v">$request</span>-&gt;<span class="f">get</span>(<span class="s">'customer_id'</span>),
items: <span class="v">$request</span>-&gt;<span class="f">get</span>(<span class="s">'items'</span>),
);
<span class="k">try</span> {
<span class="v">$output</span> = <span class="v">$this</span>-&gt;useCase-&gt;<span class="f">execute</span>(<span class="v">$input</span>);
<span class="k">return new</span> JsonResponse([<span class="s">'order_id'</span> =&gt; <span class="v">$output</span>-&gt;orderId], <span class="s">201</span>);
} <span class="k">catch</span> (DomainException <span class="v">$e</span>) {
<span class="k">return new</span> JsonResponse([<span class="s">'error'</span> =&gt; <span class="v">$e</span>-&gt;<span class="f">getMessage</span>()], <span class="s">400</span>);
}
}
}</pre>
<h2>Outbound Port und Adapter</h2>
<p>Die Domain definiert, <i>was</i> sie braucht (Port), die Infrastruktur liefert <i>wie</i> (Adapter):</p>
<pre><span class="c">// Outbound Port (in der Domain)</span>
<span class="c">// Liegt im src/Domain/</span>
<span class="k">interface</span> <span class="t">PaymentGateway</span> {
<span class="k">public function</span> <span class="f">charge</span>(<span class="t">Order</span> <span class="v">$order</span>): PaymentReceipt;
}
<span class="c">// Outbound Adapter (in der Infrastructure)</span>
<span class="c">// Liegt im src/Infrastructure/Payment/</span>
<span class="k">class</span> <span class="t">StripeGateway</span> <span class="k">implements</span> <span class="t">PaymentGateway</span> {
<span class="k">public function</span> <span class="f">__construct</span>(<span class="k">private</span> StripeClient <span class="v">$stripe</span>) {}
<span class="k">public function</span> <span class="f">charge</span>(<span class="t">Order</span> <span class="v">$order</span>): PaymentReceipt {
<span class="v">$charge</span> = <span class="v">$this</span>-&gt;stripe-&gt;charges-&gt;<span class="f">create</span>([...]);
<span class="k">return new</span> PaymentReceipt(<span class="v">$charge</span>-&gt;id);
}
}
<span class="c">// Alternative für Tests</span>
<span class="k">class</span> <span class="t">FakePaymentGateway</span> <span class="k">implements</span> <span class="t">PaymentGateway</span> {
<span class="k">public function</span> <span class="f">charge</span>(<span class="t">Order</span> <span class="v">$order</span>): PaymentReceipt {
<span class="k">return new</span> PaymentReceipt(<span class="s">'fake-receipt'</span>);
}
}</pre>
<h2>Vorteile</h2>
<ul>
<li><b>Tests ohne Infrastruktur</b> Domain mit Fakes statt echter DB/API</li>
<li><b>Framework-Wechsel möglich</b> Domain unabhängig von Symfony</li>
<li><b>Klare Verantwortung</b> jede Schicht hat ihren Job</li>
<li><b>Adapter parallel</b> HTTP, CLI und Worker rufen denselben Use Case</li>
</ul>
<h2>Verzeichnis-Struktur</h2>
<pre>src/
├── Domain/ <span class="c"># Reine Business-Logik</span>
│ ├── Order/
│ │ ├── Order.php
│ │ ├── OrderId.php
│ │ ├── OrderRepository.php <span class="c"># Interface</span>
│ │ └── PaymentGateway.php <span class="c"># Interface</span>
│ └── User/...
├── Application/ <span class="c"># Use Cases</span>
│ ├── PlaceOrderUseCase.php
│ └── PlaceOrderService.php
└── Infrastructure/ <span class="c"># Adapters</span>
├── Http/
│ └── PlaceOrderController.php
├── Persistence/
│ └── DoctrineOrderRepository.php
└── Payment/
└── StripeGateway.php</pre>
<div class="callout warn">
<div class="callout-icon">!</div>
<div class="callout-body">
<b>Nicht jede App braucht Hexagonal</b>
Die Architektur kostet: mehr Interfaces, mehr Klassen, mehr Indirection. Für CRUD-Apps Overkill. Lohnt sich, wenn die Geschäftslogik komplex und langlebig ist, Tests wichtig sind und die App über Jahre weiterleben soll.
</div>
</div>
<div class="recall">
<b>Recall</b>
<ol>
<li>Wo lebt die Geschäftslogik in Hexagonal Architecture?</li>
<li>Was ist der Unterschied zwischen Port und Adapter?</li>
<li>Warum kann man die Domain ohne Datenbank testen?</li>
</ol>
</div>
</section>
<!-- ===== KAPITEL 16 ===== -->
<section class="chapter">
<div class="chapter-head">
<div class="chapter-num">16</div>
<div class="chapter-title">
<h1>Event Sourcing</h1>
<div class="subtitle">State als Sequenz von Events</div>
</div>
</div>
<div class="gap">
<b>Frage zum Einstieg</b>
Klassische DB speichert den <i>aktuellen Zustand</i>: "Bestellung X hat 3 Items, Status 'paid'". Wer hat wann was geändert? Verloren. <b>Event Sourcing</b> dreht das um: speichere die <i>Events</i>, nicht den Zustand. Der aktuelle Zustand ist die Summe aller Events.
</div>
<h2>Das Grundprinzip</h2>
<p>Statt: <code class="inline">UPDATE orders SET status='paid' WHERE id=42</code> speicherst du: <code class="inline">OrderPaid(orderId: 42, at: 2026-01-15)</code> in einer <b>Event-Store</b>-Tabelle. Der aktuelle Zustand entsteht durch Replay aller Events:</p>
<pre>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 | {...} |
+----+------------+------------------+----------+</pre>
<h2>Aggregate mit Events</h2>
<pre><span class="k">abstract class</span> <span class="t">AggregateRoot</span> {
<span class="k">private array</span> <span class="v">$pendingEvents</span> = [];
<span class="k">private int</span> <span class="v">$version</span> = <span class="s">0</span>;
<span class="k">protected function</span> <span class="f">recordEvent</span>(<span class="t">DomainEvent</span> <span class="v">$event</span>): <span class="t">void</span> {
<span class="v">$this</span>-&gt;<span class="f">apply</span>(<span class="v">$event</span>);
<span class="v">$this</span>-&gt;pendingEvents[] = <span class="v">$event</span>;
}
<span class="k">public function</span> <span class="f">pullEvents</span>(): <span class="t">array</span> {
<span class="v">$events</span> = <span class="v">$this</span>-&gt;pendingEvents;
<span class="v">$this</span>-&gt;pendingEvents = [];
<span class="k">return</span> <span class="v">$events</span>;
}
<span class="k">abstract protected function</span> <span class="f">apply</span>(<span class="t">DomainEvent</span> <span class="v">$event</span>): <span class="t">void</span>;
}
<span class="k">class</span> <span class="t">Order</span> <span class="k">extends</span> <span class="t">AggregateRoot</span> {
<span class="k">private</span> OrderStatus <span class="v">$status</span>;
<span class="k">private array</span> <span class="v">$items</span> = [];
<span class="k">public static function</span> <span class="f">create</span>(OrderId <span class="v">$id</span>, CustomerId <span class="v">$customer</span>): <span class="t">self</span> {
<span class="v">$order</span> = <span class="k">new</span> <span class="t">self</span>();
<span class="v">$order</span>-&gt;<span class="f">recordEvent</span>(<span class="k">new</span> <span class="t">OrderCreated</span>(<span class="v">$id</span>, <span class="v">$customer</span>));
<span class="k">return</span> <span class="v">$order</span>;
}
<span class="k">public function</span> <span class="f">addItem</span>(Product <span class="v">$product</span>, <span class="t">int</span> <span class="v">$quantity</span>): <span class="t">void</span> {
<span class="k">if</span> (<span class="v">$this</span>-&gt;status !== OrderStatus::<span class="t">Draft</span>) <span class="k">throw new</span> ...;
<span class="v">$this</span>-&gt;<span class="f">recordEvent</span>(<span class="k">new</span> <span class="t">ItemAdded</span>(<span class="v">$product</span>, <span class="v">$quantity</span>));
}
<span class="k">protected function</span> <span class="f">apply</span>(<span class="t">DomainEvent</span> <span class="v">$event</span>): <span class="t">void</span> {
<span class="k">match</span>(<span class="v">$event</span>::<span class="k">class</span>) {
<span class="t">OrderCreated</span>::<span class="k">class</span> =&gt; <span class="v">$this</span>-&gt;<span class="f">whenOrderCreated</span>(<span class="v">$event</span>),
<span class="t">ItemAdded</span>::<span class="k">class</span> =&gt; <span class="v">$this</span>-&gt;<span class="f">whenItemAdded</span>(<span class="v">$event</span>),
<span class="c">// ...</span>
};
}
<span class="k">private function</span> <span class="f">whenOrderCreated</span>(<span class="t">OrderCreated</span> <span class="v">$e</span>): <span class="t">void</span> {
<span class="v">$this</span>-&gt;status = OrderStatus::<span class="t">Draft</span>;
}
<span class="k">private function</span> <span class="f">whenItemAdded</span>(<span class="t">ItemAdded</span> <span class="v">$e</span>): <span class="t">void</span> {
<span class="v">$this</span>-&gt;items[] = <span class="k">new</span> OrderItem(<span class="v">$e</span>-&gt;product, <span class="v">$e</span>-&gt;quantity);
}
}</pre>
<p>Beachte die zwei Phasen: <code class="inline">recordEvent</code> erstellt das Event und ruft <code class="inline">apply</code> auf. <code class="inline">apply</code> ändert nur State, niemals Validierung sonst kann beim Replay nichts schiefgehen.</p>
<h2>Rekonstruieren aus Events</h2>
<p>Beim Laden eines Aggregates: alle Events aus dem Store holen, neuen Aggregate-Instanz erstellen, jedes Event apply'en:</p>
<pre><span class="k">class</span> <span class="t">EventSourcedOrderRepository</span> {
<span class="k">public function</span> <span class="f">__construct</span>(<span class="k">private</span> EventStore <span class="v">$store</span>) {}
<span class="k">public function</span> <span class="f">find</span>(OrderId <span class="v">$id</span>): ?<span class="t">Order</span> {
<span class="v">$events</span> = <span class="v">$this</span>-&gt;store-&gt;<span class="f">getEvents</span>(<span class="s">"order:</span><span class="v">$id</span><span class="s">"</span>);
<span class="k">if</span> (<span class="f">empty</span>(<span class="v">$events</span>)) <span class="k">return null</span>;
<span class="v">$order</span> = (<span class="k">new</span> \<span class="t">ReflectionClass</span>(Order::<span class="k">class</span>))-&gt;<span class="f">newInstanceWithoutConstructor</span>();
<span class="k">foreach</span> (<span class="v">$events</span> <span class="k">as</span> <span class="v">$event</span>) {
<span class="v">$order</span>-&gt;<span class="f">applyHistoric</span>(<span class="v">$event</span>);
}
<span class="k">return</span> <span class="v">$order</span>;
}
<span class="k">public function</span> <span class="f">save</span>(<span class="t">Order</span> <span class="v">$order</span>): <span class="t">void</span> {
<span class="k">foreach</span> (<span class="v">$order</span>-&gt;<span class="f">pullEvents</span>() <span class="k">as</span> <span class="v">$event</span>) {
<span class="v">$this</span>-&gt;store-&gt;<span class="f">append</span>(<span class="s">"order:</span><span class="v">$order</span><span class="s">-&gt;id"</span>, <span class="v">$event</span>);
}
}
}</pre>
<h2>Vorteile</h2>
<ul>
<li><b>Komplette Historie</b> jede Änderung ist nachvollziehbar, "wer hat wann was?"</li>
<li><b>Audit-Trail</b> ideal für Banking, Healthcare, Compliance</li>
<li><b>Time-Travel-Debugging</b> State zu einem beliebigen Zeitpunkt rekonstruieren</li>
<li><b>Multiple Projections</b> verschiedene Read-Modelle aus denselben Events bauen</li>
<li><b>Natürlich mit CQRS kombinierbar</b></li>
</ul>
<h2>Nachteile</h2>
<ul>
<li><b>Komplexer als CRUD</b> nicht für alles geeignet</li>
<li><b>Event-Schema-Evolution</b> Events sind ewig, Schema-Änderungen schwierig</li>
<li><b>Performance</b> Replay vieler Events kostet → <b>Snapshots</b> als Optimierung</li>
<li><b>Eventual Consistency</b> Reads kommen aus Projektionen, nicht aus Events direkt</li>
</ul>
<div class="callout note">
<div class="callout-icon">i</div>
<div class="callout-body">
<b>Libraries für Event Sourcing in PHP</b>
<b>EventSauce</b> ist die populärste Library im PHP-Ökosystem ausgereift, mit Snapshot-Support, Upcasting für Schema-Evolution und CQRS-Integration. <b>Broadway</b> und <b>Prooph</b> sind ältere Alternativen.
</div>
</div>
<div class="recall">
<b>Recall</b>
<ol>
<li>Was speichert ein Event Store statt aktuellem State?</li>
<li>Warum trennt man <code style="color:#777BB4">recordEvent</code> und <code style="color:#777BB4">apply</code>?</li>
<li>Welche Library nutzt man typischerweise in PHP?</li>
</ol>
</div>
</section>
<!-- ===== ENDING ===== -->
<section class="ending">
<h1>Wie es weitergeht</h1>
<p>Du hast PHP jetzt von Sprach-Internals über Performance-Optimierung bis zu fortgeschrittenen Architektur-Patterns durchlaufen. Damit kennst du PHP an seinen Grenzen.</p>
<h2>Spaced-Repetition-Plan</h2>
<div class="spaced-plan">
<div class="spaced-day">
<b>Heute</b>
<p>Guide gelesen, Recall-Fragen aus jedem Kapitel beantwortet.</p>
</div>
<div class="spaced-day">
<b>+7 Tage</b>
<p>Zwei Kapitel auswählen, vertiefen, eigene Implementierung versuchen.</p>
</div>
<div class="spaced-day">
<b>+30 Tage</b>
<p>Spezialthema umsetzen: ReactPHP-Server, eigener DI-Container oder Hexagonal Architecture in echtem Projekt.</p>
</div>
<div class="spaced-day">
<b>+90 Tage</b>
<p>Source-Code von Symfony, Doctrine oder EventSauce lesen Patterns erkennen.</p>
</div>
</div>
<h2>Was als nächstes lernen</h2>
<p>Du bist jetzt jenseits des offiziellen PHP-Lernpfads. Empfehlungen für Tiefenexpertise:</p>
<ul>
<li><b>PHP-Source-Code</b> Zend Engine in C, internal-Verzeichnis auf GitHub</li>
<li><b>Symfony-Internals</b> Container-Compilation, HttpKernel-Lifecycle</li>
<li><b>Doctrine Internals</b> Unit-of-Work, Hydration, Schema-Tool</li>
<li><b>Eigene Extensions schreiben</b> C-Extension mit PHP-CPP oder Zephir</li>
<li><b>Sprach-Design</b> RFCs lesen, an PHP Internals Mailing-List teilnehmen</li>
<li><b>Performance-Tuning</b> OpCache-Internals, JIT-Verhalten, Memory-Layout</li>
</ul>
<h2>Begleitmaterial</h2>
<p>Dieser Guide schließt das Set ab:</p>
<ul>
<li><b>PHP OnePager</b> die visuelle Übersicht</li>
<li><b>PHP Cheatsheet</b> die dichte Referenz</li>
<li><b>PHP Mini-Guide</b> der 15-Min-Schnelleinstieg</li>
<li><b>PHP Anfänger-Guide</b> die Grundlagen</li>
<li><b>PHP Fortgeschritten-Guide</b> Patterns und Production</li>
<li><b>PHP Extended-Guide</b> (dieses Dokument) Internals und Architektur</li>
</ul>
</section>
</body>
</html>
```