This covers the documentation on how to create migrations in Contao 4.9 and up. In previous Contao versions, migrations were written in runonce.php files that got deleted after execution.

Updating Contao, extensions or the application itself sometimes requires to migrate data to be compatible with the new version(s). For this purpose Contao has a migration framework that lets you write migration services that are integrated in the update process.

The migrations get executed via the install tool database update or with the contao:migrate command.


To add a new migration, create a service that implements the interface Contao\CoreBundle\Migration\MigrationInterface and add the tag contao.migration (if autoconfiguration is enabled, this happens automatically):

# config/services.yaml
            - { name: contao.migration, priority: 0 }


The migration interface specifies three methods that need to be implemented:

  • getName()
    A name that describes what the migration does. This text is shown to users when they are asked if they want to execute the migration.

  • shouldRun()
    This method checks if all prerequisites that are needed for the migration to run are met and if it actually needs to run. This method should be written very defensively because the application might be in an unexpected state when the method gets called, e.g. the database could be completely empty.

  • run()
    As the name suggests, that is where the real magic happens. If shouldRun() returned true, this method will be called and should do the actual migration.
    It returns a MigrationResult object that can hold more information about what happened during the execution and if the migration was successful or not.
    If something goes unexpectedly wrong here and you want to abort the migration process completeley you should throw an exception here.

You can extend from Contao\CoreBundle\Migration\AbstractMigration which already implements the MigrationInterface and provides two methods: getName() and createResult(). You can use the latter to automatically generate a MigrationResult with a default message. You can also override its getName() method to provide a custom name for your migration, otherwise it will automatically use the FQCN of your migration class.


Lets say we have a database table tl_customers with a firstName and lastName column that we combined to a name column in the new version:

// src/Migration/CustomerNameMigration.php
namespace App\Migration;

use Contao\CoreBundle\Migration\AbstractMigration;
use Contao\CoreBundle\Migration\MigrationResult;
use Doctrine\DBAL\Connection;

class CustomerNameMigration extends AbstractMigration
    public function __construct(private readonly Connection $connection)

    public function shouldRun(): bool
        $schemaManager = $this->connection->createSchemaManager();

        // If the database table itself does not exist we should do nothing
        if (!$schemaManager->tablesExist(['tl_customers'])) {
            return false;

        $columns = $schemaManager->listTableColumns('tl_customers');

	        isset($columns['firstname']) &&
	        isset($columns['lastname']) &&

    public function run(): MigrationResult
            ALTER TABLE
                name varchar(255) NOT NULL DEFAULT ''

        $stmt = $this->connection->prepare("
                name = CONCAT(firstName, ' ', lastName)


        return $this->createResult(
            'Combined '. $stmt->rowCount().' customer names.'

Read more