Migration-Scripte mit Doctrine erstellen: Beispiel am ZendFramework2

Im letzten Teil meiner kleinen Artikelserie über Doctrine Migration möchte ich nun auf das ZendFramework2 eingehen. Die Vorgehensweise ist dabei fast die Selbe wie im Workshop über Symfony: Zunächst wird die Skeleton-Application installiert, dann werden zwei Module und zwei Migrationsscripte angelegt. Gewürzt wird die Anleitung wieder mit einem Überblick über die Konfiguration.

Die Projektstruktur

Auch für diesen Workshop setze ich einige Kenntnisse voraus: Zunächst Kenntnisse in Doctrine2 und dem ZF2. Außerdem Umgang mit dem Composer und der Kommandozeile.

Um einen besseren Überblick zu bekommen, wo in unserem ZendFramework-Projekt die wichtigen Dateien liegen, gibt es für euch eine gekürzte Version der Verzeichnisstruktur:

├── config
│   └── migration.xml
├── data
│   └── doctrine-migrations
│       ├── Version20130826191120.php
│       └── Version20130826192140.php
└── module
    ├── Hello
    │   ├── config
    │   │   └── doctrine
    │   │       ├── Hello.Entity.Comment.dcm.xml
    │   │       └── Hello.Entity.Post.dcm.xml
    │   └── src
    │       └── Hello
    │           └── Entity
    │               ├── Comment.php
    │               └── Post.php
    └── World
        ├── config
        │   └── doctrine
        │       └── World.Entity.World.dcm.xml
        └── src
            └── World
                └── Entity
                    └── World.php

Wir sehen hier eine ähnliche Struktur wie bereits beim Symfony-Projekt aus dem vorherigen Artikel. Hier fällt uns allerdings die migration.xml im config-Ordner auf. In dieser Datei können wir einige Einstellungen für Doctrine Migration vornehmen. Da diese bei einem ZendFramework-Projekt wichtig ist, werde ich auch schon in den nächsten Sätzen darauf eingehen.

Im Verzeichnis data liegen im Ordner doctrine-migrations die Migration-Scripte. Dieser Ordner sollte also auf jeden Fall bspw. mit Git bersioniert werden. Im Verzeichnis module gibt es zwei Module: Hello und World. Das Hello-Modul hat zwei Entities, das World-Modul nur eine. Die Konfigurationen liegen jeweils im Ordner config/doctrine des jeweiligen Modules.

Wer bereits den Artikel ZF2 und Doctrine als Modul einbinden gelesen hat, wird auf einige bekannte Sachen stoßen. Außerdem ist im ZF2 etwas mehr Handarbeit notwendig um die Migrations zu nutzen. Das sehe ich aber nicht als Nachteil.

Erstellen des Projektes

Wir fangen mit einem leeren Projekt an. Beim ZendFramework2 heißt es SkeletonApplication. Um ein Projekt migrate-zf2 zu erstellen, tippen wir auf der Konsole folgenden Befehl ein:

composer.phar create-project --repository-url="https://packages.zendframework.com" -s dev zendframework/skeleton-application migrate-zf2

Nun wechseln wir in des Verzeichnis migrate-zf2 und öffnen die composer.json mit einem Editor. Folgende Zeilen fügen wir hinzu:

"require": {
   ...
   "doctrine/doctrine-module": "@stable",
   "doctrine/doctrine-orm-module": "@stable",
   "doctrine/migrations": "dev-master"
}

Nach einem

composer.phar update

sind die noch fehlenden Module bzw. Bibliotheken geladen. Nun müssen wir diese nur noch aktivieren. Dazu öffnen wir die config/application.config.php und fügen an passender Stelle hinzu:

'modules' => array(
       ...
        'DoctrineModule',
        'DoctrineORMModule'
    ),

Jetzt fehlt nur noch die Datenbankverbindung. Dazu schreiben wir in eine config/autoload/database.local.php folgende Zeilen:

<?php

return array(
    'doctrine' => array(
        'connection' => array(
            'orm_default' => array(
                'driverClass' => 'Doctrine\DBAL\Driver\PDOMySql\Driver',
                'params' => array(
                    'host'     => 'localhost',
                    'port'     => '3306',
                    'user'     => 'root',
                    'password' => '',
                    'dbname'   => 'test',
                    'charset'  => 'utf8'
                )
            )
        )
    )
);

Natürlich ist das nicht die beste Lösung. Wie man es richtig macht erfährst du in der ZF2-Dokumentation.

Um zu schauen ob Doctrine ordentlich konfiguriert wurde, führen wir einmal

vendor/bin/doctrine-module

aus. Auf der Konsole sollte kein Fehler auftauchen.

Nun müssen nur noch einige Verzeichnisse angelegt werden. Diese wären:

  • data/doctrine-migrations
  • module/Hello/config/doctrine
  • module/Hello/src/Hello/Entity
  • module/World/config/doctrine
  • module/World/src/Hello/Entity

Wie bereits weiter oben erwähnt brauchen wir für Doctrine Migration und dem Zend Framework eine Konfigurationsdatei. Diese nennen wir config/migration.xml:

<?xml version="1.0" encoding="UTF-8"?>
<doctrine-migrations xmlns="http://doctrine-project.org/schemas/migrations/configuration"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://doctrine-project.org/schemas/migrations/configuration
                    http://doctrine-project.org/schemas/migrations/configuration.xsd">

    <name>Doctrine Sandbox Migrations</name>

    <migrations-namespace>DoctrineMigrations</migrations-namespace>

    <table name="doctrine_migration_versions" />;

    <migrations-directory>data/doctrine-migrations</migrations-directory>

</doctrine-migrations>

Der Parameter name kann frei vergeben werden. Dieser Text wird beim Ausführen der Migration auf der Kommandozeile angezeigt. Der migration-namespace wird in den Versions-Dateien verwendet. Auch dieser kann frei vergeben werden. Mit table_name wird der Name der Tabelle festgelegt, in der die „Versionsnummern“ der Migration abgelegt werden. Zum Schluss geben wir noch das Verzeichnis an, in dem die Migrations-Scripte liegen. Wichtig ist zu beachten, dass diese immer relativ vom Pfad zu sehen sind, von wo aus die Migration aufgerufen wird. Da ich immer vom Projekt-Hauptverzeichnis aus arbeite, beginnt der Pfad mit dem data-Verzeichnis.

Erstes Migrationsscript erstellen

Die Verzeichnisse für die Module haben wir bereits angelegt. Nun müssen wir diese nur noch mit Leben füllen. Fangen wir mit dem Hello-Modul an. Zunächst erstellen wir die Datei modules/Hello/config/module.config.php mit folgendem Inhalt:

<?php

return array(
    'doctrine' => array(
        'driver' => array(
            'hello_entity' => array(
                'class' => 'Doctrine\ORM\Mapping\Driver\XmlDriver',
                'paths' => __DIR__ . '/doctrine'
            ),
            'orm_default' => array(
                'drivers' => array(
                    'Hello\Entity' => 'hello_entity'
                ),
            ),
        ),
    )
);

Damit haben wir die Konfiguration erstellt. Jetzt fehlt noch die Modul-Klasse modules/Hello/Module.php:

<?php

namespace Hello;

use Zend\ModuleManager\Feature\ConfigProviderInterface;

class Module implements ConfigProviderInterface
{
    public function getAutoloaderConfig()
    {
        return array(
            'Zend\Loader\StandardAutoloader' =&gt; array(
                'namespaces' => array(
                    __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
                ),
            ),
        );
    }

    public function getConfig()
    {
        return include __DIR__ .'/config/module.config.php';
    }
}

Wir sollten nicht vergessen das Modul auch in der config/application.config.php einzutragen.

Nun fehlt unsere erste Entity-Definition. Leider bietet das Doctrine-Modul noch keine Möglichkeit wie bei Symfony, Entity-Konfigurationen über die Kommandozeile zu erstellen. Also kopieren wir einfach folgenden Text in die module/Hello/config/doctrine/Hello.Entity.Post.dcm.xml:

<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
  <entity name="Hello\Entity\Post">
    <id name="id" type="integer" column="id">
      <generator strategy="AUTO"/>
    </id>
    <field name="header" type="string" column="header" length="255"/>
    <field name="content" type="text" column="content"/>
  </entity>
</doctrine-mapping>

Nun können wir die erste Entity erstellen. Dazu rufen wir folgenden Befehl auf der Kommandozeile auf:

vendor/bin/doctrine-module orm:generate:entities module/Hello/src/

Jetzt haben wir eine Entity im Verzeichnis module/Hello/src/Hello/Entity. Jetzt generieren wir unser erstes Migration-Script:

vendor/bin/doctrine-module migration:diff --configuration=config/migration.xml

Im Verzeichnis data/doctrine-migration müsste nun eine erste Datei mit SQL-Anweisungen erstellt worden sein. Nun müssen wir diese Migration auch noch ausführen:

vendor/bin/doctrine-module migration:migrate --configuration=config/migration.xml

Nun sollte die erste Tabelle in der Datenbank erstellt worden sein.

Zweites Migrationsscript erstellen

Jetzt fehlt noch das World-Modul und die zweite Entity im Hello-Modul. Fangen wir mir der zweiten Entity an. Dazu kopieren wir folgendes Schema in die module/Hello/config/doctrine/Hello.Entity.Comment.dcm.xml:

<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
  <entity name="Hello\Entity\Comment">
    <id name="id" type="integer" column="id">
      <generator strategy="AUTO"/>
    </id>
    <field name="name" type="string" column="name" length="255"/>
    <field name="content" type="text" column="content"/>
  </entity>
</doctrine-mapping>

Jetzt erstellen wir die zweite Entity:

vendor/bin/doctrine-module orm:generate:entities module/Hello/src/

Hier müssen wir aufpassen, da die bereits vorhandene Post-Entity neu erstellt wird! Also immer schön mit Versionsverwaltung arbeiten. Nun bringen wir noch schnell das World-Modul zum laufen. Dazu legen wir die Datei module/World/config/module.config.php an

<?php

return array(
    'doctrine' => array(
        'driver' => array(
            'world_entity' => array(
                'class' => 'Doctrine\ORM\Mapping\Driver\XmlDriver',
                'paths' => __DIR__ . '/doctrine'
            ),
            'orm_default' => array(
                'drivers' => array(
                    'World\Entity' => 'world_entity'
                ),
            ),
        ),
    )
);

und erstellen auch noch die module/World/Module.php:

<?php

namespace World;

use Zend\ModuleManager\Feature\ConfigProviderInterface;

class Module implements ConfigProviderInterface
{
    public function getAutoloaderConfig()
    {
        return array(
            'Zend\Loader\StandardAutoloader' =&gt; array(
                'namespaces' => array(
                    __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
                ),
            ),
        );
    }

    public function getConfig()
    {
        return include __DIR__ .'/config/module.config.php';
    }

}

Nicht zu vergessen, das Modul auch noch in der config/application.config.php einzutragen!

Die Definition der Entity unter module/World/config/doctrine/World.Entity.World.dcm.xml hat diesen Inhalt:

<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
  <entity name="World\Entity\World">
    <id name="id" type="integer" column="id">
      <generator strategy="AUTO"/>
    </id>
    <field name="name" type="string" column="name" length="255"/>
  </entity>
</doctrine-mapping>

Beim erstellen der Entity müssen wir nun aber aufpassen! Doctrine generiert alle Entities in das Zielverzeichnis, auch wenn diese zu verschiedenen Modulen gehören. Um dies zu verhindern müssen wir nun die Option --filter beim Aufruf nutzen.

vendor/bin/doctrine-module orm:generate-entities --filter="World" module/World/src/

Damit sollte auch die World-Entity erstellt sein.

Kommen wir nun zum letzten Schritt: Die letzte Migration. Dazu rufen wir wieder den Befehl auf:

vendor/bin/doctrine-module migration:diff --configuration=config/migration.xml

Ein Blick in die Datei verrät uns, dass diese nun SQL-Anweisungen für beide Module enthält. Folglich sollte auch hier nach jeder Aktualisierung der Module ein neues Migrations-Script erstellt und angepasst werden.

Schlusswort

Bei dem ZendFramework2 muss man zwar ein bisschen mehr Arbeit investieren, aber auch hier ist das Erstellen von Migrations-Scripten kein Problem. Wer noch mehr über Doctrine Migration erfahren möchte, sollte sich die Dokumentation ansehen.