Edit on GitHub

Installers

Besides being enabled, bundles may need to execute installation tasks in order to be fully functional. This may concern tasks like

  • creating database tables
  • creating or updating class definitions
  • importing translations
  • updating database tables or definitions after an update to a newer version
  • ...

To give bundles full control over their install routines, Pimcore only defines a basic installer interface which must be implemented by your installer. The methods implemented by your installer drive the extension manager UI and are called when an action is triggered from the extension manager or from commands like pimcore:bundle:install. The basic installer interface can be found in InstallerInterface which is implemented in AbstractInstaller which you can use as starting point.

A pimcore bundle is expected to return an installer instance in getInstaller(). This method can also return null if you don't need any installation functionality. In this case, actions which would be handled by an installer will not be available in the extension manager (e.g. the install button is not shown).

It's recommended to define the installer as service and to fetch it from the container from your bundle class on demand.
As example:

services:
    AppBundle\Installer:
        public: true
<?php

namespace AppBundle;

use Pimcore\Extension\Bundle\AbstractPimcoreBundle;

class AppBundle extends AbstractPimcoreBundle
{
    public function getInstaller()
    {
        return $this->container->get(Installer::class);
    }
}

Migrations

A common tasks in evolving bundles is to update an already existing/installed data structure to a newer version while also supporting fresh installs of your bundle. To be able to apply versioned changes (migrations), Pimcore integrates the Doctrine Migrations library which provides a powerful migration framework. Building on Doctrine Migrations, Pimcore adds the following features:

  • There can be multiple, independent migration sets. A migration set can be seen as isolated set of migrations which can be executed in order. Each bundle has its own migration set, concerning only its data structures.
  • Doctrine Migrations is focused on database changes. Pimcore adds functionality to be able to use the migrations for generic changes (e.g. changing class definitions).
  • An installer can define a specialized install version, which is independent of the remaining migrations. This migration is executed on bundle install and reverted on bundle uninstall.

There is a dedicated documentation page regarding migrations which you should read before continuing on this page.

To use the migrations for your bundle, you can extend the MigrationInstaller in your own installer class to make your bundle installer handle migrations. This installer implements the MigrationInstallerInterface which adds support for migrations to your installer.

services:
    # note the autowiring - the migration installer has a couple of other dependencies
    _defaults:
        autowire: true
        autoconfigure: true
        public: false

    # The migration installer needs the bundle it is operating on upon construction to be able to build its migration configuration.
    # As bundles can't be directly used as service argument, we need to make use of the expression language to fetch the bundle
    # from the kernel upon construction.
    AppBundle\Installer:
        public: true
        arguments:    
            # fetch the bundle via expression language
            $bundle: "@=service('kernel').getBundle('AppBundle')"
<?php

namespace AppBundle;

use Doctrine\DBAL\Migrations\Version;
use Doctrine\DBAL\Schema\Schema;
use Pimcore\Extension\Bundle\Installer\MigrationInstaller;

class Installer extends MigrationInstaller
{
    public function migrateInstall(Schema $schema, Version $version)
    {
        $table = $schema->createTable('my_bundle');
        $table->addColumn('id', 'integer', [
            'autoincrement' => true,
        ]);

        $table->addColumn('name', 'string');
        $table->setPrimaryKey(['id']);
        
        // or
        // $version->addSql('CREATE TABLE my_bundle ...');
    }

    public function migrateUninstall(Schema $schema, Version $version)
    {
        $schema->dropTable('my_bundle');
        
        // or
        // $version->addSql('DROP TABLE my_bundle');
    }
}

As you can see, you only need to implement the methods migrateInstall and migrateUninstall which are executed on installation and uninstallation of your bundle. This is executed as the specialized install version mentioned above, which has the fixed version 00000001. After installation your bundle will be migrated to version 00000001, on uninstallation it will migrate down to 0, thus reverting the install migration and calling the migrateUninstall method.

The methods receive a Schema object which can be used to modify the database in an object oriented way and a Version object which contains metadata on the current migration. The Version class also provides an addSql() method to register raw SQL queries which should be executed instead of the schema changes. Please see the Docrine Migrations documentation for details (the version on a normal migration is available as $this->version and a normal migration class provides an addSql method which in turn is delegated to the version);

In addition to those methods, a couple of before/after methods are available to execute logic before or after install/uninstall migrations. In addition, executeMigration and migrateToVersion methods can be used to specifically execute a certain migration.

As our bundle is now ready to be installed, we can execute the installation routine (this can also be done from the extension manager UI):

$ bin/console pimcore:bundle:install AppBundle
Installing bundle AppBundle

Migrating up to 00000001 from 0

  ++ migrating 00000001

     -> CREATE TABLE my_bundle (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB

  ++ migrated (0.74s)

  ------------------------

  ++ finished in 0.74s
  ++ 1 migrations executed
  ++ 1 sql queries


 [OK] Bundle "AppBundle" was successfully installed

The installer migrated to the special version 00000001 and applied our database changes.

Writing migrations

As you can see in the example above, the schema defines the initial database schema for our bundle. Now assume we need an additional column on our database. We want to create this column on every instance which either installs the bundle for the first time or which updates an already installed bundle. On installation, the MigrationInstaller will internally call the update method after installation which makes sure that all unmigrated migrations are migrated. Already existing instances can directly use update to apply unmigrated migrations.

Start by generating a migration for your bundle:

$ bin/console pimcore:migrations:generate -b AppBundle
Generated new migration class to "src/AppBundle/Migrations/Version20170822151849.php"

This migration class defines up and down methods which are executed when the migration is executed/reverted. You can use the same schema object as above or add raw SQL queries:

<?php

namespace AppBundle\Migrations;

use Doctrine\DBAL\Schema\Schema;
use Pimcore\Migrations\Migration\AbstractPimcoreMigration;

class Version20170822151849 extends AbstractPimcoreMigration
{
    public function up(Schema $schema)
    {
        $table = $schema->getTable('my_bundle');
        $table->addColumn('test', 'text');
        
        // or
        // $this->addSql('ALTER TABLE my_bundle ...');
    }

    public function down(Schema $schema)
    {
        $table = $schema->getTable('my_bundle');
        $table->dropColumn('test');
    }
}

After creating the migration, you can either do a fresh install of your bundle or update the already installed bundle:

# this could also done via extension manager UI
$ bin/console pimcore:bundle:update AppBundle
Migrating up to 20170822151849 from 00000001

  ++ migrating 20170822151849

     -> ALTER TABLE my_bundle ADD test LONGTEXT NOT NULL

  ++ migrated (0.68s)

  ------------------------

  ++ finished in 0.68s
  ++ 1 migrations executed
  ++ 1 sql queries


 [OK] Bundle "AppBundle" was successfully updated

Keeping the install schema up to date

While developing your application, you might generate numerous migrations. Instead of applying every single migration on every fresh install, you can also configure the installer to mark a given version as migrated without actually executing the migration classes. This gives you the advantage to define the whole schema in the same migration and to have a complete overview what has to be installed. You just need to make sure to apply the same changes to instances which are updates. To mark a specific version, implement the getMigrationVersion() method. This version and all lower versions will be marked as migrated and won't be executed on an update.

Starting from the example above, we directly update our install schema with the changes we want to install define a migration version to mark:

<?php

namespace AppBundle;

use Doctrine\DBAL\Migrations\Version;
use Doctrine\DBAL\Schema\Schema;
use Pimcore\Extension\Bundle\Installer\MigrationInstaller;

class Installer extends MigrationInstaller
{
    public function getMigrationVersion(): string
    {
        return '20170822151849';
    }   
    
    public function migrateInstall(Schema $schema, Version $version)
    {
        $table = $schema->createTable('my_bundle');
        $table->addColumn('id', 'integer', [
            'autoincrement' => true,
        ]);

        $table->addColumn('name', 'string');
        
        // this was added
        $table->addColumn('test', 'text');
        
        $table->setPrimaryKey(['id']);  
    }

    public function migrateUninstall(Schema $schema, Version $version)
    {
        $schema->dropTable('my_bundle');
    }
}

Now, let's see what happens on a fresh install:

$ bin/console pimcore:bundle:install AppBundle
Installing bundle AppBundle

Migrating up to 00000001 from 0

  ++ migrating 00000001

     -> CREATE TABLE my_bundle (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, test LONGTEXT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB

  ++ migrated (0.57s)

  ------------------------

  ++ finished in 0.57s
  ++ 1 migrations executed
  ++ 1 sql queries
  -- Marking version 20170822151849 as migrated


 [OK] Bundle "AppBundle" was successfully installed
 

As you can see, the initial schema directly contains the test column and 20170822151849 is marked as migrated without actually being executed. In contrast, this is what happens on an update when updating the bundle from an earlier version:

$ bin/console pimcore:bundle:update AppBundle
Migrating up to 20170822151849 from 00000001

  ++ migrating 20170822151849

     -> ALTER TABLE my_bundle ADD test LONGTEXT NOT NULL

  ++ migrated (0.7s)

  ------------------------

  ++ finished in 0.7s
  ++ 1 migrations executed
  ++ 1 sql queries


 [OK] Bundle "AppBundle" was successfully updated

This does what we expect, but now we have duplicate code in installation migration and our migration class. Depending on your use case you could also tell the installer to manually execute your migration after installation. In this example it basically does the same as it would do if you don't mark a version, but you could use this to execute only a handful of needed migrations (e.g. ones with complex logic):

<?php

namespace AppBundle;

use Doctrine\DBAL\Migrations\Version;
use Doctrine\DBAL\Schema\Schema;
use Pimcore\Extension\Bundle\Installer\MigrationInstaller;

class Installer extends MigrationInstaller
{
    public function getMigrationVersion(): string
    {
        return '20170822151849';
    }

    public function migrateInstall(Schema $schema, Version $version)
    {
        $table = $schema->createTable('my_bundle');
        $table->addColumn('id', 'integer', [
            'autoincrement' => true,
        ]);

        $table->addColumn('name', 'string');

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

    protected function afterInstallMigration()
    {
        // manually define migrations to run after installation
        $this->executeMigration('20170822151849');
    }

    public function migrateUninstall(Schema $schema, Version $version)
    {
        $schema->dropTable('my_bundle');
    }
}

As you can see in the output, the defined migration is executed:

$ bin/console pimcore:bundle:install AppBundle
Installing bundle AppBundle

Migrating up to 00000001 from 0

  ++ migrating 00000001

     -> CREATE TABLE my_bundle (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB

  ++ migrated (0.69s)

  ------------------------

  ++ finished in 0.69s
  ++ 1 migrations executed
  ++ 1 sql queries

  ++ migrating 20170822151849

     -> ALTER TABLE my_bundle ADD test LONGTEXT NOT NULL

  ++ migrated (0.49s)


 [OK] Bundle "AppBundle" was successfully installed

Uninstallations

The MigrationInstaller by default does NOT revert any migrations besides the install migration on uninstallation. As it is bundle specific what kind of data/structures need to be removed on uninstall, it's completely up to you what you want to do on uninstall.

The default logic is:

  • Migrate the install migration down - calls migrateUninstall()
  • Clear any migration states regarding the bundle in the pimcore_migrations table

From this point on the bundle would execute all migrations on a fresh installation. As this may run into errors due to duplicate tables, it is recommended to make your migrations as failsafe as possible (e.g. check if a table exists before creating it).

As in the examples above, you can directly execute migrations on uninstallation:

<?php

namespace AppBundle;

use Pimcore\Extension\Bundle\Installer\MigrationInstaller;

class Installer extends MigrationInstaller
{
    // [...]
    
    protected function beforeUninstallMigration()
    {
        $this->migrateToVersion('0');
        $this->outputWriter->write(PHP_EOL);

        // or manually revert a single migration - the second parameter defines the migration as being migrated down
        // $this->executeMigration('20170822151849', false);
    }
}

Interacting with migrations directly

If needed, you can directly interact with the migrations library (actually, we already did that above when generating a new migration) to directly execute a migration or to get informations on the current migration state. You can find the CLI commands in the pimcore:migrations namespace. As example:

$ $ bin/console pimcore:migrations:status -b AppBundle
  
   == Configuration
  
      >> Name:                                               AppBundle Migrations
      >> Database Driver:                                    pdo_mysql
      >> Database Name:                                      pimcore5
      >> Configuration Source:                               manually configured
      >> Version Table Name:                                 pimcore_migrations
      >> Version Column Name:                                version
      >> Migrations Namespace:                               AppBundle\Migrations
      >> Migrations Directory:                               src/AppBundle/Migrations
      >> Previous Version:                                   2017-08-22 15:18:49 (20170822151849)
      >> Current Version:                                    2017-08-22 16:07:03 (20170822160703)
      >> Next Version:                                       Already at latest version
      >> Latest Version:                                     2017-08-22 16:07:03 (20170822160703)
      >> Executed Migrations:                                3
      >> Executed Unavailable Migrations:                    1
      >> Available Migrations:                               2
      >> New Migrations:                                     0

By adding the -b option, you configure the migrations commands to use a bundle configuration. Alternatively, you can also use the global migration set which is not bundle specific by omitting the -b option. This gives you the possibility to define application wide migrations which are not bound to an installer. To execute those migrations, please directly use the pimcore:migrations:migrate command instead of pimcore:bundle:update.


For further details please see