{"id":321,"date":"2015-09-05T13:28:19","date_gmt":"2015-09-05T11:28:19","guid":{"rendered":"http:\/\/staratnight.de\/blog\/?p=321"},"modified":"2018-12-02T16:20:23","modified_gmt":"2018-12-02T14:20:23","slug":"crud-in-php-mit-repositories-und-models","status":"publish","type":"post","link":"https:\/\/staratnight.de\/blog\/crud-in-php-mit-repositories-und-models\/","title":{"rendered":"CRUD in PHP mit Repositories und Models"},"content":{"rendered":"<p>Aufbauend auf dem Artikel zum Thema\u00a0<a href=\"http:\/\/staratnight.de\/blog\/crud-in-php-und-mysql-mit-pdo-erstellen\/\">CRUD in PHP und MySQL mit PDO erstellen<\/a> m\u00f6chte ich hier noch ein wenig mehr auf die Strukturierung eines solchen Projekts eingehen.<\/p>\n<p>Ich verwende dabei die Idee des <a href=\"https:\/\/de.wikipedia.org\/wiki\/Repository_%28Entwurfsmuster%29\">Repository-Patterns<\/a> unter der Verwendung einzelner Model-Klassen f\u00fcr jede Tabelle.<\/p>\n<p>&nbsp;<\/p>\n<p>Als Beispiele verwende ich hier erneut Tabellen aus meiner Kochbuch-Anwendung. Hierbei werden Rezepte (Recipes) und Zutaten (Ingredients) verwaltet.<\/p>\n<p><!--more--><\/p>\n<h1>Model<\/h1>\n<p>Zun\u00e4chst erstellen wir eine Modelklasse zu jeder unserer Tabellen. Haben wir z.B. eine Ingredient-Tabelle mit den Spalten Name, Anzahl, Einheit, Kommentar und Rezept-ID, sieht die Klasse dazu folgenderma\u00dfen aus:<\/p>\n<pre class=\"lang:php decode:true\">&lt;?php namespace model;\r\n\r\nclass Ingredient\r\n{\r\n\tprivate $Id;\r\n\tprivate $Name;\r\n\tprivate $Amount;\r\n\tprivate $Entity;\r\n\tprivate $Comment;\r\n\tprivate $RecipeId;\r\n[...]<\/pre>\n<p>Zu jedem Feld der Klasse gibt es nun noch passende Getter und Setter.<\/p>\n<p>Hier die Getter und Setter am Beispiel des Felds Name:<\/p>\n<pre class=\"lang:php decode:true\">public function getName()\r\n{\r\n\treturn $this-&gt;Name;\r\n}\r\n\r\n\/**\r\n * Set the value of [name] column.\r\n *\r\n * @param string $Name new value\r\n *\r\n * @return Ingredient The current object (for fluent API support)\r\n *\/\r\npublic function setName($Name)\r\n{\r\n\tif ($Name !== null &amp;&amp; is_numeric($Name))\r\n\t{\r\n\t\t$Name = (string)$Name;\r\n\t}\r\n\r\n\tif ($this-&gt;Name !== $Name)\r\n\t{\r\n\t\t$this-&gt;Name = $Name;\r\n\t}\r\n\r\n\treturn $this;\r\n}\r\n<\/pre>\n<p>Beim Setter wird der \u00fcbergebene Parameter noch auf den passenden Typ gepr\u00fcft und ggf. umgewandelt.<\/p>\n<p>&nbsp;<\/p>\n<p>Das Model kann der\u00fcber hinaus noch weitere hilfreiche Methoden enthalten. Etwa eine Methode <em>clear()<\/em>, welche alle Werte auf <em>null<\/em> setzt oder Umwandlungsmethoden wie toArray():<\/p>\n<pre class=\"lang:php decode:true \">\/**\r\n * Exports the object as an array.\r\n *\r\n * @return array an associative array containing the field names (as keys) and field values\r\n *\/\r\npublic function toArray()\r\n{\r\n\t$result = array(\r\n\t\t'Id' =&gt; $this-&gt;getId(),\r\n\t\t'Name' =&gt; $this-&gt;getName(),\r\n\t\t'Amount' =&gt; $this-&gt;getAmount(),\r\n\t\t'Entity' =&gt; $this-&gt;getEntity(),\r\n\t\t'Comment' =&gt; $this-&gt;getComment(),\r\n\t\t'RecipeId' =&gt; $this-&gt;getRecipeId()\r\n\t);\r\n\r\n\treturn $result;\r\n}<\/pre>\n<p>Auch sind Factory-Methoden denkbar:<\/p>\n<pre class=\"lang:php decode:true\">\/**\r\n * Creates the object from an array.\r\n *\r\n * @param $arr array an associative array containing the field names (as keys) and field values\r\n *\r\n * @return Ingredient the created Ingredient object\r\n *\/\r\npublic static function fromArray($arr)\r\n{\r\n\t$ingredient = new Ingredient();\r\n\r\n\tif (isset($arr['Id']))\r\n\t{\r\n\t\t$ingredient-&gt;setId($arr['Id']);\r\n\t}\r\n\r\n\tif (isset($arr['Name']))\r\n\t{\r\n\t\t$ingredient-&gt;setName($arr['Name']);\r\n\t}\r\n\r\n\tif (isset($arr['Amount']))\r\n\t{\r\n\t\t$ingredient-&gt;setAmount($arr['Amount']);\r\n\t}\r\n\r\n\tif (isset($arr['Entity']))\r\n\t{\r\n\t\t$ingredient-&gt;setEntity($arr['Entity']);\r\n\t}\r\n\r\n\tif (isset($arr['Comment']))\r\n\t{\r\n\t\t$ingredient-&gt;setComment($arr['Comment']);\r\n\t}\r\n\r\n\tif (isset($arr['RecipeId']))\r\n\t{\r\n\t\t$ingredient-&gt;setRecipeId($arr['RecipeId']);\r\n\t}\r\n\r\n\treturn $ingredient;\r\n}<\/pre>\n<pre class=\"lang:php decode:true\">\/**\r\n * Creates the object from an JSON string.\r\n *\r\n * @param $jsonString string a JSON string containing the field names (as keys) and field values\r\n *\r\n * @return Ingredient the object containing the specified values\r\n *\/\r\npublic static function fromJson($jsonString)\r\n{\r\n\t$array = json_decode($jsonString, true);\r\n\t$model = self::fromArray($array);\r\n\r\n\treturn $model;\r\n}<\/pre>\n<p>&nbsp;<\/p>\n<p>Das Model dient allerdings nur dazu, die Daten aus der Datenbanktabelle zu tragen. Es enth\u00e4lt keinerlei Businesslogik oder -funktionen.<\/p>\n<p>&nbsp;<\/p>\n<h1>Repository<\/h1>\n<p>Das Repository ist daf\u00fcr zust\u00e4ndig, die Daten aus der Datenbank zu lesen oder in ihr zu schreiben, bzw. zu aktualisieren. Dabei arbeitet es entweder mit einfachen Werten (Strings, Integer, &#8230;) oder eben mit unseren Model-Klassen.<\/p>\n<p>Die Methoden der Repository-Klassen sollten einem einheitlichen Muster folgen. So sollten alle Methoden, die einen Datensatz aus der Tabelle auslesen <em>find&#8230;()<\/em> hei\u00dfen, alle die einen Datensatz l\u00f6schen <em>delete&#8230;()<\/em>, alle die einen Datensatz aktualisieren <em>update&#8230;()<\/em> und das neu Anlegen sollte <em>create()<\/em> oder <em>save()<\/em> hei\u00dfen. Wenn die Methoden in allen verwendeten Repositories einheitlich benannt sind, f\u00e4llt uns die Verwendung hinterher leichter.<\/p>\n<p>&nbsp;<\/p>\n<p>Das Repository f\u00fcr unser Ingredient-Model etwa k\u00f6nnte die folgenden Methoden haben:<\/p>\n<pre class=\"lang:php decode:true\">&lt;?php namespace repository;\r\n\r\nuse model\\Ingredient;\r\nuse PDO;\r\n\r\nclass IngredientRepository\r\n{\r\n\t\/**\r\n\t * Retrieves the ingredients from the database by name.\r\n\t *\r\n\t * @param PDO $con connection to the database\r\n\t * @param string $name The name of the Ingredient\r\n\t *\r\n\t * @return Ingredient The Ingredient found.\r\n\t *\/\r\n\tpublic function findIngredientsByName(PDO $con, $name)\r\n\t{\r\n[...]\r\n\t}\r\n\r\n    \/**\r\n     * Retrieves the ingredients from the database by id of the corresponding recipe.\r\n     *\r\n     * @param PDO $con connection to the database\r\n     * @param int $recipeId The id of the recipe the ingredients should be returned for.\r\n     *\r\n     * @return array The ingredients found.\r\n     *\/\r\n    public function findIngredientsByRecipeId(PDO $con, $recipeId)\r\n    {\r\n[...]\r\n    }\r\n\r\n    \/**\r\n     * Retrieves all ingredients from the database.\r\n     *\r\n     * @param PDO $con connection to the database\r\n     *\r\n     * @return array The ingredients found.\r\n     *\/\r\n    public function findAllIngredients(PDO $con)\r\n    {\r\n[...]\r\n    }\r\n\r\n    \/**\r\n     * Saves the given Ingredient into the database.\r\n     * When a id is specified, the ingredient is updated. It is created otherwise with a new id.\r\n     *\r\n     * @param PDO $con connection to the database\r\n     * @param Ingredient $ingredient the ingredient to save.\r\n     *\r\n     * @return Ingredient The saved ingredient. Contains generated id when created.\r\n     *\/\r\n    public function save(PDO $con, Ingredient $ingredient)\r\n    {\r\n[...]\r\n    }\r\n\r\n    \/**\r\n     * Creates the Ingredient in the database.\r\n     *\r\n     * @param PDO $con connection to the database\r\n     * @param Ingredient $ingredient The Ingredient to save.\r\n     *\/\r\n    private function createIngredient(PDO $con, Ingredient $ingredient)\r\n    {\r\n[...]\r\n    }\r\n\r\n    \/**\r\n     * Updates the Ingredient in the database.\r\n     *\r\n     * @param PDO $con connection to the database\r\n     * @param Ingredient $ingredient The Ingredient to update.\r\n     *\/\r\n    private function updateIngredient(PDO $con, Ingredient $ingredient)\r\n    {\r\n[...]\r\n    }\r\n\r\n    \/**\r\n     * Deletes the Ingredient in the database.\r\n     *\r\n     * @param PDO $con connection to the database\r\n     * @param int $id The name of the Ingredient to delete.\r\n     *\/\r\n    public function deleteIngredient(PDO $con, $id)\r\n    {\r\n[...]\r\n    }\r\n}<\/pre>\n<p>Ich verwende hier als Schnittstelle <em>save()<\/em> zum Erstellen oder Aktualisieren eines Datensatzes. Der Aufrufer muss sich dann nicht darum k\u00fcmmern, ob der Datensatz nun neu ist und <em>create()<\/em> aufrufen muss oder ob er eine Aktualisierung durch <em>update()<\/em> anzusto\u00dfen hat.<\/p>\n<p>Die <em>save()<\/em>-Methode sieht dabei folgenderma\u00dfen aus:<\/p>\n<pre class=\"lang:php decode:true \">\/**\r\n * Saves the given Ingredient into the database.\r\n * When a id is specified, the ingredient is updated. It is created otherwise with a new id.\r\n *\r\n * @param PDO $con connection to the database\r\n * @param Ingredient $ingredient the ingredient to save.\r\n *\r\n * @return Ingredient The saved ingredient. Contains generated id when created.\r\n *\/\r\npublic function save(PDO $con, Ingredient $ingredient)\r\n{\r\n    $id = $ingredient-&gt;getId();\r\n    if (!isset($id) || $id == 0) {\r\n        $id = $this-&gt;generateId($con);\r\n        $ingredient-&gt;setId($id);\r\n\r\n        $this-&gt;createIngredient($con, $ingredient);\r\n    } else {\r\n        $this-&gt;updateIngredient($con, $ingredient);\r\n    }\r\n\r\n    return $ingredient;\r\n}<\/pre>\n<p>Die beiden internen Methoden <em>createIngredient()<\/em> und <em>updateIngredient()<\/em> sind private und von au\u00dfen somit nicht erreichbar.<\/p>\n<p>&nbsp;<\/p>\n<h1>Verwaltung des PDO-Objekts<\/h1>\n<p>Wie wir ein PDO-Objekt f\u00fcr den Zugriff auf die Datenbank erstellen, habeich im Artikel <a href=\"http:\/\/staratnight.de\/blog\/crud-in-php-und-mysql-mit-pdo-erstellen\/\">CRUD in PHP und MySQL mit PDO erstellen<\/a> bereits beschrieben.<\/p>\n<p>Im oben vorgestellten Repository wird das PDO-Objekt als Parameter \u00fcbergeben. Das ist auch gut so, dann kann es in Unit-Tests einfach durch eine Mock- oder Stub-Implementierung ersetzt werden (wem das jetzt nichts sagt, der sollte das mal auf unserem <a href=\"http:\/\/invidit.de\/blog\/von-tests-und-komponenten\/\">Blog zum Thema Code-Qualit\u00e4t<\/a> nachlesen).<\/p>\n<p>Die Frage ist aber, wie der Aufrufer (ein Controller oder besser ein Service) das PDO-Objekt bekommt.<\/p>\n<p>Daf\u00fcr gibt es zwei L\u00f6sungen<\/p>\n<ol>\n<li>Wir erstellen das PDO-Objekt zentral in unserer index.php und \u00fcberall dort, wo wir es ben\u00f6tigen, definieren wir es als <strong>global.<br \/>\n<\/strong>Hier ein Beispiel:<\/p>\n<pre class=\"lang:php decode:true \" title=\"index.php\">try\r\n{\r\n  $con = new PDO('mysql:host=localhost;dbname=datenbankname', 'benutzer', 'passwort'); \r\n}\r\ncatch(PDOException $e)\r\n{\r\n  \/\/ Fehlerbehandlung \r\n}<\/pre>\n<pre class=\"lang:php decode:true \" title=\"IngredientController.php\">public static function saveIngredient($ingredientJSON)\r\n{\r\n\tglobal $con;\r\n\r\n\t$ingredientRepository = new IngredientRepository();\r\n\r\n\t$ingredient = Ingredient::fromJson($ingredientJSON);\r\n\t$ingredientRepository-&gt;save($con, $ingredient);\r\n[...]<\/pre>\n<\/li>\n<li>Wir erstellen eine eigene Klasse, z.B. Database. Diese hat eine statische Methode connect(), die die Verbindung herstellt, falls nock keine bestehen sollte. Hier der Code dazu:\n<pre class=\"lang:php decode:true \">&lt;?php namespace repository;\r\nuse PDO;\r\nuse PDOException;\r\n\r\nclass Database\r\n{\r\n\tprivate static $dbServer = 'localhost';\r\n\tprivate static $dbName = 'datenbankname';\r\n\tprivate static $dbUsername = 'benutzer';\r\n\tprivate static $dbUserPassword = 'passwort';\r\n\r\n\tprivate static $con = null;\r\n\r\n\tprivate function __construct()\r\n\t{ \/* static class should not be instantiated *\/}\r\n\r\n\tpublic static function connect()\r\n\t{\r\n\t\t\/\/ Only One connection for the application\r\n\t\tif (null == self::$con)\r\n\t\t{\r\n\t\t\t$options = array\r\n\t\t\t(\r\n\t\t\t\tPDO::MYSQL_ATTR_INIT_COMMAND =&gt; 'SET NAMES utf8',\r\n\t\t\t);\r\n\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tself::$con = new PDO(\"mysql:host=\" . self::$dbServer . \";\" . \"dbname=\" . self::$dbName, self::$dbUsername, self::$dbUserPassword, $options);\r\n\t\t\t\tself::$con-&gt;setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);\r\n\t\t\t}\r\n\t\t\tcatch (PDOException $e)\r\n\t\t\t{\r\n\t\t\t\tdie($e-&gt;getMessage());\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn self::$con;\r\n\t}\r\n\r\n\tpublic static function disconnect()\r\n\t{\r\n\t\tself::$con = null;\r\n\t}\r\n}<\/pre>\n<\/li>\n<\/ol>\n<p>Bisher habe ich die erste L\u00f6sung verwendet. Sie funktioniert, f\u00fchlt sich aber irgendwie seltsam an. Ich werde nun nur noch die L\u00f6sung \u00fcber die statische Fabrik verwenden.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Aufbauend auf dem Artikel zum Thema\u00a0CRUD in PHP und MySQL mit PDO erstellen m\u00f6chte ich hier noch ein wenig mehr auf die Strukturierung&hellip;<\/p>\n","protected":false},"author":2,"featured_media":431,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[14],"tags":[],"class_list":["post-321","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-php"],"_links":{"self":[{"href":"https:\/\/staratnight.de\/blog\/wp-json\/wp\/v2\/posts\/321","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/staratnight.de\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/staratnight.de\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/staratnight.de\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/staratnight.de\/blog\/wp-json\/wp\/v2\/comments?post=321"}],"version-history":[{"count":6,"href":"https:\/\/staratnight.de\/blog\/wp-json\/wp\/v2\/posts\/321\/revisions"}],"predecessor-version":[{"id":328,"href":"https:\/\/staratnight.de\/blog\/wp-json\/wp\/v2\/posts\/321\/revisions\/328"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/staratnight.de\/blog\/wp-json\/wp\/v2\/media\/431"}],"wp:attachment":[{"href":"https:\/\/staratnight.de\/blog\/wp-json\/wp\/v2\/media?parent=321"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/staratnight.de\/blog\/wp-json\/wp\/v2\/categories?post=321"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/staratnight.de\/blog\/wp-json\/wp\/v2\/tags?post=321"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}