Because Flarum supports optional extension dependencies, there is a use case for optional database migrations that should only be applied if another extension is installed/enabled.

In particular, this allows achieving:

  • Add column to a table created by the optional dependency extension
  • Create pivot tables and foreign keys for tables created by optional dependencies

This cannot be achieved with regular migrations because there's a need for the migration to be attempted again every time an extension is enabled. And an extension cannot delete itself from the MigrationRepository (migrations table) from within the migration since that record is only updated after the migration returns successfully.

My proposal is to add a when callback key to migration arrays. When running up migrations, the when key would run first. If it exists and returns a falsy value, up is skipped and no migration is written to the MigrationRepository. On down migrations, everything would always run and the down migration can simply become a no-op if it cannot find the columns/tables that it needs to delete.

I have implemented this proposal as part of flamarkt/backoffice. The migrator changes can be found in https://github.com/flamarkt/backoffice/blob/main/src/Database/AugmentedMigrator.php while an example usage can be found in flamarkt/taxonomies https://github.com/flamarkt/taxonomies/blob/main/migrations/20210401_000400_create_product_term_table.php

A different implementation could be to use a special exception that the up migration could throw to cause the migration to be skipped without stopping the migrator.

Supporting down migration so that they run when the dependency is uninstalled without running all of the extension's down migrations is probably way too complex and I see a lot less use case for that (would only be useful when changing existing columns that must be reverted)

Are there other use cases of implementation concerns I might not have thought of?

clarkwinkelmann changed the title to Optional/conditional database migrations .

I had a similar thought for conditional extenders. Wouldn't it be a better generalised solution like this if we only run migrations if they are registered from an extender?

return [
     Extend\Migration::from(__DIR__ . '/migrations')
]

with conditional:

return [
    Extend\When('flarum/tags', fn() => Extender\Migration::from(__DIR__ . '/migrations'))
],

This would then make it easier to conditionally load any extender.

    luceos I think for most use cases that would work just as well. But I still think more flexibility around migrations would be needed:

    • Additional conditions: a table might only exist in a specific version of another extension, or might only exist on some conditions determinated by the other extension which you don't want to duplicate.
    • Timing: under some circumstances, you might want to run your migrations against another extension's table, even if that other extension was installed but then disabled.

    EDIT: however 100% for an extender that registers additional database migration folders!