---
title: "Migrations"
description: "The configuration of the CLI of Doctrine Migrations from a DSN or an array, without a framework"
type: "docs"
category: "doc"
tags: []
authors: [Anonymous]
date: "2026-10-26"
last_update: "2026-10-26"
time_minutes: 5
draft: false
unlisted: false
url: "https://www.derafu.dev/docs/data/orm/migrations"
---

# Migrations

The CLI of [Doctrine Migrations](https://www.doctrine-project.org/projects/migrations.html), `vendor/bin/doctrine-migrations`, has to know the database that it connects to and the directory of the migrations. In an application with `doctrine-migrations-bundle` the bundle gives it, and its commands are used instead. Without a framework, the convention of Doctrine is a `cli-config.php` file that returns a `DependencyFactory`. Without that file the command stops with `It was not possible to locate any configuration file.`

`DoctrineCliConfigurationFactory` creates that `DependencyFactory` from a DSN or from an array, so the file is a single line.

The CLI looks for a file called `cli-config.php` in the directory where the command runs, and then in its `config/` subdirectory, so it can be in the root of the project or in `config/`. The file returns the factory:

```php
use Derafu\Orm\Factory\DoctrineCliConfigurationFactory;

return (new DoctrineCliConfigurationFactory())->create('postgresql://app:secret@127.0.0.1:5432/app?serverVersion=16&charset=utf8');
```

The credentials do not have to be in the file: `create((string) getenv('DATABASE_URL'))` reads the DSN from the environment.

## DSN

| Database | DSN |
|----------|-----|
| PostgreSQL | `postgresql://app:secret@127.0.0.1:5432/app?serverVersion=16&charset=utf8` |
| MySQL, MariaDB | `mysql://app:secret@127.0.0.1:3306/app?serverVersion=8.0&charset=utf8mb4` |
| SQLite | `sqlite:///var/data.db`, relative to the current directory, or `sqlite:////path/to/data.db`, absolute |

The DSN is read with DBAL's `DsnParser`, as the `url` option of the [entity manager](https://www.derafu.dev/docs/data/orm/entity-manager#dbal), so the other schemes and the rules are the same: `pgsql` and `postgres` are the same driver as `postgresql`, the parameters after `?` are parameters of the connection (`serverVersion`, `charset`, `sslmode`...), and a special character in the user or the password is written URL-encoded (`p@ss` is `p%40ss`).

## Array

With an array, which has the sections `dbal` and `orm` of the entity manager and `migrations`:

```php
return (new DoctrineCliConfigurationFactory())->create([
    'migrations' => [
        'paths' => ['App\\Migrations' => __DIR__ . '/migrations'],
        'all_or_nothing' => true,
        'transactional' => true,
        'check_database_platform' => true,
    ],
    'dbal' => ['driver' => 'pdo_sqlite', 'path' => __DIR__ . '/var/data.db'],
    'orm' => ['paths' => [__DIR__ . '/src/Entity']],
]);
```

In `config/cli-config.php`, `__DIR__` is the `config/` directory, so the paths are built from `dirname(__DIR__)`.

The three options of `migrations` are enabled by default. With a DSN, the migrations are in `migrations/` and the entities in `src/Entity/`, relative to the current directory, so the commands are run from the root of the project. The directories of the migrations, the entities and the SQLite file are created if they do not exist. The commands are those of Doctrine: `vendor/bin/doctrine-migrations diff`, `migrate` and `status`.

## Usage

The commands are run from the root of the project, with the file in place:

1. `vendor/bin/doctrine-migrations generate` creates an empty migration class in the directory of the migrations, with the `up()` and `down()` methods to complete.
2. `vendor/bin/doctrine-migrations diff` creates the migration by comparing the database with the entities of `orm.paths`: its `up()` has the SQL that makes the database like the entities (a `CREATE TABLE` for an entity whose table does not exist).
3. `vendor/bin/doctrine-migrations status` shows the database that it uses and the executed and available migrations.
4. `vendor/bin/doctrine-migrations migrate` executes the pending migrations and ends with `[OK] Successfully migrated to version: App\Migrations\Version20260101000000`.
5. `vendor/bin/doctrine-migrations up-to-date` tells if there is nothing left to execute: `[OK] Up-to-date! No migrations to execute.`

The executed migrations are recorded in the table `doctrine_migration_versions` of the database, which Doctrine creates. No entity maps that table, so `diff` leaves it out of the comparison and never proposes to drop it, and the other commands always see it, even if the `schema_filter` of `dbal` would leave it out.

`migrate` always asks for a confirmation, and `diff` asks for one when there are migrations that are not executed. Add `--no-interaction` to run them in a script or in a CI, where nobody can answer.

| Command | Purpose |
|---------|---------|
| `migrations:generate` (`generate`) | Create an empty migration |
| `migrations:diff` (`diff`) | Create a migration from the differences between the entities and the database |
| `migrations:migrate` (`migrate`) | Execute the migrations up to a version, or the latest |
| `migrations:execute` (`execute`) | Execute one or more versions, up or down |
| `migrations:status` (`status`) | Show the configuration and the state of the migrations |
| `migrations:list` (`list-migrations`) | List the migrations and their state |
| `migrations:current` (`current`), `migrations:latest` (`latest`) | Show the current version and the latest one |
| `migrations:up-to-date` (`up-to-date`) | Tell if the database is up to date |
| `migrations:version` (`version`) | Add or remove versions in the table of versions, without executing them |
| `migrations:rollup` (`rollup`) | Replace the recorded versions with a single one |
| `migrations:dump-schema` (`dump-schema`) | Write the schema of the database as a migration |
| `migrations:sync-metadata-storage` (`sync-metadata-storage`) | Update the table of versions to the latest format |

## Tables that no entity maps

`diff` makes the database like the entities, so it proposes to drop the tables of the database that no entity maps (a `DROP TABLE legacy_x` for a table of another application, for example). To keep it away from them, limit the tables that Doctrine reads with the `schema_filter` of the `dbal` section, a regular expression of the tables to keep:

```php
'dbal' => [
    'url' => 'postgresql://app:secret@127.0.0.1:5432/app?serverVersion=16&charset=utf8',
    'schema_filter' => '~^(?!legacy_)~',
],
```

The `--filter-expression` option of `diff` does the same for one run. When both are used, a table has to pass both.

With SQLite, a table whose identifier is a single integer without a generated value is proposed again by every `diff`, as Doctrine reads that table back as an auto-increment key. It is a behavior of Doctrine with SQLite, not of the factory.



---
Last updated on 26/10/2026

