Doctrine2 ORM standalone Demo
Erstellung einer Webanwendung mit Doctrine 2 ORM ohne Symfony.
Datum: 2014-04, Tags: PHP, Doctine 2, Twig, HowtoUpdate 2020-05: Dieser Artikel wurde überarbeitet und auf die Komponenten PHP Version 7.2.5 und doctrine ORM Version 2.7.3, sowie doctrine DBAL Version 2.10.2 aktualisiert. Als Webserver wird nicht mehr apache2 eingesetzt, sondern der PHP-eigene.
The sources are availlable on github: https://github.com/limitland/Doctrine-2-standalone-Demo.
Mit dem Symfony Framework zu arbeiten, ist für mich eine große Erleichterung. Der Schritt hin zur Mikro-Architektur, den Symfony mit der Version 4 gegangen ist, erleichtert das Arbeiten mit Symfony für kleine Webanwendungen. Trotzdem möchte ich manchmal nicht das komplette Framework einsetzen, sondern nur einzelne Komponenten. Das sind insbesondere Twig als Template-Engine, sowie Doctrine DBAL und ORM. Gerade der Object Relation Mapper ORM erleichtert mir den Umgang mit der Datenbank erheblich.
Beide Projekte können auch unabhängig von Symfony für eine einfache Webanwendung eingesetzt werden. Wie so eine Webanwendung aufsetzt werden kann, beschreibe ich in dem folgenden Artikel.
Installationen
Für die Webanwendung benutze ich meine lokale Entwicklungsumgebung mit PHP Version 7.2.5
.
Die Installation der Komponenten im Projekt wird durch den Abhängigkeiten-Manager
Composer bewerkstelligt.
Dieser benötigt die PHP-Erweiterung PHAR
. Das wird bei meiner Distribution als Paket
php7-phar
installiert. Weiterhin wird die Konfigurations-Sprache Yaml benutzt, dafür wird die PHP-Erweiterung
YAML
benötigt. Dieses muss ggf. per sudo pecl insall yaml
kompiliert und in der php.ini
als extension aktiviert werden:
~php -v PHP 7.2.5 (cli) ( NTS ) Copyright (c) 1997-2018 The PHP Group Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies with Zend OPcache v7.2.5, Copyright (c) 1999-2018, by Zend Technologies with Xdebug v2.7.0, Copyright (c) 2002-2019, by Derick Rethans ~sudo pecl install yaml downloading yaml-2.1.0.tgz ... Starting to download yaml-2.1.0.tgz (39,439 bytes) ..........done: 39,439 bytes 8 source files, building running: phpize Configuring for: PHP Api Version: 20170718 Zend Module Api No: 20170718 Zend Extension Api No: 320170718 Please provide the prefix of libyaml installation [autodetect] : building in /tmp/pear/temp/pear-build-root3OzYLL/yaml-2.1.0 running: /tmp/pear/temp/yaml/configure --with-php-config=/usr/bin/php-config --with-yaml ... Build process completed successfully Installing '/usr/lib64/php7/extensions/yaml.so' install ok: channel://pecl.php.net/yaml-2.1.0 configuration option "php_ini" is not set to php.ini location You should add "extension=yaml.so" to php.ini
; extension=yaml.dll on windiows extension=yaml.so
Projekt Setup
Als Erstes wird eine einfache Projekt-Struktur erzeugt, die gleich PSR-4 unterstützt.
~mkdir doctrine2-standalone ~cd doctrine2-standalone ~/doctrine2-standalonemkdir -p config src/Controller src/Entity templates public
Dann wird der Composer installiert, die benötigten Komponenten konfiguriert und die Abhängigkeiten installiert.
~/doctrine2-standalonephp -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" ~/doctrine2-standalonephp -r "if (hash_file('sha384', 'composer-setup.php') === 'e0012edf3e80b6978849f5eff0d4b4e4c79ff1609dd1e613307e16318854d24ae64f26d17af3ef0bf7cfb710ca74755a') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" Installer verified ~/doctrine2-standalonephp composer-setup.php All settings correct for using Composer Downloading... Composer (version 1.10.6) successfully installed to: /home/jens/doctrine2-standalone/composer.phar Use it: php composer.phar ~/doctrine2-standalonephp -r "unlink('composer-setup.php');" ~/doctrine2-standalonephp composer.phar require php>=7.2 twig/twig doctrine/dbal doctrine/orm Using version ^7.2 for php Using version ^3.0 for twig/twig Using version ^2.10 for doctrine/dbal Using version ^2.7 for doctrine/orm ./composer.json has been created Loading composer repositories with package information Updating dependencies (including require-dev) Package operations: 20 installs, 0 updates, 0 removals - Installing ocramius/package-versions (1.4.2): Loading from cache - Installing symfony/polyfill-ctype (v1.17.0): Loading from cache - Installing symfony/polyfill-mbstring (v1.17.0): Loading from cache - Installing twig/twig (v3.0.3): Loading from cache - Installing psr/container (1.0.0): Loading from cache - Installing symfony/service-contracts (v2.1.2): Downloading (100%) - Installing symfony/polyfill-php73 (v1.17.0): Loading from cache - Installing symfony/console (v5.0.8): Loading from cache - Installing doctrine/lexer (1.2.1): Downloading (100%) - Installing doctrine/annotations (1.10.3): Downloading (100%) - Installing doctrine/reflection (1.2.1): Loading from cache - Installing doctrine/event-manager (1.1.0): Loading from cache - Installing doctrine/collections (1.6.5): Downloading (100%) - Installing doctrine/cache (1.10.1): Downloading (100%) - Installing doctrine/persistence (2.0.0): Downloading (100%) - Installing doctrine/instantiator (1.3.0): Loading from cache - Installing doctrine/inflector (1.4.2): Downloading (100%) - Installing doctrine/dbal (2.10.2): Loading from cache - Installing doctrine/common (3.0.0): Downloading (100%) - Installing doctrine/orm (v2.7.3): Downloading (100%) symfony/service-contracts suggests installing symfony/service-implementation symfony/console suggests installing symfony/event-dispatcher symfony/console suggests installing symfony/lock symfony/console suggests installing symfony/process symfony/console suggests installing psr/log (For using the console logger) doctrine/cache suggests installing alcaeus/mongo-php-adapter (Required to use legacy MongoDB driver) doctrine/orm suggests installing symfony/yaml (If you want to use YAML Metadata Mapping Driver) Writing lock file Generating autoload files 13 packages you are using are looking for funding. Use the `composer fund` command to find out more! ~/doctrine2-standalonephp composer.phar require --ignore-platform-reqs ext-yaml Using version * for ext-yaml ./composer.json has been updated Loading composer repositories with package information Updating dependencies (including require-dev) Nothing to install or update Writing lock file Generating autoload files 13 packages you are using are looking for funding. Use the `composer fund` command to find out more! ocramius/package-versions: Generating version class... ocramius/package-versions: ...done generating version class
Dabei wird das vendor
Verzeichnis angelegt und besonders darin die Datei
autoload.php
, über die die Eingebungenen Bibliotheken geladen werden können.
Aufbau der Anwendung
Zuerst wird die Konfigurationsdatei config/parameters.yaml
erstellt und darin grundlegende Anwendungseinstellungen und der Datenbank-Zugriff konfiguriert.
locale: 'de_DE' site: name: 'Doctrine 2 standalone Demo' database: dbname: 'demodb' user: 'root' password: 'password' host: 'localhost' driver: 'pdo_mysql' charset: 'utf8' driverOptions: 1002: 'SET NAMES utf8'
Im webroot wird die Datei index.php
erzeugt, die in diesem Fall einfach nur auf den
Frontend-Controller verweist:
<?php require_once __DIR__ . '/../src/Controller/FrontendController.php';
Dieser Frontend Controller leitet sich von einer Klasse "Application" ab, in der wiederum die Requests und Responses vorbereitet
werden. Ansonsten wird in diesem Fall einfach nur die Konfiguration an das Twig template index.html.twig
übergeben, die Ansicht gerendert und ausgegeben.
<?php namespace App\Controller; require_once __DIR__ . '/../Application.php'; use App\Application; class FrontendController extends Application { /** * @return mixed */ public function processRequest() { $request = parent::processRequest(); $viewdata = [ 'config' => $this->config ]; return $this->twig->render('index.html.twig', $viewdata); } } $app = new FrontendController(); $view = $app->processRequest(); $app->handleResponse($view);
In der Application-Klasse werden die benötigten Komponenten über die bootstrap.php
initialisiert:
<?php namespace App; use Doctrine\DBAL\Connection; use Doctrine\ORM\EntityManagerInterface; use Twig\Environment; class Application { /** @var array */ protected $config; /** @var Environment */ protected $twig; /** @var Connection */ protected $conn; /** @var EntityManagerInterface */ protected $em; public function __construct() { require_once __DIR__ . '/../bootstrap.php'; $this->config = $config; $this->twig = $twig; $this->conn = $conn; $this->em = $em; } /** * @return mixed */ public function processRequest() { return parse_url($_SERVER['REQUEST_URI']); } /** * @param string $view * @return void */ public function handleResponse(string $view): void { echo $view; } }
Die Application benutzt bei der Initialisierung die Datei bootstrap.php
, in der
die Twig- und Doctrine-Bilbliotheken initialisiert werden. Die innerhalb der bootstrap.php
erzeugten Objekte, insbesondere der Doctrine EntityManager, werden in der Application in Variablen gespeichert
und können so vom Controller benutzt werden.
In der Datei bootstrap.php
werden zuerst die Bibliotheks-Namensräume über die Datei
autoload.php
des Composers eingebunden. Dann werden nacheinander die
Konfiguration gelesen, dann Twig, Doctrine DBAL und Doctrine ORM initialisiert, der Doctrine EntityManager
instanziert und am Ende der Doctrine ClassLoader für die benötigten Namespaces geladen. Für
das Objekt-Mapping werden in diesem Fall Annotations benutzt, das
Kapitel
2. Installation and Configuration in der Doctrine-Dokumentation zeigt noch andere Wege.
<?php use Doctrine\Common\Cache\ArrayCache; use Doctrine\DBAL\DriverManager; use Twig\Environment; use Twig\Loader\FilesystemLoader; $cloader = require_once __DIR__ . '/vendor/autoload.php'; // Read application Configuration $config = yaml_parse_file(__DIR__ . '/config/parameters.yaml'); setlocale(LC_ALL, $config['locale']); // Twig $loader = new FilesystemLoader(__DIR__ . '/templates'); $twig = new Environment($loader); // Doctrine DBAL $dbalconfig = new Doctrine\DBAL\Configuration(); $conn = DriverManager::getConnection($config['database'], $dbalconfig); // Doctrine ORM $ormconfig = new Doctrine\ORM\Configuration(); $cache = new ArrayCache(); $ormconfig->setQueryCacheImpl($cache); $ormconfig->setProxyDir(__DIR__ . '/Entity'); $ormconfig->setProxyNamespace('EntityProxy'); $ormconfig->setAutoGenerateProxyClasses(true); // ORM mapping by Annotation Doctrine\Common\Annotations\AnnotationRegistry::registerFile( __DIR__ . '/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php'); $driver = new Doctrine\ORM\Mapping\Driver\AnnotationDriver( new Doctrine\Common\Annotations\AnnotationReader(), array(__DIR__ . '/src/Entity') ); $ormconfig->setMetadataDriverImpl($driver); $ormconfig->setMetadataCacheImpl($cache); // EntityManager $em = Doctrine\ORM\EntityManager::create($config['database'],$ormconfig); // The Doctrine Classloader require __DIR__ . '/vendor/doctrine/common/lib/Doctrine/Common/ClassLoader.php'; $classLoader = new Doctrine\Common\ClassLoader('App\Entity', __DIR__.'/src/Entity'); $classLoader->register();
Wenn nun die Twig-Dateien index.html.twig
und layout.html.twig
erzeugt werden, kann die Anwendung schon einmal getestet werden.
{% extends "layout.html.twig" %} {% block content %} <div id="content"> <h1>Hello Twig!</h1> </div> {% endblock %}
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="content-type" content="text/html; charset=UTF-8; IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content=""> <meta name="author" content=""> <title>{% block title %}{{ config.site.name }}{% endblock %}</title> <base href="{{ config.base }}"> </head> <body> <div class="container"> {% block content %}{% endblock %} </div> <!-- /container --> </body> </html>
Die ORM-Konsole
Bisher benutzt die Anwendung jedoch "nur" Twig, nicht die Doctrine-Komponenten. Deren Stärke liegt aber nicht nur in der Datenbank-Abstraktion DBA und dem Objekt-Mapping ORM, ein besonders hilfreiches Werkzeug ist die ORM-Konsole. Diese wird über PHP aus einer Shell heraus angesprochen und ist unter anderem in der Lage, die Datenbankstruktur mit den Anwendungs-Entities synchron zu halten.
Dafür wird das script cli.php
geschrieben. Darin werden die
Initialisierungen aus der bootstrap.php
geladen und die Konsole mittels EntityManager initialisiert.
<?php use Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper; use Doctrine\ORM\Tools\Console\ConsoleRunner; use Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper; use Symfony\Component\Console\Helper\HelperSet; require_once __DIR__ . '/bootstrap.php'; $helperSet = new HelperSet(array( 'em' => new EntityManagerHelper($em), 'conn' => new ConnectionHelper($em->getConnection()) )); ConsoleRunner::run($helperSet);
Wenn diese nun aus einer Shell heraus gestartet wird, wird die Kurzanleitung der ORM-Konsole ausgegeben.
~/doctrine2-standalonephp cli.php Doctrine Command Line Interface v2.7.3@d95e03ba660d50d785a9925f41927fef0ee553cf Usage: command [options] [arguments] Options: -h, --help Display this help message -q, --quiet Do not output any message -V, --version Display this application version --ansi Force ANSI output --no-ansi Disable ANSI output -n, --no-interaction Do not ask any interactive question -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Available commands: help Displays help for a command list Lists commands dbal dbal:import Import SQL file(s) directly to Database. dbal:reserved-words Checks if the current database contains identifiers that are reserved. dbal:run-sql Executes arbitrary SQL directly from the command line. orm orm:clear-cache:metadata Clear all metadata cache of the various cache drivers orm:clear-cache:query Clear all query cache of the various cache drivers orm:clear-cache:region:collection Clear a second-level cache collection region orm:clear-cache:region:entity Clear a second-level cache entity region orm:clear-cache:region:query Clear a second-level cache query region orm:clear-cache:result Clear all result cache of the various cache drivers orm:convert-d1-schema [orm:convert:d1-schema] Converts Doctrine 1.x schema into a Doctrine 2.x schema orm:convert-mapping [orm:convert:mapping] Convert mapping information between supported formats orm:ensure-production-settings Verify that Doctrine is properly configured for a production environment orm:generate-entities [orm:generate:entities] Generate entity classes and method stubs from your mapping information orm:generate-proxies [orm:generate:proxies] Generates proxy classes for entity classes orm:generate-repositories [orm:generate:repositories] Generate repository classes from your mapping information orm:info Show basic information about all mapped entities orm:mapping:describe Display information about mapped objects orm:run-dql Executes arbitrary DQL directly from the command line orm:schema-tool:create Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output orm:schema-tool:drop Drop the complete database schema of EntityManager Storage Connection or generate the corresponding SQL output orm:schema-tool:update Executes (or dumps) the SQL needed to update the database schema to match the current mapping metadata orm:validate-schema Validate the mapping files
Mit der Konsole kann die Datenbank mit den Objekt-Modellen synchron gehalten werden.
Allerdings muss erst einmal eine Datenbank angelegt werden.
Ich gehe von einer laufenden MySQL Installation aus, mit Vollzugriff für den Benutzer root
mit dem Passwort password
auf eine leere Datenbank demodb
.
Die Zugangsdaten zu dem MySQL-Server können in der Datei ~/.my.cnf
hinterlegt werden.
~/doctrine2-standalonecat ~/.my.cnf [mysql] user = root password = password ~/doctrine2-standalonemysql -e "create database demodb"
Die Demo-Anwendung soll Zitate von Personen ausgeben. Dazu werden erst einmal zwei Entitäten Author
und Quote
in dem Verzeichnis src/Entity
angelegt. Die Annotation in den Klassen
werden von Doctrine auf die Datenbank umgesetzt.
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class Author { /** * @var integer * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\OneToMany(targetEntity="Quote", mappedBy="author") */ private $quotes; /** * @var string * * @ORM\Column(name="name", type="string", length=255) */ private $name; }
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class Quote { /** * @var integer * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\ManyToOne(targetEntity="Author", inversedBy="quotes") * @ORM\JoinColumn(name="id_acount", referencedColumnName="id", onDelete="CASCADE") */ private $author; /** * @var string * * @ORM\Column(name="text", type="string", length=255) */ private $text; }
Für diese beiden Klassen werden die passenden Getter- und Setter-Methoden benötigt. Die konnten wir in früher problemlos
über die ORM-Konsole bzw. das "Doctrine Command Line Interface" (s.o.) generieren lassen, das geht aber mittlerweile nicht mehr.
Der Befehl orm:generate-entities
ist zwar noch vorhanden, legt aber die Entity-Klassen entsprechend ihres
Namespaces in dem Verzeichnis src/App/Entity
an, was wir dort nicht gebrauchen können. Ausserdem ist der
Befehl als veraltet gekennzeichnet.
Wir müssen also die Getter- und Setter-Methoden per Hand schreiben, oder sie von unserer IDE generieren lassen. Anschließend sehen die beiden Entity-Klassen so aus:
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class Author { /** * @var integer * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\OneToMany(targetEntity="Quote", mappedBy="author") */ private $quotes; /** * @var string * * @ORM\Column(name="name", type="string", length=255) */ private $name; /** * @return int */ public function getId(): int { return $this->id; } /** * @param int $id */ public function setId(int $id): void { $this->id = $id; } /** * @return mixed */ public function getQuotes() { return $this->quotes; } /** * @param mixed $quotes */ public function setQuotes($quotes): void { $this->quotes = $quotes; } /** * @return string */ public function getName(): string { return $this->name; } /** * @param string $name */ public function setName(string $name): void { $this->name = $name; } }
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class Quote { /** * @var integer * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\ManyToOne(targetEntity="Author", inversedBy="quotes") * @ORM\JoinColumn(name="id_acount", referencedColumnName="id", onDelete="CASCADE") */ private $author; /** * @var string * * @ORM\Column(name="text", type="string", length=255) */ private $text; /** * @return int */ public function getId(): int { return $this->id; } /** * @param int $id */ public function setId(int $id): void { $this->id = $id; } /** * @return mixed */ public function getAuthor() { return $this->author; } /** * @param mixed $author */ public function setAuthor($author): void { $this->author = $author; } /** * @return string */ public function getText(): string { return $this->text; } /** * @param string $text */ public function setText(string $text): void { $this->text = $text; } }
Dann kann die ORM-Konsole die Datenbank synchronisieren.
~/doctrine2-standalonephp cli.php orm:schema-tool:create --dump-sql The following SQL statements will be executed: CREATE TABLE Quote (id INT AUTO_INCREMENT NOT NULL, id_acount INT DEFAULT NULL, text VARCHAR(255) NOT NULL, INDEX IDX_AAB0E4F017A67BCE (id_acount), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; CREATE TABLE Author (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; ALTER TABLE Quote ADD CONSTRAINT FK_AAB0E4F017A67BCE FOREIGN KEY (id_acount) REFERENCES Author (id) ON DELETE CASCADE; ~/doctrine2-standalonephp cli.php orm:schema-tool:update --force Updating database schema... 3 queries were executed [OK] Database schema updated successfully!
Es werden Daten in der Datenbank benötigt, die über eine shell eingetragen werden können.
~/doctrine2-standalonemysql demodb -e "INSERT INTO Author VALUES (0,'Robin Williams'), (0,'Lionel Richie'), (0,'Leonard Nimoy'), (0,'Bruce Willis')" ~/doctrine2-standalonemysql demodb -e "INSERT INTO Quote VALUES (0,1,'Nano Nano!'), (0,2,'Hello!'), (0,3,'LLAP!'), (0,4,'Yippie-Ya-Yeah!'), (0,1,'Gooood Morning!')"
Damit können nun die Entities über den Doctrine ORM im Controller
geladen und an die View weitergegeben werden. Den Frontend-Controller und die
index.html.twig
erweitern wir wie folgt:
<?php namespace App\Controller; require_once __DIR__ . '/../Application.php'; use App\Application; use App\Entity\Author; class FrontendController extends Application { /** * @return mixed */ public function processRequest() { $viewdata = [ 'config' => $this->config ]; $viewdata['authors'] = $this->em->getRepository(Author::class)->findAll(); return $this->twig->render('index.html.twig', $viewdata); } } $app = new FrontendController(); $view = $app->processRequest(); $app->handleResponse($view);
{% extends "layout.html.twig" %}; {% block content %} <h1>Famous Quotes</h1> {% for author in authors %} <h3>{{ author.name }}</h3> {% for quote in author.quotes %} <blockquote>{{ quote.text }}</blockquote> {% endfor %} {% endfor %} </h3> {% endblock %}
Unser Projekt hat jetzt folgende Struktur:
~/doctrine2-standalonetree . ├── bootstrap.php ├── cli.php ├── composer.json ├── composer.lock ├── composer.phar ├── config │ └── parameters.yaml ├── public │ └── index.php ├── src │ ├── Application.php │ ├── Controller │ │ └── FrontendController.php │ └── Entity │ ├── Author.php │ └── Quote.php ├── templates │ ├── index.html.twig │ └── layout.html.twig └── vendor ├── autoload.php ├── bin │ ├── doctrine -> ../doctrine/orm/bin/doctrine │ └── doctrine-dbal -> ../doctrine/dbal/bin/doctrine-dbal ... 242 directories, 1398 files
Dann starten wir den PHP-eigenen webserver auf localhost:8000
~/doctrine2-standalonephp -S localhost:8000 public/index.php PHP 7.2.5 Development Server started at Thu May 28 17:13:30 2020 Listening on http://localhost:8000 Document root is ~/doctrine2-standalone Press Ctrl-C to quit.
Und sehen das Ergebnis unter http://localhost:8000/:
Famous Quotes
Robin Williams
Nano Nano!
Gooood Morning!
Lionel Richie
Hello!
Leonard Nimoy
LLAP!
Bruce Willis
Yippie-Ya-Yeah!
Zusammenfassung
Wenn die Anwendung und die Datenbank komplizierter werden - gerade bei vielen n:1 und n:m Beziehungen - wird dieser Aufbau früher oder später zu Performance-Einbrüchen führen. So wie die Datenbankabfragen hier aufgebaut sind, ist die Anwendung weit entfernt von optimiert. Nicht sichtbar sind die 5 zusätzlichen SQL-Abfragen für die Ermittlung der eigentlichen Zitate, dafür sollte die Flexibilität des Object-Relation Mappings deutlich werden. Auch ist Vorsicht bei der Verarbeitung der Routen, Requests und Responses und ggf. von Formularen geboten. Das sind Punkte, die bei fertigen Frameworks wie Symfony schon entsprechend abgefangen werden.