<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: webdevmentor</title>
    <description>The latest articles on DEV Community by webdevmentor (@webdevmentor).</description>
    <link>https://dev.to/webdevmentor</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4027763%2Fbbbb37a2-e7a0-47c6-96a0-c5397745ade2.png</url>
      <title>DEV Community: webdevmentor</title>
      <link>https://dev.to/webdevmentor</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/webdevmentor"/>
    <language>en</language>
    <item>
      <title>POST Requests in Plain PHP sauber verarbeiten</title>
      <dc:creator>webdevmentor</dc:creator>
      <pubDate>Wed, 26 Aug 2026 18:01:59 +0000</pubDate>
      <link>https://dev.to/webdevmentor/post-requests-in-plain-php-sauber-verarbeiten-1nmg</link>
      <guid>https://dev.to/webdevmentor/post-requests-in-plain-php-sauber-verarbeiten-1nmg</guid>
      <description>&lt;p&gt;Wer mit PHP arbeitet, kommt früher oder später mit &lt;strong&gt;POST (Hypertext Transfer Protocol POST)&lt;/strong&gt; Requests in Berührung. Immer dann, wenn ein Benutzer Daten an einen Server sendet – beispielsweise über ein Kontaktformular, eine Registrierung oder einen Login – kommt in der Regel ein POST Request zum Einsatz.&lt;/p&gt;

&lt;p&gt;Im Gegensatz zu einem &lt;strong&gt;GET (Hypertext Transfer Protocol GET)&lt;/strong&gt; Request, der hauptsächlich zum Abrufen von Daten verwendet wird, dient ein POST Request dazu, neue Daten zu übermitteln oder bestehende Daten zu verändern. Die eigentliche Verarbeitung beginnt auf dem Server, wo PHP die übermittelten Werte unter anderem über das globale Array &lt;code&gt;$_POST&lt;/code&gt; bereitstellt.&lt;/p&gt;

&lt;p&gt;Für kleine Projekte genügt es häufig, die Werte direkt aus &lt;code&gt;$_POST&lt;/code&gt; auszulesen. Mit zunehmender Projektgröße verteilt sich diese Logik jedoch schnell über mehrere Dateien. Das Auslesen der Daten, das Umwandeln in die richtigen Datentypen, die Validierung und schließlich die Erstellung von Objekten vermischen sich häufig in einer einzigen Datei.&lt;/p&gt;

&lt;p&gt;Genau an diesem Punkt setzt dieser Artikel an.&lt;/p&gt;

&lt;p&gt;Wir entwickeln &lt;strong&gt;kein Framework&lt;/strong&gt; und bauen auch keine vollständige Anwendung. Stattdessen betrachten wir ausschließlich den Weg eines POST Requests – vom Eingang der Daten bis zu einem fertigen Domain-Model.&lt;/p&gt;

&lt;p&gt;Dabei kommen vier kleine Klassen zum Einsatz, die jeweils genau eine Aufgabe übernehmen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Request DTO (Data Transfer Object)&lt;/strong&gt; – nimmt die eingehenden Daten entgegen und übernimmt das Casting in die passenden Datentypen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validator&lt;/strong&gt; – prüft, ob die Daten vollständig und fachlich gültig sind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mapper&lt;/strong&gt; – übersetzt das Request DTO in das Domain-Model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain Model&lt;/strong&gt; – repräsentiert die eigentlichen Fachobjekte der Anwendung.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alles, was nach diesem Schritt passiert – etwa das Speichern in einer Datenbank, das Versenden einer E-Mail oder der Aufruf einer &lt;strong&gt;API (Application Programming Interface)&lt;/strong&gt; – gehört bewusst &lt;strong&gt;nicht&lt;/strong&gt; zum Thema dieses Artikels.&lt;/p&gt;

&lt;p&gt;Der vollständige Beispielcode ist im folgenden Repository verfügbar:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/webdevmentor/post-request-example" rel="noopener noreferrer"&gt;https://github.com/webdevmentor/post-request-example&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Im weiteren Verlauf verfolgen wir Schritt für Schritt den Weg eines POST Requests durch diese vier Klassen und sehen uns an, warum diese Trennung den Code übersichtlicher und wartbarer macht.&lt;/p&gt;




&lt;h2&gt;
  
  
  Das Request DTO
&lt;/h2&gt;

&lt;p&gt;Der erste Schritt nach dem Eingang eines POST Requests ist die Überführung der Daten in eine eigene Struktur.&lt;/p&gt;

&lt;p&gt;PHP stellt die eingehenden Daten zunächst als Array über &lt;code&gt;$_POST&lt;/code&gt; bereit. Dieses Array kommt direkt aus der Außenwelt und enthält zunächst nur Werte, die vom Client übertragen wurden. Für die weitere Verarbeitung in unserer Anwendung ist diese Form jedoch nicht besonders komfortabel.&lt;/p&gt;

&lt;p&gt;Genau hier kommt das &lt;strong&gt;Request DTO (Data Transfer Object)&lt;/strong&gt; zum Einsatz.&lt;/p&gt;

&lt;p&gt;Ein Request DTO hat eine einfache Aufgabe: Es nimmt die Daten aus dem eingehenden Request entgegen und überführt sie in eine definierte Struktur. Dabei können bereits erste technische Anpassungen erfolgen, beispielsweise das Entfernen von Leerzeichen oder die Umwandlung in die benötigten Datentypen.&lt;/p&gt;

&lt;p&gt;Das Request DTO kennt dabei ausschließlich die Struktur der eingehenden Daten. Es entscheidet nicht, ob die Daten fachlich gültig sind. Diese Aufgabe übernimmt später der Validator.&lt;/p&gt;

&lt;p&gt;Der Ablauf sieht vereinfacht so aus:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.webdevmentor.info%2Fimages%2Frequest-flow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.webdevmentor.info%2Fimages%2Frequest-flow.png" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Im Beispielprojekt wird ein Gästebuch-Eintrag verarbeitet. Deshalb heißt die konkrete Klasse &lt;code&gt;GuestbookEntryRequest&lt;/code&gt;. Der Name beschreibt den Anwendungsfall des Beispiels, während die Aufgabe der Klasse weiterhin die eines allgemeinen Request DTOs bleibt.&lt;/p&gt;

&lt;p&gt;Der erste Einsatz des Request DTOs befindet sich im Repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/webdevmentor/post-request-example/tree/step/02-introduction-request-dto" rel="noopener noreferrer"&gt;https://github.com/webdevmentor/post-request-example/tree/step/02-introduction-request-dto&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Die Klasse sieht in vereinfachter Form so aus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GuestbookEntryRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$firstname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$lastname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$homepage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$twitter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bereits der Constructor zeigt eine wichtige Eigenschaft des Request DTOs: Die erwarteten Daten sind explizit definiert.&lt;/p&gt;

&lt;p&gt;Statt später an verschiedenen Stellen mit beliebigen Array-Schlüsseln zu arbeiten:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'firstname'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;existiert nun ein Objekt mit klar beschriebenen Eigenschaften:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;firstname&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Die Anwendung arbeitet dadurch nicht mehr direkt mit dem ursprünglichen Request-Array, sondern mit einer festen Struktur.&lt;/p&gt;

&lt;p&gt;Durch die Typdefinitionen ist außerdem sichtbar, welche Werte erwartet werden. Ein Vorname ist beispielsweise ein &lt;code&gt;string&lt;/code&gt;, genau wie eine Nachricht oder eine Homepage URL (Uniform Resource Locator).&lt;/p&gt;

&lt;p&gt;Damit ist die erste Verantwortung des Request DTOs erfüllt: Aus unstrukturierten Request-Daten wird ein definiertes Objekt.&lt;/p&gt;

&lt;p&gt;Der nächste Schritt ist die Erstellung dieses Objekts aus den tatsächlichen POST-Daten.&lt;/p&gt;

&lt;p&gt;Dafür verwendet das Beispiel eine eigene Factory-Methode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;fromPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;self&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;firstname&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'firstname'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;lastname&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'lastname'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;homepage&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'homepage'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;twitter&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'twitter'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Die Methode &lt;code&gt;fromPost()&lt;/code&gt; kapselt den Übergang zwischen dem PHP-Array und unserem Objekt.&lt;/p&gt;

&lt;p&gt;Hier passieren bereits erste technische Verarbeitungsschritte:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fehlende Werte erhalten einen Standardwert (&lt;code&gt;''&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Leerzeichen am Anfang und Ende werden entfernt.&lt;/li&gt;
&lt;li&gt;Die Werte werden direkt den passenden Eigenschaften des DTOs zugeordnet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Diese Schritte sind bewusst technisch gehalten. Das Request DTO bereitet die Daten für die Anwendung auf, trifft aber noch keine fachlichen Entscheidungen.&lt;/p&gt;

&lt;p&gt;Ein leerer Name wird hier nicht abgelehnt. Eine ungültige Homepage wird hier nicht geprüft.&lt;/p&gt;

&lt;p&gt;Das DTO sagt lediglich:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Diese Daten sind angekommen und liegen jetzt in einer einheitlichen Struktur vor."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ob die Daten für unsere Anwendung gültig sind, entscheidet der nächste Schritt: der Validator.&lt;/p&gt;

&lt;p&gt;Den vollständigen Stand dieses Schritts findest du im Repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/webdevmentor/post-request-example/tree/step/02-introduction-request-dto" rel="noopener noreferrer"&gt;https://github.com/webdevmentor/post-request-example/tree/step/02-introduction-request-dto&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Der Validator
&lt;/h2&gt;

&lt;p&gt;Nachdem die Daten aus dem POST Request in ein Request DTO überführt wurden, liegt uns eine strukturierte Darstellung der Eingaben vor.&lt;/p&gt;

&lt;p&gt;Im ersten Beispiel wurden die Daten bereits validiert. Das ist ein wichtiger Schritt, denn eine Anwendung sollte nicht einfach beliebige Eingaben weiterverarbeiten.&lt;/p&gt;

&lt;p&gt;Das Problem ist jedoch nicht die Validierung selbst, sondern deren Platzierung.&lt;/p&gt;

&lt;p&gt;Wenn die Prüfung direkt zwischen dem Einlesen des Requests und der weiteren Verarbeitung stattfindet, vermischen sich verschiedene Verantwortlichkeiten:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Der Request muss entgegengenommen werden.&lt;/li&gt;
&lt;li&gt;Die Daten müssen in eine Struktur gebracht werden.&lt;/li&gt;
&lt;li&gt;Die Eingaben müssen fachlich geprüft werden.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mit dem nächsten Schritt wird diese Verantwortung aus der bisherigen Verarbeitung herausgelöst und in eine eigene Klasse verschoben: den &lt;strong&gt;Validator&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Der Validator kennt das Request DTO und überprüft ausschließlich, ob die enthaltenen Daten die erwarteten Regeln erfüllen.&lt;/p&gt;

&lt;p&gt;Der Ablauf erweitert sich damit:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.webdevmentor.info%2Fimages%2Frequest-flow-validator.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.webdevmentor.info%2Fimages%2Frequest-flow-validator.png" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Im Beispielprojekt heißt die konkrete Klasse &lt;code&gt;GuestbookEntryValidator&lt;/code&gt;. Auch hier verwenden wir im Artikel weiterhin den allgemeinen Begriff &lt;strong&gt;Validator&lt;/strong&gt;, da die Aufgabe unabhängig vom Gästebuch-Beispiel gleich bleibt.&lt;/p&gt;

&lt;p&gt;Die Klasse sieht vereinfacht so aus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GuestbookEntryValidator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;GuestbookEntryRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;firstname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;lastname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$errors&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Bitte gib deinen Namen ein.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$errors&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Bitte gib eine Nachricht ein.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;mb_strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$errors&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Die Nachricht darf maximal 500 Zeichen lang sein.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$errors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Die Aufgabe der Klasse ist bewusst überschaubar: Sie nimmt ein Request DTO entgegen und liefert eine Liste von Fehlern zurück.&lt;/p&gt;

&lt;p&gt;Dabei verändert der Validator die Daten nicht.&lt;/p&gt;

&lt;p&gt;Er macht aus einer ungültigen Nachricht keine gültige Nachricht. Er kürzt keine Texte und ergänzt keine fehlenden Werte.&lt;/p&gt;

&lt;p&gt;Er beantwortet lediglich die Frage:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Erfüllen diese Daten die Regeln unserer Anwendung?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Im Beispiel werden drei Regeln geprüft:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Es muss entweder ein Vorname oder ein Nachname angegeben werden.&lt;/li&gt;
&lt;li&gt;Eine Nachricht darf nicht leer sein.&lt;/li&gt;
&lt;li&gt;Eine Nachricht darf maximal 500 Zeichen enthalten.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Das Ergebnis der Prüfung ist ein Array mit Fehlermeldungen.&lt;/p&gt;

&lt;p&gt;Ein leeres Array bedeutet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Die Daten sind gültig.&lt;/p&gt;

&lt;p&gt;Enthält das Array Einträge, wurden Regeln verletzt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'Bitte gib eine Nachricht ein.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Die weitere Verarbeitung kann anhand dieses Ergebnisses entscheiden, ob mit den Daten fortgefahren wird.&lt;/p&gt;

&lt;p&gt;Auch hier bleibt die Verantwortung klar getrennt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Das Request DTO beschreibt die eingegangenen Daten.&lt;/li&gt;
&lt;li&gt;Der Validator prüft diese Daten.&lt;/li&gt;
&lt;li&gt;Die nächste Schicht entscheidet, was mit gültigen Daten passiert.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Den vollständigen Stand dieses Schritts findest du im Repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/webdevmentor/post-request-example/tree/step/03-extract-validator" rel="noopener noreferrer"&gt;https://github.com/webdevmentor/post-request-example/tree/step/03-extract-validator&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Der Mapper
&lt;/h2&gt;

&lt;p&gt;Nach dem Request DTO und der Validierung befinden sich die Daten in einem guten Zustand.&lt;/p&gt;

&lt;p&gt;Wir wissen jetzt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Die Daten wurden aus dem POST Request übernommen.&lt;/li&gt;
&lt;li&gt;Die Werte befinden sich in einer festen Struktur.&lt;/li&gt;
&lt;li&gt;Die Eingaben erfüllen unsere Validierungsregeln.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trotzdem sind wir noch nicht am Ziel.&lt;/p&gt;

&lt;p&gt;Das Request DTO beschreibt weiterhin die Daten aus Sicht des eingehenden Requests. Es ist ein Objekt der Transportebene.&lt;/p&gt;

&lt;p&gt;Unsere Anwendung benötigt jedoch ein Objekt, das die eigentliche Fachlichkeit beschreibt: das Domain-Model.&lt;/p&gt;

&lt;p&gt;Genau für diese Übersetzung ist der &lt;strong&gt;Mapper&lt;/strong&gt; zuständig.&lt;/p&gt;

&lt;p&gt;Der Mapper verbindet zwei unterschiedliche Welten:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.webdevmentor.info%2Fimages%2Frequest-dto-to-domain-model.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.webdevmentor.info%2Fimages%2Frequest-dto-to-domain-model.png" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Dabei kopiert ein Mapper nicht einfach nur Werte von einem Objekt in ein anderes. Er kann auch Anpassungen durchführen, die für das Domain-Model sinnvoll sind.&lt;/p&gt;

&lt;p&gt;Im Beispielprojekt heißt die konkrete Klasse &lt;code&gt;GuestbookEntryMapper&lt;/code&gt;. Die allgemeine Aufgabe bleibt jedoch immer gleich: Ein Request DTO wird in ein Domain-Model überführt.&lt;/p&gt;

&lt;p&gt;Die Klasse sieht vereinfacht so aus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GuestbookEntryMapper&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;fromRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;GuestbookEntryRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;GuestbookEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="nv"&gt;$displayName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;firstname&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;lastname&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GuestbookEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;displayName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$displayName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;homepage&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;homepage&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
                &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;homepage&lt;/span&gt;
                &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;twitter&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;twitter&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
                &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;twitter&lt;/span&gt;
                &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schon an der Signatur der Methode erkennen wir die Aufgabe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;fromRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GuestbookEntryRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GuestbookEntry&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Die Methode nimmt ein Request DTO entgegen und liefert ein Domain-Model zurück.&lt;/p&gt;

&lt;p&gt;Der Mapper ist damit die Stelle, an der entschieden wird, wie aus den eingehenden Daten ein Objekt der Anwendung entsteht.&lt;/p&gt;

&lt;p&gt;Ein gutes Beispiel dafür sind optionale Felder.&lt;/p&gt;

&lt;p&gt;Ein HTML-Formular liefert bei leeren Eingaben häufig einen leeren String:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;''&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Für die weitere Verarbeitung ist es jedoch oft hilfreicher, zwischen zwei Zuständen unterscheiden zu können:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kc"&gt;null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;bedeutet:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Es wurde kein Wert angegeben.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;während&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;''&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;lediglich bedeutet:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Es existiert ein String, der keine Zeichen enthält.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Der Mapper übernimmt diese kleine, aber wichtige Umwandlung:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;homepage&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;homepage&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
    &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;homepage&lt;/span&gt;
    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dadurch erhält das Domain-Model eine klarere Darstellung der Daten.&lt;/p&gt;

&lt;p&gt;Auch der Anzeigename wird erst an dieser Stelle zusammengesetzt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$displayName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;firstname&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;lastname&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Das Request DTO kennt lediglich die einzelnen Eingabefelder aus dem Formular.&lt;/p&gt;

&lt;p&gt;Die Bedeutung dieser Felder entsteht erst beim Mapping in das Domain-Model.&lt;/p&gt;

&lt;p&gt;Genau deshalb gehört diese Logik nicht in das Request DTO.&lt;/p&gt;

&lt;p&gt;Das DTO sagt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Diese Daten wurden übermittelt."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Der Mapper sagt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"So werden diese Daten innerhalb unserer Anwendung verwendet."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Das Domain-Model selbst muss anschließend nicht mehr wissen, woher die Daten ursprünglich kamen.&lt;/p&gt;

&lt;p&gt;Den vollständigen Stand dieses Schritts findest du im Repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/webdevmentor/post-request-example/tree/step/04-introduce-domain-object" rel="noopener noreferrer"&gt;https://github.com/webdevmentor/post-request-example/tree/step/04-introduce-domain-object&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Das Domain-Model
&lt;/h2&gt;

&lt;p&gt;Nach Request DTO, Validator und Mapper ist der eigentliche Übergang abgeschlossen.&lt;/p&gt;

&lt;p&gt;Die Daten wurden:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;aus dem POST Request übernommen,&lt;/li&gt;
&lt;li&gt;in eine definierte Struktur gebracht,&lt;/li&gt;
&lt;li&gt;validiert,&lt;/li&gt;
&lt;li&gt;und in ein Objekt der Anwendung übersetzt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An diesem Punkt spielt die ursprüngliche Quelle der Daten keine Rolle mehr.&lt;/p&gt;

&lt;p&gt;Das Domain-Model weiß nicht, ob die Daten ursprünglich aus einem HTML-Formular, einer API oder einer anderen Quelle stammen. Es beschreibt ausschließlich die Informationen, die unsere Anwendung benötigt.&lt;/p&gt;

&lt;p&gt;Im Beispielprojekt wird ein Gästebuch-Eintrag durch die Klasse &lt;code&gt;GuestbookEntry&lt;/code&gt; dargestellt.&lt;/p&gt;

&lt;p&gt;Die Klasse sieht bewusst einfach aus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GuestbookEntry&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$displayName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$homepage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$twitter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auf den ersten Blick wirkt diese Klasse vielleicht unspektakulär. Genau das ist aber ihre Stärke.&lt;/p&gt;

&lt;p&gt;Ein Domain-Model muss nicht automatisch eine große Klasse mit vielen Methoden sein. Seine wichtigste Aufgabe ist es, die Daten und die Bedeutung eines Fachobjekts innerhalb der Anwendung abzubilden.&lt;/p&gt;

&lt;p&gt;In unserem Beispiel beschreibt ein &lt;code&gt;GuestbookEntry&lt;/code&gt; genau die Informationen, die ein Eintrag im Gästebuch benötigt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;einen Anzeigenamen,&lt;/li&gt;
&lt;li&gt;optional eine Homepage,&lt;/li&gt;
&lt;li&gt;optional einen Twitter-Account,&lt;/li&gt;
&lt;li&gt;und die eigentliche Nachricht.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Auffällig sind die optionalen Werte:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$homepage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$twitter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Das &lt;code&gt;?string&lt;/code&gt; bedeutet, dass diese Eigenschaften entweder einen String oder &lt;code&gt;null&lt;/code&gt; enthalten können.&lt;/p&gt;

&lt;p&gt;Das passt zu der Entscheidung aus dem Mapper: Ein nicht ausgefülltes optionales Feld wird nicht als leerer String gespeichert, sondern als fehlender Wert dargestellt.&lt;/p&gt;

&lt;p&gt;Dadurch bleibt das Domain-Model eindeutig:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kc"&gt;null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;bedeutet:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Es gibt keinen Wert.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ein leerer String dagegen würde bedeuten:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Es gibt einen Wert, aber dieser besteht aus keinen Zeichen.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Diese Unterscheidung mag bei einem kleinen Gästebuch unwichtig wirken. In größeren Anwendungen wird sie jedoch schnell relevant.&lt;/p&gt;

&lt;p&gt;Der wichtigste Punkt ist aber die Trennung der Verantwortlichkeiten:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Das Request DTO kennt die eingehenden Daten.&lt;/li&gt;
&lt;li&gt;Der Validator kennt die Regeln für gültige Eingaben.&lt;/li&gt;
&lt;li&gt;Der Mapper kennt die Übersetzung zwischen beiden Welten.&lt;/li&gt;
&lt;li&gt;Das Domain-Model repräsentiert die eigentliche Fachlichkeit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Damit ist der POST Request verarbeitet, ohne dass eine einzelne Klasse alle Aufgaben übernehmen musste.&lt;/p&gt;

&lt;p&gt;Den vollständigen Stand dieses Schritts findest du im Repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/webdevmentor/post-request-example/tree/step/05-add-domain-model" rel="noopener noreferrer"&gt;https://github.com/webdevmentor/post-request-example/tree/step/05-add-domain-model&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dto</category>
      <category>validator</category>
      <category>domainmodel</category>
    </item>
    <item>
      <title>Docker für PHP, Apache und MariaDB – aus einem Container wird eine Entwicklungsumgebung</title>
      <dc:creator>webdevmentor</dc:creator>
      <pubDate>Mon, 13 Jul 2026 19:40:56 +0000</pubDate>
      <link>https://dev.to/webdevmentor/docker-fur-php-apache-und-mariadb-aus-einem-container-wird-eine-entwicklungsumgebung-4oic</link>
      <guid>https://dev.to/webdevmentor/docker-fur-php-apache-und-mariadb-aus-einem-container-wird-eine-entwicklungsumgebung-4oic</guid>
      <description>&lt;p&gt;Im &lt;a href="https://www.webdevmentor.info/posts/php-apache-docker/" rel="noopener noreferrer"&gt;letzten Beitrag&lt;/a&gt; unserer Docker-Serie haben wir eine einfache PHP-Entwicklungsumgebung mit Docker erstellt. Für kleine Projekte oder erste Experimente ist das bereits völlig ausreichend und hat den großen Vorteil, dass keine lokale PHP-Installation mehr notwendig ist.&lt;/p&gt;

&lt;p&gt;Sobald eine Anwendung Daten speichern soll, stößt dieses einfache Setup allerdings an seine Grenzen. Neben dem Webserver benötigen wir nun auch eine Datenbank. Außerdem muss PHP die passenden Erweiterungen mitbringen, damit später überhaupt eine Verbindung zur Datenbank aufgebaut werden kann.&lt;/p&gt;

&lt;p&gt;In diesem Beitrag erweitern wir deshalb unsere bestehende Docker-Umgebung um einen MariaDB-Container und passen unseren Webcontainer entsprechend an.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unsere compose.yml erweitern
&lt;/h2&gt;

&lt;p&gt;Im letzten Beitrag bestand unsere &lt;code&gt;compose.yml&lt;/code&gt; lediglich aus einem Webcontainer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php:8.5-apache&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/var/www/html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Für unsere Datenbank ergänzen wir nun einen zweiten Service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/var/www/html&lt;/span&gt;

  &lt;span class="na"&gt;mariadb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mariadb:10.11&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MARIADB_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
      &lt;span class="na"&gt;MARIADB_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
      &lt;span class="na"&gt;MARIADB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
      &lt;span class="na"&gt;MARIADB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mariadb_data:/var/lib/mysql&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mariadb_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aus einem einzelnen Container ist damit eine kleine Entwicklungsumgebung geworden. Der Webcontainer kümmert sich um PHP und Apache, während MariaDB unsere Daten speichert.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warum braucht die Datenbank ein eigenes Volume?
&lt;/h2&gt;

&lt;p&gt;Vielleicht ist dir der neue Abschnitt am Ende der Datei bereits aufgefallen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mariadb_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ein Docker-Container ist grundsätzlich austauschbar. Wird ein Container gelöscht, gehen auch alle darin gespeicherten Daten verloren.&lt;/p&gt;

&lt;p&gt;Damit uns das mit unserer Datenbank nicht passiert, verwenden wir ein &lt;strong&gt;Docker Volume&lt;/strong&gt;. Man kann es sich zunächst wie einen Speicherort vorstellen, den Docker unabhängig vom eigentlichen Container verwaltet.&lt;/p&gt;

&lt;p&gt;Der MariaDB-Container speichert seine Daten nicht im Container selbst, sondern im Volume &lt;code&gt;mariadb_data&lt;/code&gt;. Selbst wenn der Container später gelöscht oder neu erstellt wird, bleiben unsere Tabellen und Daten erhalten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Den Webcontainer anpassen
&lt;/h2&gt;

&lt;p&gt;Vielleicht ist dir noch eine weitere Änderung aufgefallen.&lt;/p&gt;

&lt;p&gt;Im letzten Beitrag haben wir den Webcontainer direkt über das &lt;code&gt;image&lt;/code&gt;-Attribut gestartet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php:8.5-apache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jetzt verwenden wir stattdessen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Der Grund ist einfach: Unser PHP-Container benötigt jetzt zusätzlich die Erweiterungen für die Kommunikation mit MySQL-kompatiblen Datenbanken. Dafür erstellen wir erstmals ein eigenes Dockerfile.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; php:8.5-apache&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;docker-php-ext-install mysqli pdo pdo_mysql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mehr ist an dieser Stelle zunächst gar nicht notwendig.&lt;/p&gt;

&lt;p&gt;Während der Entwicklung möchten wir unseren Quellcode möglichst komfortabel bearbeiten. Deshalb kopieren wir ihn nicht in das Image, sondern binden ihn direkt in den Container ein. Änderungen an den PHP-Dateien stehen dadurch sofort zur Verfügung und wir müssen das Image nicht nach jeder kleinen Anpassung neu erstellen.&lt;/p&gt;

&lt;p&gt;Dafür verwenden wir dieses Volume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/var/www/html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Diese Art der Einbindung bezeichnet Docker als &lt;strong&gt;Bind Mount&lt;/strong&gt;. Dabei wird unser Projektverzeichnis direkt mit dem Verzeichnis &lt;code&gt;/var/www/html&lt;/code&gt; im Container verbunden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Die Container starten
&lt;/h2&gt;

&lt;p&gt;Sind beide Dateien vorbereitet, genügt wie gewohnt ein einziger Befehl.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker erstellt zunächst unser PHP-Image aus dem Dockerfile, lädt anschließend das MariaDB-Image herunter, legt das Docker Volume an und startet beide Container.&lt;/p&gt;

&lt;p&gt;Mit&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;lässt sich anschließend überprüfen, ob beide Dienste erfolgreich laufen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wie findet PHP die Datenbank?
&lt;/h2&gt;

&lt;p&gt;Obwohl Webserver und Datenbank in unterschiedlichen Containern laufen, können sie problemlos miteinander kommunizieren.&lt;/p&gt;

&lt;p&gt;Docker Compose erstellt dafür automatisch ein gemeinsames Netzwerk. Jeder Service ist innerhalb dieses Netzwerks über seinen Servicenamen erreichbar. Unsere Datenbank ist deshalb nicht unter &lt;code&gt;localhost&lt;/code&gt;, sondern unter &lt;code&gt;mariadb&lt;/code&gt; erreichbar.&lt;/p&gt;

&lt;p&gt;Eine Verbindung mit PDO könnte beispielsweise so aussehen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$dsn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'mysql:host=mariadb;dbname=app;charset=utf8mb4'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'app'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'secret'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$pdo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PDO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$dsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Den Hostnamen müssen wir dabei nicht selbst konfigurieren. Docker übernimmt die interne Namensauflösung automatisch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warum MariaDB?
&lt;/h2&gt;

&lt;p&gt;In diesem Beitrag verwenden wir bewusst MariaDB. Die Datenbank ist vollständig Open Source, weit verbreitet und für die meisten PHP-Anwendungen ein direkter Ersatz für MySQL.&lt;/p&gt;

&lt;p&gt;Soll stattdessen MySQL verwendet werden, genügt in den meisten Fällen bereits eine kleine Änderung.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:8.4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Außerdem müssen die Umgebungsvariablen von &lt;code&gt;MARIADB_&lt;/code&gt; auf &lt;code&gt;MYSQL_&lt;/code&gt; angepasst werden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dateien aus dem Artikel
&lt;/h2&gt;

&lt;p&gt;Du möchtest die Entwicklungsumgebung direkt ausprobieren? Alle benötigten Dateien aus diesem Artikel findest du als Gist:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/webdevmentor/b77d394f064f1ecc7c1ebaa5d9c6cb63" rel="noopener noreferrer"&gt;PHP Apache MariaDB Development Environment with Docker Compose auf GitHub.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fazit
&lt;/h2&gt;

&lt;p&gt;Mit wenigen zusätzlichen Zeilen ist aus unserem einzelnen PHP-Container eine vollständige Entwicklungsumgebung geworden. Docker Compose startet Webserver und Datenbank gemeinsam, Docker Volumes sorgen dafür, dass unsere Daten dauerhaft erhalten bleiben, und über das Dockerfile können wir unser PHP-Image an die Anforderungen unseres Projekts anpassen.&lt;/p&gt;

&lt;p&gt;Damit steht eine solide Grundlage für die weitere Entwicklung bereit – und das alles mit einem einzigen Befehl.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>mariadb</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
