Toolchain zum Rapid Prototyping mit PHP und MySQL

In diesem Artikel geht es darum einige Werkzeuge vorzustellen, die es erlauben schnell und ohne viele manuelle Eingriffe einen Applikationsrumpf für eine PHP/MySQL-Anwendung zu schaffen.

Ziel ist es eine PHP-basierte Tool-Infrastruktur zu etablieren, die einen ähnlich schnellen Einstieg in die heiße, iterative Phase der Entwicklung erlaubt, wie es von „ruby on rails“ gemunkelt wird (Stichworte: Scaffolding und Rapid Prototyping).

Die verwendeten Tools sind alle open source und teilweise austauschbar. So kann man z.B. auf die grafische Modellierung mit MySQL-Workbench komplett verzichten (und z.B. Datenbankschemen direkt als yaml-Files schreiben) oder als Datenbankabstraktionsschicht anstatt doctrine propel verwenden.

Wir gehen davon aus, dass wir eine lauffähige symfony-Basisinstallation (standard edition, Version 2.x) auf einem Webserver zur Verfügung haben und einen MySQL-Server auf den wir zugreifen können.

Datenbankmodellierung mit MySQL Workbench

MySQL Workbench
Das Modellierungs-Modul von MySQL-Workbench erlaubt es uns erweitere ER-Diagramme zu zeichnen und dabei auch direkt die entsprechenden Tabellen, Datenfelder, Contraints und Relationen zu definieren.

Als Nebeneffekt erhalten wir auch ein hübsches „Bild“ unseres Datenbankschemas, das wir uns zum Ausdrucken und an-die-Wand-hängen exportieren lassen können.

EER Diagramm Screenshot

Theoretisch können wir uns auch direkt an dieser Stelle von MySQL-Workbench die entsprechenden DDL-Statements generieren lassen (Shift + Ctrl + G) – aber das ist nicht der Weg, der uns interessiert.

SQL Export Options Screenshot

SQL Script Screenshot

Doctrine Entities mit dem MySQL-Schema-Exporter-Plugin

MySQL Workbench Schema Exporter

Das php-Tool mysql-workbench-schema-exporter erlaubt es, direkt aus den .mwb-Files – in der die Modellierungskomponente von MySQL-Workbench das DB-Modell abspeichert – Datenbankdefinitionen (diverse Formate: YAML für doctrine 1 und 2, doctrine entities mit annotations, Zend Db Table, propel xml schemas u.a ) und Datenbankzugriffsklassen (doctrine entities) zu generieren.

Wechselt man ins Installationsverzeichnis von mysql-worbench-schema-exporter lässt sich die Generierung mit folgender allgemeinen Syntax anstoßen:

php cli/export.php

Alle Anpassungen in Symfony werden in sog. Bundles abgelegt. im nachfolgenden Text wird stehts mit dem Bundle musicxModelBundle (Namespace musicx\ModelBundle) gearbeitet. Das Bundle wird in einer Standard-Symfony-Installation unter src/musicx/ModelBundle/ abgelegt.

Damit man nicht alle Parameter jedes Mal auf der Kommandozeile übergeben muss, kann man auch eine Konfigurationsdatei benutzen:

[jens@localhost schema-exporter]$ cat musicx.conf.json

{
    "export": "doctrine2-annotation",
    "zip": false,
    "dir": "generated",
    "params": {
        "backupExistingFile": true,
        "skipPluralNameChecking": false,
        "enhanceManyToManyDetection": true,
        "bundleNamespace": "musicx\\ModelBundle",
        "entityNamespace": "",
        "repositoryNamespace": "",
        "useAnnotationPrefix": "ORM\\",
        "useAutomaticRepository": false,
        "indentation": 4,
        "filename": "%entity%.%extension%",
        "quoteIdentifier": false
    }
}

Die Konfigurationsdatei lässt sich dann beim Aufruf des Exports mit dem Parameter –config angeben:

[jens@localhost schema-exporter]$ php cli/export.php –config=musicx.conf.json

/home/jens/Dropbox/musicx/model.mwb
Using config file musicx.conf.json for parameters.

Exporting model.mwb as Doctrine 2.0 Annotation Classes.

File exported to generated

Done in 0.066 second, 1.750 MB memory used.

Wir lassen uns direkt doctrine 2.0 annotation classes in Unterverzeichnis generated exportieren.

[jens@localhost mysql-workbench-schema-exporter]$ ls generated/
Album.php  Musician.php  Song.php
[jens@localhost mysql-workbench-schema-exporter]$ head -n 40 generated/Album.php
<?php
 
namespace musicx\ModelBundle\Entity;
 
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
 
/**
* musicx\ModelBundle\Entity\Album
*
* @ORM\Entity()
* @ORM\Table(name="Album", uniqueConstraints={@ORM\UniqueConstraint(name="id_UNIQUE", columns={"id"})})
*/
class Album
{
    /**
    * @ORM\Id
    * @ORM\Column(type="integer")
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    protected $id;
 
    /**
    * @ORM\Column(type="text")
    */
    protected $Title;
 
    /**
    * @ORM\Column(type="smallint", nullable=true)
    */
    protected $Year;
 
    /**
    * @ORM\OneToMany(targetEntity="Song", mappedBy="album")
    * @ORM\JoinColumn(name="Album_ID", referencedColumnName="id", nullable=false)
    */
    protected $songs;
 
    public function __construct()
    {
    ...

Diese Klassen können wir später in Symfony für den Datenbankzugriff benutzen. Außerdem wird uns doctrine aus den generierten Klassen direkt die entsprechenden Datenbankdefinitionstatements liefern (sowohl für die erstmalige Erstellung als auch später bei Erweiterungen) und bei Bedarf auch direkt die entsprechenden Tabellen auf einem DB-Server anlegen. Zuguterletzt werden wir mit Hilfe des SensioGeneratorBundles in Symfony direkt CRUD-Controller für den (Lese- und Schreibzugriff) auf die Datenbankentitäten generieren lassen.

Anpassung Datenbankstruktur mit doctrine-Kommandozeile

Die im vorherigen Schritt generierten Klassen kopieren wir direkt in das entsprechende Verzeichnis unserer Symfony-Installation und wechseln in das Stammverzeichnis der Installation.

[jens@localhost schema-exporter]$ cp generated/* ~/f17vm-ws/sym-2.1.2/src/musicx/ModelBundle/Entity/
[jens@localhost schema-exporter]$ cd ~/f17vm-ws/sym-2.1.2/
[jens@localhost sym-2.1.2]$ ls
app composer.json composer.lock index.html LICENSE README.md src UPGRADE.md vendor web

Wir gehen nachfolgend davon aus, dass wir einen MySQL-Server mit der IP-Adresse 192.168.56.102 benutzen. Als Datenbankschema auf dem Server benutzen wir musicx. Die Datenbank ist zunächst komplett leer. Um Berechtigungen machen wir uns zunächst überhaupt keine Gedanken, der MySQL-Server (development) läuft mit skip-grant-tables.

[jens@localhost sym-2.1.2]$ mysql -h 192.168.56.102 -u root
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 20
Server version: 5.5.28 MySQL Community Server (GPL)</code>

Copyright (c) 2000, 2012, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| musicx             |
| mysql              |
| performance_schema |
| test               |
+--------------------+
5 rows in set (0.01 sec)

mysql> use musicx;
Database changed
mysql> show tables;
Empty set (0.00 sec)

Das symfony-Projekt ist entsprechend konfiguriert:

[jens@localhost sym-2.1.2]$ cat app/config/parameters.yml

parameters:
database_driver: pdo_mysql
database_host: 192.168.0.17
database_port: 3306
database_name: database_name
database_user: username
database_password: passwd
...

Jetzt können wir uns von doctrine die SQL-DDL-Statements ausgeben lassen, die nötig wären, um die Datenbanktabellen für unsere erzeugten doctrine-Entity-Klassen anzulegen.

[jens@localhost sym-2.1.2]$ php app/console doctrine:schema:update --dump-sql
CREATE TABLE Musician (id INT AUTO_INCREMENT NOT NULL, Instrument INT NOT NULL, Sortkey INT DEFAULT NULL, Name LONGTEXT DEFAULT NULL, Role LONGTEXT DEFAULT NULL, Song_ID INT NOT NULL, INDEX IDX_C8072371C1EA8F05 (Song_ID), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
CREATE TABLE Song (id INT NOT NULL, Title LONGTEXT DEFAULT NULL, Album_ID INT NOT NULL, INDEX IDX_93DF419F46ABCDF3 (Album_ID), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
CREATE TABLE Album (id INT AUTO_INCREMENT NOT NULL, Title LONGTEXT NOT NULL, Year SMALLINT DEFAULT NULL, UNIQUE INDEX id_UNIQUE (id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
ALTER TABLE Musician ADD CONSTRAINT FK_C8072371C1EA8F05 FOREIGN KEY (Song_ID) REFERENCES Song (id) ON DELETE CASCADE;
ALTER TABLE Song ADD CONSTRAINT FK_93DF419F46ABCDF3 FOREIGN KEY (Album_ID) REFERENCES Album (id)

Mit dem zusätzlichen Parameter --force können wir dann doctrine sagen, dass er die Tabellen auch wirklich anlegen soll. Im Produktivbetrieb ist das gefährlich und es gibt andere Möglichkeiten (MigrationBundle, Dokumentation => to-do).

[jens@localhost sym-2.1.2]$ php app/console doctrine:schema:update --force
Updating database schema...
Database schema updated successfully! "5" queries were executed

und auf dem MySQL-Server sieht das dann so aus:

mysql> SHOW TABLES;
+------------------+
| Tables_in_musicx |
+------------------+
| Album            |
| Musician         |
| Song             |
+------------------+
3 ROWS IN SET (0.00 sec)</code>

mysql> DESC Album;
+-------+-------------+------+-----+---------+----------------+
| FIELD | TYPE        | NULL | KEY | DEFAULT | Extra          |
+-------+-------------+------+-----+---------+----------------+
| id    | INT(11)     | NO   | PRI | NULL    | AUTO_INCREMENT |
| Title | longtext    | NO   |     | NULL    |                |
| YEAR  | SMALLINT(6) | YES  |     | NULL    |                |
+-------+-------------+------+-----+---------+----------------+
3 ROWS IN SET (0.00 sec)

mysql> DESC Musician;
+------------+----------+------+-----+---------+----------------+
| FIELD      | TYPE     | NULL | KEY | DEFAULT | Extra          |
+------------+----------+------+-----+---------+----------------+
| id         | INT(11)  | NO   | PRI | NULL    | AUTO_INCREMENT |
| Instrument | INT(11)  | NO   |     | NULL    |                |
| Sortkey    | INT(11)  | YES  |     | NULL    |                |
| Name       | longtext | YES  |     | NULL    |                |
| ROLE       | longtext | YES  |     | NULL    |                |
| Song_ID    | INT(11)  | NO  | MUL  | NULL    |                |
+------------+----------+------+-----+---------+----------------+
6 ROWS IN SET (0.00 sec)

mysql> DESC Song;
+----------+----------+------+-----+---------+-------+
| FIELD    | TYPE     | NULL | KEY | DEFAULT | Extra |
+----------+----------+------+-----+---------+-------+
| id       | INT(11)  | NO   | PRI | NULL    |       |
| Title    | longtext | YES  |     | NULL    |       |
| Album_ID | INT(11)  | NO   | MUL | NULL    |       |
+----------+----------+------+-----+---------+-------+
3 ROWS IN SET (0.00 sec)

Symfony: Backend-Controller mit dem SensioGeneratorBundl

http://symfony.com/doc/current/bundles/SensioGeneratorBundle/commands/generate_doctrine_crud.html

Damit wir auch direkt in unserer Webapplikation mit der Datenbank arbeiten können, wollen wir das SensioGeneratorBundle (in der Symfony Standard Edition bereits enthalten) nutzen, um einen klassischen CRUD-Controller für die Anzeige und Pflege der Datenbankinhalte generieren zu lassen. Und dann wollen wir es uns natürlich auch nicht nehmen lassen, mit unserem frisch generierten Controller einen neuen Datensatz in die DB zu schreiben.

Da wir mit einem leeren Symfony-Projekt und einem leeren musicx-Bundle gestart sind, gibt es nur den DefaultController und die standardmäßig aktivierten Routen.

[jens@localhost sym-2.1.2]$ ls src/musicx/ModelBundle/Controller/
DefaultController.php
[jens@localhost sym-2.1.2]$ php app/console router:debug
[router] Current routes
Name Method Pattern
_welcome ANY /
_demo_login ANY /demo/secured/login
_security_check ANY /demo/secured/login_check
_demo_logout ANY /demo/secured/logout
acme_demo_secured_hello ANY /demo/secured/hello
_demo_secured_hello ANY /demo/secured/hello/{name}
_demo_secured_hello_admin ANY /demo/secured/hello/admin/{name}
_demo ANY /demo/
_demo_hello ANY /demo/hello/{name}
_demo_contact ANY /demo/contact
_wdt ANY /_wdt/{token}
_profiler_search ANY /_profiler/search
_profiler_purge ANY /_profiler/purge
_profiler_info ANY /_profiler/info/{about}
_profiler_import ANY /_profiler/import
_profiler_export ANY /_profiler/export/{token}.txt
_profiler_phpinfo ANY /_profiler/phpinfo
_profiler_search_results ANY /_profiler/{token}/search/results
_profiler ANY /_profiler/{token}
_profiler_redirect ANY /_profiler/
_configurator_home ANY /_configurator/
_configurator_step ANY /_configurator/step/{index}
_configurator_final ANY /_configurator/final
musicx_model_default_index ANY /hello/{name}

Um CRUD-Controller zu generieren, nutzen wir den Kommandzeilenwizard des SensioGeneratorBundles

[jens@localhost sym-2.1.2]$ php app/console generate:doctrine:crud</code>

Welcome to the Doctrine2 CRUD generator

This command helps you generate CRUD controllers and tpl.

First, you need to give the entity for which you want to generate a CRUD.
You can give an entity that does not exist yet and the wizard will help
you defining it.

You must use the shortcut notation like AcmeBlogBundle:Post.

The Entity shortcut name: musicxModelBundle:Album

By default, the generator creates two actions: list and show.
You can also ask it to generate "write" actions: new, update, and delete.

Do you want to generate the "write" actions [no]? yes

Determine the format to use for the generated CRUD.

Configuration format (yml, xml, php, or annotation) [annotation]:

Determine the routes prefix (all the routes will be "mounted" under this
prefix: /prefix/, /prefix/new, ...).

Routes prefix [/album]:

Summary before generation

You are going to generate a CRUD controller for "musicxModelBundle:Album"
using the "annotation" format.

Do you confirm generation [yes]?

CRUD generation

Generating the CRUD code: OK
Generating the Form code: OK

You can now start using the generated code!

Diesen Prozess wiederholen wir für die beiden anderen Entitäten (Song, Musician) unserer Test-Datenbank.

Möchte man die Controller-Generierung ohne interaktiven Modus (z.B. in einem Skript) durchführen, so kann man die Option –no-interaction des generate:doctrine:code-Kommandos benutzen. Dann muss man aber sicherstellen, dass alle notwendigen Einstellungen als Kommandozeilenparameter beim Aufruf mit übergeben werden.

Wenn wir uns jetzt das Controller-Verzeichnis unseres Bundles und die möglichen Routen unserer Applikation anschauen, sehen wir, dass sich einiges getan hat.

[jens@localhost sym-2.1.2]$ ls src/musicx/ModelBundle/Controller/
AlbumController.php DefaultController.php MusicianController.php SongController.php
[jens@localhost sym-2.1.2]$ php app/console router:debug
[router] Current routes
Name Method Pattern
_welcome ANY /
_demo_login ANY /demo/secured/login
_security_check ANY /demo/secured/login_check
_demo_logout ANY /demo/secured/logout
acme_demo_secured_hello ANY /demo/secured/hello
_demo_secured_hello ANY /demo/secured/hello/{name}
_demo_secured_hello_admin ANY /demo/secured/hello/admin/{name}
_demo ANY /demo/
_demo_hello ANY /demo/hello/{name}
_demo_contact ANY /demo/contact
_wdt ANY /_wdt/{token}
_profiler_search ANY /_profiler/search
_profiler_purge ANY /_profiler/purge
_profiler_info ANY /_profiler/info/{about}
_profiler_import ANY /_profiler/import
_profiler_export ANY /_profiler/export/{token}.txt
_profiler_phpinfo ANY /_profiler/phpinfo
_profiler_search_results ANY /_profiler/{token}/search/results
_profiler ANY /_profiler/{token}
_profiler_redirect ANY /_profiler/
_configurator_home ANY /_configurator/
_configurator_step ANY /_configurator/step/{index}
_configurator_final ANY /_configurator/final
album ANY /album/
album_show ANY /album/{id}/show
album_new ANY /album/new
album_create POST /album/create
album_edit ANY /album/{id}/edit
album_update POST /album/{id}/update
album_delete POST /album/{id}/delete
musicx_model_default_index ANY /hello/{name}
musician ANY /musician/
musician_show ANY /musician/{id}/show
musician_new ANY /musician/new
musician_create POST /musician/create
musician_edit ANY /musician/{id}/edit
musician_update POST /musician/{id}/update
musician_delete POST /musician/{id}/delete
song ANY /song/
song_show ANY /song/{id}/show
song_new ANY /song/new
song_create POST /song/create
song_edit ANY /song/{id}/edit
song_update POST /song/{id}/update
song_delete POST /song/{id}/delete</code>

Und jetzt können wir über die new-Route neue Alben anlegen.

Zusätzlich werden Routen zum Editieren, Löschen und zum Listing aller Entitäten angelegt.

Unter Strich erhalten wir mit den genannten Tools – wenn Sie denn einmal installiert und konfiguriert sind – eine sehr schnelle Möglichkeit ein Skelett für Applikationsprototypen mit Datenbankzugriff zu generieren, praktisch ohne eine einzige Zeile SQL- oder PHP-Code selbst zu schreiben.

Das lässt dann hoffentlich insgesamt mehr Zeit für das eigentlich wichtige Thema: Die Business-Logik, die fehlt natürlich auch noch komplett in diesem Stadium.