Codice: obsługa migracji baz danych dla wtyczek

W poprzednim wpisie przedstawiłem zarys głównego API stojącego za obsługą rozszerzeń w Codice. Nie jest to oczywiście całość systemu, wtyczki poza integracją ze rdzeniem za pomocą akcji i filtrów muszą mieć zapewnione inne możliwości, pozwalające na swobodne operowanie. Jedną z bardziej istotnych jest wpływ na strukturę bazy danych.

W większości nowoczesnych frameworków dzieje się to za pomocą migracji, czyli opisanych w ustalony wcześniej sposób procedur kolejnych zmian w bazie danych. Dotyczy to dodawania, usuwania a także modyfikowania kolumn lub całych tabel. Przy instalacji aplikacji (także Codice) wszystkie migracje są wykonywane w kolejności ich definiowania - zapewnia to między innymi bardzo pożądaną możliwość wersjonowania struktury bazy danych.

Po tym opisie widać już że bardzo wskazane byłoby posiadanie analogicznego mechanizmu dla wtyczek. Ponieważ migracje powinno definiować się dwustronnie, można założyć że będą one aplikowane przy instalacji wtyczki, a wszelkie wprowadzone do struktury bazy zmiany będą cofane przy deinstalacji rozszerzenia (owa "druga strona" migracji).

By osiągnąć założone cele nie będziemy tworzyć zupełnie autorskiego systemu. Wszakże Laravel posiada już niemal identyczny mechanizm - brakuje tylko możliwości przypisania każdej z migracji konkretnej wtyczki. Pierwotnie próbowałem osiągnąć to za pomocą parametrów przekazywanych do komendy artisan migrate. Dokumentacja wspomina bowiem o opcji --path, pozwalającej na wykonanie migracji ograniczonych do jednej lokalizacji w systemie plików. Okazuje się jednak że w ten sposób nie możemy odwrócić migracji. Zdecydowałem się więc na rozszerzenie domyślnego repozytorium (w rozumieniu architektury aplikacji obiektowych) migracji o potrzebne wymagania.

<?php

namespace Codice\Plugins;

use Illuminate\Database\ConnectionResolverInterface as Resolver;
use Illuminate\Database\Migrations\DatabaseMigrationRepository;
use Illuminate\Database\Migrations\MigrationRepositoryInterface;

class MigrationRepository extends DatabaseMigrationRepository implements MigrationRepositoryInterface
{
    protected $plugin;

    /**
     * Create a new database migration repository instance.
     *
     * @param  Illuminate\Database\ConnectionResolverInterface  $resolver
     * @param  string  $plugin  Unique application-wide plugin identifier
     */
    public function __construct(Resolver $resolver, $plugin)
    {
        $this->resolver = $resolver;
        $this->plugin = $plugin;
        $this->table = 'migrations_plugins';

        parent::__construct($resolver, $this->table);
    }

    /**
     * Get the ran migrations.
     *
     * @return array
     */
    public function getRan()
    {
        return $this->table()
            ->where('plugin', $this->plugin)
            ->orderBy('batch', 'asc')
            ->orderBy('migration', 'asc')
            ->pluck('migration')->all();
    }

    /**
     * Get list of migrations.
     *
     * @param  int  $steps
     * @return array
     */
    public function getMigrations($steps)
    {
        $query = $this->table()
            ->where('plugin', $this->plugin)
            ->where('batch', '>=', '1');

        return $query->orderBy('migration', 'desc')->take($steps)->get()->all();
    }

    /**
     * Get the last migration batch.
     *
     * @return array
     */
    public function getLast()
    {
        $query = $this->table()
            ->where('plugin', $this->plugin)
            ->where('batch', $this->getLastBatchNumber());

        return $query->orderBy('migration', 'desc')->get()->all();
    }

    /**
     * Log that a migration was run.
     *
     * @param  string  $file
     * @param  int     $batch
     * @return void
     */
    public function log($file, $batch)
    {
        $record = ['plugin' => $this->plugin, 'migration' => $file, 'batch' => $batch];

        $this->table()->insert($record);
    }

    /**
     * Remove a migration from the log.
     *
     * @param  object  $migration
     * @return void
     */
    public function delete($migration)
    {
        $this->table()->where('migration', $migration->migration)->delete();
    }

    /**
     * Create the migration repository data store.
     *
     * @return void
     */
    public function createRepository()
    {
        $schema = $this->getConnection()->getSchemaBuilder();

        $schema->create($this->table, function ($table) {
            $table->increments('id');
            $table->string('plugin');
            $table->string('migration');
            $table->integer('batch');
        });
    }
}

Nadpisywane są wyłącznie niektóre metody, dokładnie tak aby po przekazaniu do konstruktora klasy identyfikatora danej wtyczki, wykonywać operacje na powiązanych z nią migracjach. Ich wywoływanie przebiega niemal analogicznie do klasycznych migracji wbudowanych w Laravela, informacji zasięgnąłem oczywiście buszując w jego źródłach (i zdecydowanie polecam to rozwiązanie, Larwę za wiele rzeczy można słusznie skrytykować, ale w kodzie naprawdę łatwo się połapać).

// ...

// Metoda pomocnicza.
// Używamy wbudowanego migratora, przekazujemy jedynie własne repozytorium.
protected function prepareMigrator($identifier)
{
    $repository = new MigrationRepository($this->app['db'], $identifier);

    if (!$repository->repositoryExists()) {
        $repository->createRepository();
    }

    return new Migrator($repository, $this->app['db'], $this->app['files']);
}

// ...

// Przy instalacji wtyczki
$migrator = self::prepareMigrator($identifier);
$migrator->run([$plugin->path('migrations')]);

// I analogicznie przy jej odinstalowywaniu
$migrator = self::prepareMigrator($identifier);
$migrator->rollback([$plugin->path('migrations')]);

Jak widać jest to naprawdę nieskomplikowane zadanie. Nie dajcie się zmylić ilości kodu, większość z niego to delikatnie zmodyfikowane metody klasy rodzica. Co ważniejsze, kryje się za tym życiowa lekcja.

Nie wynajduj koła od nowa, zwłaszcza jeśli masz już cały zestaw narzędzi w postaci frameworka. Poświęć chwilę na zgłębienie jego działania, zaoszczędzisz wiele czasu i bólu głowy.

~Sobak, gdzieś w tym momencie

Pozdrawiam!


Powyższy wpis przedstawia część doświadczeń zdobytych przy tworzeniu projektu Codice. Po więcej szczegółów zapraszam do pierwszego wpisu oraz na codice.eu. Poza planowaną od dawna serią jest to też realizacja wymogów konkursu "Daj Się Poznać". Więcej o nim możecie przeczytać w tym wpisie. Po nowe materiały poświęcone tworzeniu projektu Codice zapraszam w każdy wtorek. Ponadto więcej wpisów, poświęconych temu projektowi lub ogólnej tematyce bloga przeczytacie co sobotę. Miejmy nadzieję, że tym razem już zgodnie z harmonogramem!

Komentarze wyłączone

Możliwość komentowania na blogu została wyłączona. Zapraszam do kontaktu na Twitterze, Facebooku lub poprzez formularz, o ten tutaj. Do usłyszenia!