Migration-Scripte mit Doctrine erstellen: Beispiel an Symfony

Im zweiten Teil meiner Artikelserie möchte ich das Einrichten und die Funktionsweise von Doctrine Migration im Symfony2-Framework zeigen. Dazu installieren wir uns zunächst das Standardprojekt und erstellen anschließend zwei Bundles. Aus diesen wird dann das Migrations-Script erstellt. Zum Schluss gibt es noch eine kurze Auflistung der Konfigurationsoptionen.

Die Projektstruktur

Bevor wir in die Erstellung von Migrationsscripten eintauchen, möchte ich einige grundlegenden Dinge voraussetzen: Zunächst weiß jeder, was Doctrine2 und Symfony2 ist. Außerdem sollte der Leser wissen, wie Doctrine2 arbeitet (Konfiguration anlegen, Entities anlegen usw.). Und dann sollten Grundkenntnisse im Umgang mit der Kommandozeile unter Linux vorhanden sein.

Um einen Überblick über unser Ziel zu erhalten, schauen wir uns zunächst die gekürzte Verzeichnisstruktur des finalen Symfony-Projektes an:

├── app
│   ├── config
│   │   └── config.yml
│   └── DoctrineMigrations
│       ├── Version20130823100917.php
│       └── Version20130823101023.php
└── src
    └── Acme
        ├── HelloBundle
        │   ├── AcmeHelloBundle.php
        │   ├── Entity
        │   │   ├── Comment.php
        │   │   └── Post.php
        │   └── Resources
        │       └── config
        │           └── doctrine
        │               ├── Comment.orm.xml
        │               └── Post.orm.xml
        └── WorldBundle
            ├── AcmeWorldBundle.php
            ├── Entity
            │   └── World.php
            └── Resources
                └── config
                    └── doctrine
                        └── World.orm.xml

Im Verzeichnis src liegen zwei Bundles (Bundles sind das Selbe wie Module im ZendFramework2). Das HelloBundle hat zwei mit Hilfe von Doctrine2 erstellte Entities, das WorldBundle eine. Wie wir sehen können, existieren für die Anwendung zwei Migrations-Skripte: Version20130823100917.php und Version20130823101023.php. Diese sind aus der Konfiguration bzw. den Entities der zwei Bundles erstellt worden. Zum Schluss gibt es noch die config.yml, in der man einige Konfigurationen für die Migration vornehmen kann.

Als Beispiel nehme ich den normalen Lebenszyklus eines Bundles. Zunächst binden wir ein neues Bundle ein und erstellen das erste Migrationsscript. Später kommt in diesem Bundle eine weitere Entity hinzu und wir nutzen ein weiteres Bundle. Für diese beiden Änderungen erstellen wir die zweite Migration.

Erstellen des Projektes

Um das Beispiel nachzuvollziehen, installieren wir zunächst die Symfony Standard Distribution, am besten mit dem Composer:

cd ~/Projects
composer.phar create-project symfony/framework-standard-edition migrate-symfony 2.3.0
cd migrate-symfony

Während der Installation werden wir nach einem Datenbankadapter gefragt; standardmäßig der pdo_mysql. Geben wir hier die Daten zu einer existierenden Datenbank an. Ich nutze wie vorgeschlagen eine MySQL-Datenbank, aber auch eine SQLite-Datenbank ist möglich. Dazu sind aber noch manuelle Einstellungen in der app/config/parameters.yml notwendig. Die restlichen Daten belassen wir bei den Standardwerten. Nun müssen wir noch das DoctrineMigrationBundle und die DoctrineMigration-Library installieren. Dazu öffnen wir im Projektverzeichnis (migration-symfony) die composer.json und fügen folgende beiden Zeilen hinzu:

"require": {
	...
	"doctrine/migrations": "dev-master",
	"doctrine/doctrine-migrations-bundle": "dev-master"
},

Mit einem composer.phar update installieren wir die beiden Pakete. Als nächstes müssen wir noch das Bundle in die app/AppKernel.php eintragen:

$bundles = array(
	...
	new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),
);

Kommt wir nun zur eigentlichen Magie. Als erstes schauen wir, ob das DoctrineMigrationBundle ordentlich installiert wurde. Dazu rufen wir auf der Konsole auf:

app/console doctrine:migrations:status

Das sollte ohne Probleme klappen. Wenn es einen Fehler gibt, ist meistens die Verbindung zur Datenbank falsch oder das Bundle wurde in der app/AppKernel.php falsch geschrieben. Funktioniert alles, erstellen wir das erste eigene Bundle.

Erstes Migrationsscript erstellen

Um das erste Bundle zu erstellen, tippen wir im Projekt-Root auf der Kommandozeile:

app/console generate:bundle --namespace=Acme/HelloBundle --format=xml

Belassen wir jede weitere Abfrage bei den Standardwerten. Nun erstellen wir die Konfiguration für die erste Entity:

app/console doctrine:generate:entity

Als „Entity short name“ gebe ich „AcmeHelloBundle:Post“ an. Als „Configuration format“ entscheide ich mich für xml. Jetzt kommen noch zwei Spalten hinzu, eine Namens headline vom Typ string und einer Länge von 255 sowie eine Spalte Namens content vom Typ text. Den Rest belasse ich auch hier bei den Standardvorgaben.

Jetzt können wir schon das erste Migrationsscript anlegen:

app/console doctrine:migrations:diff

Mit dem Befehl wird im Verzeichnis app/DoctrineMigration die Migration-Datei erstellt. Diese hat den Namen VersionYYYYMMDD.php und den Namespace Application\DoctrineMigration. Wenn nötig können nun weitere SQL-Anweisungen, z.B. zur Datenmigration, hinzugefügt werden. Wie man den Pfad und den Namespace ändern kann, erkläre ich am Ende des Posts.

Nun müssen wir die erste Änderung in die Datenbank übernehmen. Dazu führen wir das erste Migrationscript aus:

app/console doctrine:migrations:migrate

Nun wurden in unserer Testdatenbank zwei Tabellen erstellt: Zunächst die migration_versions, in der die Versionsnummern der bereits vorgenommenen Migrationen gespeichert werden, und dann die Tabelle Post. Wen der Name migration_versions stört, der sollte an das Ende dieses Artikels scrollen. Dort steht, wie man den Namen ändern kann.

Zweites Migrationsscript erstellen

Legen wir nun eine weitere Konfiguration für eine Entity an:

app/console doctrine:generate:entity

Die Daten hier: Als „Entity short name“ gebe ich „AcmeHelloBundle:Comment“ an. Beim „Configuration format“ entscheide ich mich wieder für xml. Die Felder sind in diesem Fall name vom Type string, Länge 255 und content vom Type text. Den Rest belasse ich auch hier bei den Standardvorgaben. Mit einem

app/console doctrine:generate:entities --path=src/ AcmeHelloBundle:Comment

wird die Entity erstellt. Nun legen wir aber gleich hinterher ein weiteres Bundle an:

app/console generate:bundle --namespace=Acme/WorldBundle --format=xml

Wiederum belassen wir jede Abfrage bei ihren Standardwerten. Im WorldBundle legen wir noch eine Konfiguration für eine World-Entity an:

app/console doctrine:generate:entity

Diese Entity heißt World, hat einen Name name vom Type string und einer Länge von 255. Das reicht auch schon. Erstellen wir nun die neue Entity und danach das zweite Migrationsscript:

app/console doctrine:migrations:diff

Wenn wir nun in die zweite Migrationsdatei schauen sehen wir folgenden Inhalt:

<?php

namespace Application\Migrations;

use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;

/**
 * Auto-generated Migration: Please modify to your needs!
 */
class Version20130823101023 extends AbstractMigration
{
    public function up(Schema $schema)
    {
        // this up() migration is auto-generated, please modify it to your needs
        $this->abortIf($this->connection->getDatabasePlatform()->getName() != "mysql", "Migration can only be executed safely on 'mysql'.");
        
        $this->addSql("CREATE TABLE Comment (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, content LONGTEXT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB");
        $this->addSql("CREATE TABLE World (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB");
    }

    public function down(Schema $schema)
    {
        // this down() migration is auto-generated, please modify it to your needs
        $this->abortIf($this-&gt;connection-&gt;getDatabasePlatform()-&gt;getName() != "mysql", "Migration can only be executed safely on 'mysql'.");
        
        $this->addSql("DROP TABLE Comment");
        $this->addSql("DROP TABLE World");
    }
}

Uns fällt auf, dass hier die Änderungen aus beiden Bundles enthalten sind. Folglich sieht man auch sehr gut, das die Migrationen mit den eigentlichen Bundles nichts zu tun haben. Sie beziehen sich also auf das komplette Projekt. Um nun diese Änderungen durchzuführen reicht wieder ein

app/console doctrine:migrations:migrate

Konfiguration

Noch ein Wort zur Konfiguration: DoctrineMigration kann über Parameter konfiguriert werden. Ein Beispiel,was man in die app/config/config.yml schreiben könnte sieht so aus:

...
doctrine_migrations:
    name: Acme SuperApp Migrations
    namespace: App\Migrations
    table_name: superapp_migration_versions
    dir_name: %kernel.root_dir%/migrations
...

Mit name wird der Name der Migration gesetzt. Hier macht es Sinn, den Namen der Anwendung zu schreiben. Dieser taucht allerdings nur beim Aufrufen der Migration über die Kommandozeile auf. Mit namespace wird – wen wunderts – der Namespace der Migrations-Klassen definiert. Auch dieser kann frei gewählt werden, weil dieser theoretisch uninteressant sein sollte. Über table_name wird der Name der Tabelle eingetragen, in der die einzelnen Versionsnummern gespeichert werden. Last but not least wird über den Parameter dir_name der Pfad definiert, wo die Migrations-Scripte liegen sollen. Logischer Weise sollten diese Parameter vor der ersten Nutzung von Doctrine Migration definiert werden.

Schlusswort

Mit diesen Kenntnissen ausgestattet kannst du nun deiner eigenen Migrationsskripte schreiben. Wer wissen möchte, wie Doctrine Migrations mit dem ZendFramework2 funktioniert, muss auf den nächsten Artikel warten.