MigrationBundle
, (*1)
Doctrine based database schema and fixtures manipulator., (*2)
Overview
MigrationBundle is forked version of an open source ORO platform migration bundle. ORO developers made a great tool, but they are not interested in contributions from community. That's why we forked this bundle and made it available for everyone., (*3)
Features
- Database agnostic migrations
- Semantic migration versions
- Fast installation to latest version
- Automatic installer generation
- Versioned fixtures and sample data
- Custom extensions
- Migration hooks
Installation
Add the bundle to your composer.json:, (*4)
composer require ramunasd/migration-bundle
, (*5)
Then add the bundle to your application kernel:, (*6)
// app/AppKernel.php
<?php
use Symfony\Component\HttpKernel\Kernel;
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = array(
// ...
new Rdv\Bundle\MigrationBundle\RdvMigrationBundle(),
);
}
// ...
}
Configuration
``` yaml
rdv_migration:
migration_path: "Migrations/Schema"
fixtures_path_main: "Migrations/Data/ORM"
fixtures_path_demo: "Migrations/Data/Demo/ORM", (*7)
Database structure migrations
-----------------------------
Each bundle can have migration files that allow to update database schema.
Migration files should be located in `Migrations\Schema\version_number` folder. A version number must be an PHP-standardized version number string, but with some limitations. This string must not contain "." and "+" characters as a version parts separator. More info about PHP-standardized version number string can be found in [PHP manual][1].
Each migration class must implement [Migration](./Migration/Migration.php) interface and must implement `up` method. This method receives a current database structure in `schema` parameter and `queries` parameter which can be used to add additional queries.
With `schema` parameter, you can create or update database structure without fear of compatibility between database engines.
If you want to execute additional SQL queries before or after applying a schema modification, you can use `queries` parameter. This parameter represents a [query bag](./Migration/QueryBag.php) and allows to add additional queries which will be executed before (`addPreQuery` method) or after (`addQuery` or `addPostQuery` method). A query can be a string or an instance of a class implements [MigrationQuery](./Migration/MigrationQuery.php) interface. There are several ready to use implementations of this interface:
- [SqlMigrationQuery](./Migration/SqlMigrationQuery.php) - represents one or more SQL queries
- [ParametrizedSqlMigrationQuery](./Migration/ParametrizedSqlMigrationQuery.php) - similar to the previous class, but each query can have own parameters.
If you need to create own implementation of [MigrationQuery](./Migration/MigrationQuery.php) the [ConnectionAwareInterface](./Migration/ConnectionAwareInterface.php) can be helpful. Just implement this interface in your migration query class if you need a database connection. Also you can use [ParametrizedMigrationQuery](./Migration/ParametrizedMigrationQuery.php) class as a base class for your migration query.
If you have several migration classes within the same version and you need to make sure that they will be executed in a specified order you can use [OrderedMigrationInterface](./Migration/OrderedMigrationInterface.php) interface.
Example of migration file:
``` php
<?php
namespace Acme\Bundle\TestBundle\Migrations\Schema\v1_0;
use Doctrine\DBAL\Schema\Schema;
use RDV\Bundle\MigrationBundle\Migration\Migration;
use RDV\Bundle\MigrationBundle\Migration\QueryBag;
use RDV\Bundle\MigrationBundle\Migration\Extension\RenameExtension;
use RDV\Bundle\MigrationBundle\Migration\Extension\RenameExtensionAwareInterface;
class AcmeTestBundle implements Migration, RenameExtensionAwareInterface
{
/**
* @var RenameExtension
*/
protected $renameExtension;
/**
* @inheritdoc
*/
public function setRenameExtension(RenameExtension $renameExtension)
{
$this->renameExtension = $renameExtension;
}
/**
* @inheritdoc
*/
public function up(Schema $schema, QueryBag $queries)
{
$table = $schema->createTable('test_table');
$table->addColumn('id', 'integer', ['autoincrement' => true]);
$table->addColumn('created', 'datetime', []);
$table->addColumn('field', 'string', ['length' => 500]);
$table->addColumn('another_field', 'string', ['length' => 255]);
$table->setPrimaryKey(['id']);
$this->renameExtension->renameTable(
$schema,
$queries,
'old_table_name',
'new_table_name'
);
$queries->addQuery(
"ALTER TABLE another_table ADD COLUMN test_column INT NOT NULL",
);
}
}
Each bundle can have an installation file as well. This migration file replaces running multiple migration files. Install migration class must implement Installation interface and must implement up
and getMigrationVersion
methods. The getMigrationVersion
method must return max migration version number that this installation file replaces., (*8)
During an install process (it means that you installs a system from a scratch), if install migration file was found, it will be loaded first and then migration files with versions greater then a version returned by getMigrationVersion
method will be loaded., (*9)
For example. We have v1_0
, v1_1
, v1_2
, v1_3
migrations. And additionally, we have install migration class. This class returns v1_2
as a migration version. So, during an install process the install migration file will be loaded and then only v1_3
migration file will be loaded. Migrations from v1_0
to v1_2
will not be loaded., (*10)
Example of install migration file:, (*11)
``` php
<?php, (*12)
namespace Acme\Bundle\TestBundle\Migrations\Schema;, (*13)
use Doctrine\DBAL\Schema\Schema;
use RDV\Bundle\MigrationBundle\Migration\Installation;
use RDV\Bundle\MigrationBundle\Migration\QueryBag;, (*14)
class AcmeTestBundleInstaller implements Installation
{
/**
* @inheritdoc
*/
public function getMigrationVersion()
{
return 'v1_1';
}, (*15)
/**
* @inheritdoc
*/
public function up(Schema $schema, QueryBag $queries)
{
$table = $schema->createTable('test_installation_table');
$table->addColumn('id', 'integer', ['autoincrement' => true]);
$table->addColumn('field', 'string', ['length' => 500]);
$table->setPrimaryKey(['id']);
}
}, (*16)
To run migrations, there is **rdv:migration:load** command. This command collects migration files from bundles, sorts them by version number and applies changes.
This command supports some additional options:
- **force** - Causes the generated by migrations SQL statements to be physically executed against your database;
- **dry-run** - Outputs list of migrations without apply them;
- **show-queries** - Outputs list of database queries for each migration file;
- **bundles** - A list of bundles to load data from. If option is not set, migrations will be taken from all bundles;
- **exclude** - A list of bundle names which migrations should be skipped.
Also there is **rdv:migration:dump** command to help in creation installation files. This command outputs current database structure as a plain sql or as `Doctrine\DBAL\Schema\Schema` queries.
This command supports some additional options:
- **plain-sql** - Out schema as plain sql queries
- **bundle** - Bundle name for which migration wll be generated
- **migration-version** - Migration version number. This option will set the value returned by `getMigrationVersion` method of generated installation file.
Good practice for bundle is to have installation file for current version and migration files for migrating from previous versions to current.
Next algorithm may be used for new versions of your bundle:
- Create new migration
- Apply it with **rdv:migration:load**
- Generate fresh installation file with **rdv:migration:dump**
- If required - add migration extensions calls to generated installation.
Extensions for database structure migrations
--------------------------------------------
Sometime you cannot use standard Doctrine methods for database structure modification. For example `Schema::renameTable` does not work because it drops existing table and then creates a new table. To help you to manage such case and allow to add some useful functionality to any migration a extensions mechanism was designed. The following example shows how [RenameExtension][5] can be used:
``` php
<?php
namespace Acme\Bundle\TestBundle\Migrations\Schema\v1_0;
use Doctrine\DBAL\Schema\Schema;
use RDV\Bundle\MigrationBundle\Migration\Migration;
use RDV\Bundle\MigrationBundle\Migration\QueryBag;
use RDV\Bundle\MigrationBundle\Migration\Extension\RenameExtension;
use RDV\Bundle\MigrationBundle\Migration\Extension\RenameExtensionAwareInterface;
class AcmeTestBundle implements Migration, RenameExtensionAwareInterface
{
/**
* @var RenameExtension
*/
protected $renameExtension;
/**
* @inheritdoc
*/
public function setRenameExtension(RenameExtension $renameExtension)
{
$this->renameExtension = $renameExtension;
}
/**
* @inheritdoc
*/
public function up(Schema $schema, QueryBag $queries)
{
$this->renameExtension->renameTable(
$schema,
$queries,
'old_table_name',
'new_table_name'
);
}
}
As you can see to use the RenameExtension your migration class should implement [RenameExtensionAwareInterface][6] and setRenameExtension
method.
Also there is some additional useful interfaces you can use in your migration class:, (*17)
Create own extensions for database structure migrations
To create your own extension you need too do the following simple steps:, (*18)
- Create an extension class in
YourBundle/Migration/Extension
directory. Using YourBundle/Migration/Extension
directory is not mandatory, but highly recommended. For example:
``` php
<?php
namespace Acme\Bundle\TestBundle\Migration\Extension;, (*19)
use Doctrine\DBAL\Schema\Schema;
use RDV\Bundle\MigrationBundle\Migration\QueryBag;, (*20)
class MyExtension
{
public function doSomething(Schema $schema, QueryBag $queries, /* other parameters, for example / $tableName)
{
$table = $schema->getTable($tableName); // highly recommended to make sure that a table exists
$query = 'SOME SQL'; / or $query = new SqlMigrationQuery('SOME SQL'); */, (*21)
$queries->addQuery($query);
}
}, (*22)
- Create `*AwareInterface` in the same namespace. It is important that the interface name should be `{ExtensionClass}AwareInterface` and set method should be `set{ExtensionClass}({ExtensionClass} ${extensionName})`. For example:
``` php
<?php
namespace Acme\Bundle\TestBundle\Migration\Extension;
/**
* MyExtensionAwareInterface should be implemented by migrations that depends on a MyExtension.
*/
interface MyExtensionAwareInterface
{
/**
* Sets the MyExtension
*
* @param MyExtension $myExtension
*/
public function setMyExtension(MyExtension $myExtension);
}
- Register an extension in dependency container. For example
``` yaml
parameters:
acme_test.migration.extension.my.class: Acme\Bundle\TestBundle\Migration\Extension\MyExtension
services:
acme_test.migration.extension.my:
class: %acme_test.migration.extension.my.class%
tags:
- { name: rdv_migration.extension, extension_name: test /*, priority: -10 - priority attribute is optional an can be helpful if you need to override existing extension */ }, (*23)
If you need an access to the database platform or the name generator you extension class should implement [DatabasePlatformAwareInterface][3] or [NameGeneratorAwareInterface][4] appropriately.
Also if you need to use other extension in your extension the extension class should just implement `*AwareInterface` of the extension you need.
Data fixtures
-------------
Symfony allows to load data using data fixtures. But these fixtures are run each time when `doctrine:fixtures:load` command is executed.
To avoid loading the same fixture several time, **rdv:migration:data:load** command was created. This command guarantees that each data fixture will be loaded only once.
This command supports two types of migration files: `main` data fixtures and `demo` data fixtures. During an installation, user can select to load or not demo data.
Data fixtures for this command should be put in `Migrations/Data/ORM` or in `Migrations/Data/Demo/ORM` directory.
Fixtures order can be changed with standard Doctrine ordering or dependency functionality. More information about fixture ordering can be found in [doctrine data fixtures manual][2].
Versioned fixtures
------------------
There are fixtures which need to be executed time after time. An example is a fixture which uploads countries data. Usually, if you add new countries list, you need to create new data fixture which will upload this data. To avoid this you can use versioned data fixtures.
To make fixture versioned, this fixture must implement [VersionedFixtureInterface](./Fixture/VersionedFixtureInterface.php). Method `getVersion` returns a version of fixture data and `getLoadedVersion` a version of currently loaded fixture.
Example:
``` php
<?php
namespace Acme\DemoBundle\Migrations\Data\ORM;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\Persistence\ObjectManager;
use RDV\Bundle\MigrationBundle\Fixture\VersionedFixtureInterface;
class LoadSomeDataFixture extends AbstractFixture implements VersionedFixtureInterface
{
/**
* @var string
*/
protected $loadedVersion;
/**
* {@inheritdoc}
*/
public function getVersion()
{
return '1.0';
}
/**
* {@inheritdoc}
*/
public function setLoadedVersion($version = null)
{
$this->loadedVersion = $version;
}
/**
* {@inheritdoc}
*/
public function load(ObjectManager $manager)
{
// Here we can use fixture data code which will be run time after time
if ($this->loadedVersion === null) { // loadedVersion is null for first time
}
}
}
In this example, if the fixture was not loaded yet, it will be loaded and version 1.0 will be saved as current loaded version of this fixture., (*24)
To have possibility to load this fixture again, the fixture must return a version greater then 1.0, for example 1.0.1 or 1.1. A version number must be an PHP-standardized version number string. More info about PHP-standardized version number string can be found in PHP manual., (*25)