ZURÜCK

Doctrine2 ORM standalone Demo

Erstellung einer Webanwendung mit Doctrine 2 ORM ohne Symfony.

Datum: 2014-04, Tags: PHP, Doctine 2, Twig, Howto

Update 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
/etc/php7/cli/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.

config/parameters.yaml
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:

public/index.php
<?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.

src/Controller/FrontendController.php
<?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:

src/Application.php
<?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.

bootstrap.php
<?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.

templates/index.html.twig
{% extends "layout.html.twig" %}
{% block content %}
<div id="content">
    <h1>Hello Twig!</h1>
</div>
{% endblock %}
templates/layout.html.twig
<!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.

cli.php
<?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.

src/Entity/Author.php
<?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;
}
src/Entity/Quote.php
<?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:

src/Entity/Author.php
<?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;
    }
}
src/Entity/Quote.php
<?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:

src/Controller/FrontendController.php
<?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);
templates/index.html.twig
{% 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.