
Toolchain zum Rapid Prototyping mit PHP und MySQL
- 10. Juni 2013
- 0
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.
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.
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.