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