Doctrine: DBAL + Migrations ohne ORM

Die Tage konnte ich endlich die Frage beantworten, wie doctrine/dbal ohne ORM einsetzen und weiterhin den Luxus von Migrations genießen.

War doch einfacher als befürchtet.

Ich bin mal wieder beeindruckt, wie Legostein-mäßig sich Doctrine und Symfony-Komponenten zusammensetzen lassen.

Für mich war es wichtig, irgendein DBAL zu nutzen, um eine breitere Abdeckung an Datenbanken unterstützen zu können. Meine Wahl fiel auf doctrine/dbal, weil es einfach genial ist.

Und auf der anderen Seite wollte ich das Schema der DB irgendwie migrationsartig ausrollen können. Idealerweise auch Schema-Änderungen automatisiert generieren. Dafür gibt es doctrine/migrations.

doctrine/migrations kommt auch mit einer guten Beschreibung, wie wir Dinge in Custom Commands nutzen können. Zur Vereinfachung konzentrieren wir uns mal nur auf den Diff-Command.

#!/usr/bin/env php
<?php

// ... siehe https://www.doctrine-project.org/projects/doctrine-migrations/en/3.8/reference/custom-configuration.html

$dependencyFactory = DependencyFactory::fromConnection(
    new ExistingConfiguration($configuration),
    new ExistingConnection($connection),
);

$cli = new Application('Smoo - Do it');
$cli->setCatchExceptions(true);

$cli->addCommands([
    new Command\DiffCommand($dependencyFactory),
]);

$cli->run();

Und schon können wir sowas wie smoo migrations:diff ausführen und bekommen Fehlermeldungen wie „The schema provider is not available“.

Mit Schema ist der Stand der Datenbank gemeint, also Tabellen, Spalten, Schlüssel usw.

doctrine/migrations gibt uns ein Interface „SchemaProvider“, mit dem wir Doctrine sagen können: Hey, da kommt ein Schema. In dem SchemaProvider können wir dann unser Schema zusammenbasteln. Ein Beispiel:

<?php

namespace Smoothie\DbalAndMigrations;

use Doctrine\DBAL\Schema\Schema as DbalSchema;
use Doctrine\Migrations\Provider\SchemaProvider as DbalSchemaProvider;

class SchemaProvider implements DbalSchemaProvider
{
    public function createSchema(): DbalSchema
    {
        $schema = new DbalSchema();

        $table = $schema->createTable('users');

        $table->addColumn('id', 'integer', [
            'autoincrement' => true,
        ]);

        $table->addColumn('username', 'string', [
            'notnull' => false,
            'length' => 255,
        ]);

        $table->setPrimaryKey(['id']);

        return $schema;
    }
}

Nun müssen wir nur noch herausfinden, wie wir den SchemaProvider bereitstellen. Und auch dafür hat Doctrine eine Lösung.

Doctrine Service Management ist wirklich nice. Schaut euch mal die DependencyFactory an – einfach ein Genuss.

Wenn wir da reinschauen, stellen wir fest, es gibt eine Methode DependencyFactory::getSchemaProvider(). Jeder getter innerhalb der DependencyFactory kann zur Laufzeit überschrieben werden. Das gibt Grund zur Hoffnung, da es ja irgendwie möglich ist, die Methode nach Belieben anzupassen.

Und ja, so ist es.

Hier mal das Diff-Beispiel, leicht angepasst:

#!/usr/bin/env php
<?php

// ...

$dependencyFactory = DependencyFactory::fromConnection(
    new ExistingConfiguration($configuration),
    new ExistingConnection($connection),
);

// Neu dazu gekommen
$schemaProvider = new Smoothie\DbalAndMigrations\SchemaProvider();
$dependencyFactory->setDefinition(Doctrine\Migrations\Provider\SchemaProvider::class, static fn () => $schemaProvider);

$cli = new Application('Smoo - Do it');
$cli->setCatchExceptions(true);

$cli->addCommands([
    new Command\DiffCommand($dependencyFactory),
]);

$cli->run();

Einfach über DependencyFactory::setDefinition den Callback austauschen und schon ist der Drops gelutscht.

Nehmen wir mal eine klassische Symfony Flex-Anwendung. Da läuft dann so ein Service Container und den können wir konfigurieren, easy. Wenn wir nun sagen, hey, symfony/orm-pack wäre ein bisschen viel, gibt es da eine Alternative? Die Antwort lautet -> hell yeah.

Wir können mit Hilfe eines CompilerPass ganz einfach den SchemaProvider überschreiben und der Drops ist mal wieder gelutscht.

Hier mal als Beispiel (angenommen der SchemaProvider ist als Alias verfügbar):

<?php

declare(strict_types=1);

namespace Smoothie\DbalAndMigrations\Symfony\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class DoctrineMigrationsCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if ($container->has('doctrine.migrations')) {
            throw new \RuntimeException('"smoothie.DbalAndMigrations" requires "DoctrineMigrationsBundle" to be enabled.');
        }

        $definition = $container->getDefinition('doctrine.migrations.dependency_factory');
        $definition->addMethodCall(
            'setDefinition',
            [
                'Doctrine\Migrations\Provider\SchemaProvider',
                new ServiceClosureArgument(new Reference('smoothie.dbal-and-migrations.schema-provider')),
            ],
        );
    }
}

Dann nur noch den CompilerPass im Bundle bereitstellen und BAM, we are done.

Goil.

Zum Abschluss noch ein Package: TalisORM.

TalisORM fährt einen ziemlich interessanten Ansatz mit wie Schema und Domains zusammen hängen.

Bei mir löst es direkt die philosophischen Fragen auf: Was ist ein Schema? Wer definiert ein Schema? Etwa die Domain? Sollte die Domain Kenntnis darüber haben ob eine Datenbank im Einsatz ist? Wenn ja, wie weit geht diese Kenntnis?

Fragen über die ich mal wann anders weiter grübele..

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert