122 KiB
122 KiB
<!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 & 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 & 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 & 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 & 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>-><span class="f">getName</span>(); <span class="c">// 'App\User'</span>
<span class="v">$reflection</span>-><span class="f">getShortName</span>(); <span class="c">// 'User'</span>
<span class="v">$reflection</span>-><span class="f">getMethods</span>(); <span class="c">// ReflectionMethod[]</span>
<span class="v">$reflection</span>-><span class="f">getProperties</span>(); <span class="c">// ReflectionProperty[]</span>
<span class="v">$reflection</span>-><span class="f">getParentClass</span>(); <span class="c">// ReflectionClass|false</span>
<span class="v">$reflection</span>-><span class="f">getInterfaceNames</span>(); <span class="c">// string[]</span>
<span class="v">$reflection</span>-><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>-><span class="f">newInstanceWithoutConstructor</span>();
<span class="c">// Jetzt Properties direkt setzen</span>
<span class="v">$nameProperty</span> = <span class="v">$reflection</span>-><span class="f">getProperty</span>(<span class="s">'name'</span>);
<span class="v">$nameProperty</span>-><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>-><span class="f">getProperty</span>(<span class="s">'internalRef'</span>);
<span class="v">$prop</span>-><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>-><span class="f">getMethod</span>(<span class="s">'calculateTax'</span>);
<span class="v">$tax</span> = <span class="v">$method</span>-><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->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>-><span class="f">push</span>(<span class="s">'a'</span>);
<span class="v">$stack</span>-><span class="f">push</span>(<span class="s">'b'</span>);
<span class="v">$stack</span>-><span class="f">push</span>(<span class="s">'c'</span>);
<span class="v">$stack</span>-><span class="f">pop</span>(); <span class="c">// 'c'</span>
<span class="v">$stack</span>-><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>-><span class="f">enqueue</span>(<span class="s">'first'</span>);
<span class="v">$queue</span>-><span class="f">enqueue</span>(<span class="s">'second'</span>);
<span class="v">$queue</span>-><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>-><span class="f">insert</span>(<span class="s">'low-priority-task'</span>, <span class="s">1</span>);
<span class="v">$pq</span>-><span class="f">insert</span>(<span class="s">'urgent-task'</span>, <span class="s">10</span>);
<span class="v">$pq</span>-><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>-><span class="f">isEmpty</span>()) {
<span class="k">echo</span> <span class="v">$pq</span>-><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>-><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>->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> < <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> < <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> => <span class="s">'Hallo, Welt!'</span>,
<span class="s">'goodbye'</span> => <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> &<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>->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>->data, <span class="v">$this</span>->position, <span class="v">$count</span>);
<span class="v">$this</span>->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>->position >= <span class="f">strlen</span>(<span class="v">$this</span>->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>-><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>->next = <span class="v">$b</span>; <span class="c">// $b refcount = 2</span>
<span class="v">$b</span>->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->next)</span>
<span class="k">unset</span>(<span class="v">$b</span>); <span class="c">// $b refcount = 1 (von $a->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>-><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>() > <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>() > <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>-><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>-><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>-><span class="f">getpid</span>(); <span class="c">// 12345 (Process-ID)</span>
<span class="v">$ffi</span>-><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>-><span class="f">SDL_Init</span>(SDL_INIT_VIDEO);
<span class="v">$window</span> = <span class="v">$ffi</span>-><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>-><span class="f">new</span>(<span class="s">'Point'</span>);
<span class="v">$point</span>->x = <span class="s">42</span>;
<span class="v">$point</span>->y = <span class="s">17</span>;
<span class="c">// Array von Structs</span>
<span class="v">$points</span> = <span class="v">$ffi</span>-><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> < <span class="s">100</span>; <span class="v">$i</span>++) {
<span class="v">$points</span>[<span class="v">$i</span>]->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"><?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>-><span class="f">getExtension</span>() === <span class="s">'php'</span>) {
<span class="f">opcache_compile_file</span>(<span class="v">$file</span>-><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>-><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>-><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 & 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>-><span class="f">getUri</span>()-><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>-><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>-><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>-><span class="f">write</span>(<span class="f">strtoupper</span>(<span class="v">$chunk</span>));
});
<span class="v">$stdin</span>-><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>->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>->clients-><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>->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>-><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>-><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())-><span class="f">setTitle</span>(<span class="s">'Order Processing'</span>);
<span class="v">$probe</span> = <span class="v">$blackfire</span>-><span class="f">createProbe</span>(<span class="v">$config</span>);
<span class="c">// Code, der gemessen werden soll</span>
<span class="v">$service</span>-><span class="f">processOrders</span>(<span class="v">$orders</span>);
<span class="v">$blackfire</span>-><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. Symfony 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>->cache-><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>-><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>-><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>-><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>-><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>-><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>-><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>-><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>-><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>-><span class="f">setPublic</span>();
<span class="v">$response</span>-><span class="f">setMaxAge</span>(<span class="s">3600</span>);
<span class="v">$response</span>-><span class="f">setSharedMaxAge</span>(<span class="s">86400</span>); <span class="c">// CDN cached 1 Tag</span>
<span class="v">$response</span>-><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>-><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>->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>-><span class="f">bind</span>(<span class="v">$abstract</span>, <span class="v">$concrete</span>);
<span class="v">$this</span>->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>->instances) && <span class="v">$this</span>->instances[<span class="v">$id</span>]) {
<span class="k">return</span> <span class="v">$this</span>->instances[<span class="v">$id</span>];
}
<span class="v">$instance</span> = <span class="v">$this</span>-><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>->instances)) {
<span class="v">$this</span>->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>->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>-><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>-><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>-><span class="f">getType</span>();
<span class="k">if</span> (<span class="v">$type</span> && !<span class="v">$type</span>-><span class="f">isBuiltin</span>()) {
<span class="v">$args</span>[] = <span class="v">$this</span>-><span class="f">get</span>(<span class="v">$type</span>-><span class="f">getName</span>());
} <span class="k">elseif</span> (<span class="v">$param</span>-><span class="f">isDefaultValueAvailable</span>()) {
<span class="v">$args</span>[] = <span class="v">$param</span>-><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>-><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>-><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>-><span class="f">bind</span>(\<span class="t">PDO</span>::<span class="k">class</span>, <span class="k">fn</span>() =>
<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>-><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>-><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>-><span class="f">save</span>(<span class="v">$order</span>);
<span class="v">$this</span>->mailer-><span class="f">sendConfirmation</span>(<span class="v">$order</span>);
<span class="v">$this</span>->inventory-><span class="f">reserve</span>(<span class="v">$order</span>);
<span class="v">$this</span>->analytics-><span class="f">track</span>(<span class="v">$order</span>);
<span class="v">$this</span>->webhooks-><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>-><span class="f">save</span>(<span class="v">$order</span>);
<span class="v">$this</span>->events-><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>->mailer-><span class="f">send</span>(<span class="v">$event</span>->order->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>->inventory-><span class="f">reserve</span>(<span class="v">$event</span>->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>->mailer-><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>-><span class="f">getString</span>(<span class="s">'name'</span>),
email: <span class="v">$request</span>-><span class="f">getString</span>(<span class="s">'email'</span>),
);
<span class="v">$bus</span>-><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>->name, <span class="v">$cmd</span>->email);
<span class="v">$this</span>->repo-><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>->db-><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>-><span class="f">execute</span>([<span class="v">$q</span>->country, <span class="s">50</span>, (<span class="v">$q</span>->page - <span class="s">1</span>) * <span class="s">50</span>]);
<span class="k">return</span> <span class="v">$stmt</span>-><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>->queryBus-><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>->value, <span class="f">strpos</span>(<span class="v">$this</span>->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>->value) === <span class="f">strtolower</span>(<span class="v">$other</span>->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>-><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>->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>->status !== OrderStatus::<span class="t">Draft</span>) {
<span class="k">throw new</span> CannotModifyConfirmedOrder();
}
<span class="v">$this</span>->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>->items)) <span class="k">throw new</span> CannotConfirmEmptyOrder();
<span class="v">$this</span>->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>->items,
<span class="k">fn</span>(Money <span class="v">$sum</span>, OrderItem <span class="v">$item</span>) => <span class="v">$sum</span>-><span class="f">add</span>(<span class="v">$item</span>-><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>-><span class="f">find</span>(<span class="s">123</span>);
<span class="v">$item</span>->quantity = <span class="s">5</span>;
<span class="v">$itemRepo</span>-><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>-><span class="f">find</span>(<span class="v">$orderId</span>);
<span class="v">$order</span>-><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>-><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>->em-><span class="f">find</span>(<span class="t">Order</span>::<span class="k">class</span>, <span class="v">$id</span>->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>->em-><span class="f">persist</span>(<span class="v">$order</span>);
<span class="v">$this</span>->em-><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>->payment-><span class="f">charge</span>(<span class="v">$order</span>);
<span class="v">$this</span>->orders-><span class="f">save</span>(<span class="v">$order</span>);
<span class="v">$this</span>->events-><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>->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>-><span class="f">get</span>(<span class="s">'customer_id'</span>),
items: <span class="v">$request</span>-><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>->useCase-><span class="f">execute</span>(<span class="v">$input</span>);
<span class="k">return new</span> JsonResponse([<span class="s">'order_id'</span> => <span class="v">$output</span>->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> => <span class="v">$e</span>-><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>->stripe->charges-><span class="f">create</span>([...]);
<span class="k">return new</span> PaymentReceipt(<span class="v">$charge</span>->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>-><span class="f">apply</span>(<span class="v">$event</span>);
<span class="v">$this</span>->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>->pendingEvents;
<span class="v">$this</span>->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>-><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>->status !== OrderStatus::<span class="t">Draft</span>) <span class="k">throw new</span> ...;
<span class="v">$this</span>-><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> => <span class="v">$this</span>-><span class="f">whenOrderCreated</span>(<span class="v">$event</span>),
<span class="t">ItemAdded</span>::<span class="k">class</span> => <span class="v">$this</span>-><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>->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>->items[] = <span class="k">new</span> OrderItem(<span class="v">$e</span>->product, <span class="v">$e</span>->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>->store-><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>))-><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>-><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>-><span class="f">pullEvents</span>() <span class="k">as</span> <span class="v">$event</span>) {
<span class="v">$this</span>->store-><span class="f">append</span>(<span class="s">"order:</span><span class="v">$order</span><span class="s">->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>