Using migrations in concrete5 packages

The core uses migrations, but why wouldn't we want to use migrations in packages? Migrations can be used to e.g. install a block, set permissions, run some queries, install a page, etc. They might be a good alternative to your install/upgrade routines.

An approach I often see in packages is that the 'install' and 'upgrade' methods in a package controller both run the same routines / methods. I think it's a good approach, but once you have a big package, updating is going to take longer and longer. Up to a point that you think, hmm, maybe we should do things differently!

Example use case

Say your package installs 100 attribute keys. Should it, each time the package upgrades, check whether all those keys exist and if not, install them? Is that really necessary? Or can we just assume they exist, and install attribute key 101 via a migration? If you use a package for your company website or something, I find migrations are the way to go. They are way faster. I also find them more convenient to use, e.g. when assigning permissions.

Why migrations?

The idea is that each migration runs once. Say you want to set permissions of a certain page, you'd do so via a migration. There's no need to re-apply those permissions every time the package is updated. If you want to be able to re-run migrations too, you'd maybe implement the RepeatableMigrationInterface, but it would require a slightly different approach.

Show me some code

Aight. So I'll propose to add an 'upgrade' method to a package controller, and then use something as follows:

<?php
public function upgrade()
{
    $lastMigration = $this->app['config']->get('pkg_handle.last_migration');

    $iterator = new DirectoryIterator($this->getPackagePath() . '/src/Migrations');
    foreach ($iterator as $fileInfo) {
        // Skip directories.
        if ($fileInfo->isDot()) {
            continue;
        }

        // Run each migration once.
        if ($lastMigration > $fileInfo->getBasename()) {
            continue;
        }

        // Create an object of a single migration.
        $migration = $this->app->make(
            '\Concrete\Package\PkgHandle\Src\Migrations\\' .
            $fileInfo->getBasename('.' . $fileInfo->getExtension())
        );
        $migration->up();

        // Mark the migration as executed.
        $this->app['config']->save('pkg_handle.last_migration', $fileInfo->getBasename());
    }
    
    parent::upgrade();
}

You'd then create a directory with all your migrations. Just make sure they will be executed alphabetically. Using the date in the filename is good way, I find. Here's an example of a migration:

<?php

namespace Concrete\Package\PkgHandle\Src\Migrations;

use Concrete\Core\Page\Page;
use Concrete\Core\User\Group\Group;

class Version2018092401
{
    public function up()
    {
        /** @var Page $page */
        $page = Page::getByPath('/dashboard/calendar');

        $page->assignPermissions(Group::getByName('Administrators'), ['view_page']);
    }
}

Once that's done, you can run your package upgrade. E.g. via the web interface, or just via the CLI: './vendor/bin/concrete5 c5:package:update pkg_handle'.

Testing migrations

It's a bit annoying if you need to restore the package version and the config value each time you try to test a migration. For testing I like to use the 'c5:exec' CLI command. It bootstraps concrete5 and allows you to quickly execute a single PHP script. Once the script works OK, you can convert it easily to a migration.

Comments?

If you have ideas about this approach, feel free to let me know via the Feedback button. Thanks! :)