Skip to content

IBX-11939: Doctrine migrations (6.0) - #3

Merged
Steveb-p merged 53 commits into
6.0from
feat/doctrine-migrations-6.0
Jul 28, 2026
Merged

Steveb-p merged 53 commits into
6.0from
feat/doctrine-migrations-6.0

Conversation

@Steveb-p

@Steveb-p Steveb-p commented Jun 29, 2026 •

Copy link
Copy Markdown
Collaborator

Warning

#1 (4.6) and #2 (5.0) have both landed (#1 merged, #2 closed after landing directly on 5.0), so this PR's base is now the real 6.0 branch itself rather than the temporary base/ibx-11939-4.6-5.0-merged-6.0 scaffold.

Warning

This is the 6.0 follow-up to #2 (5.0). Review that PR first — this carries the same migration forward onto this branch.

🎫 Issue IBX-11939

Related PRs:

Package 4.6 5.0 6.0
ibexa/activity-log #166 #167 #168 ⏭️
ibexa/cart #172 #173 🔄 #174
ibexa/collaboration – #115 #116
ibexa/connector-ai – #198 #199
ibexa/connector-payum ⚠️ #39 #40 ⏭️ #41
ibexa/core #785 🩹 #787 🔄 🩹 #788 🩹
ibexa/corporate-account #369 #370 🔄 #371
ibexa/discounts – #341 🩹 #342 🩹
ibexa/discounts-codes – #50 #51
ibexa/doctrine-migrations #1 #2 #3 ← this PR
ibexa/doctrine-schema #41 #42 #43
ibexa/fieldtype-page #208 🩹 #209 🔄 🩹 #210 🩹
ibexa/form-builder #240 🩹 #241 🔄 🩹 #242 🩹
ibexa/measurement #133 #134 🔄 #135
ibexa/messenger – #22 #23
ibexa/migrations #438 #439 #440
ibexa/order-management #188 #189 🔄 #190
ibexa/payment #204 #205 🔄 #206
ibexa/product-catalog #1543 #1544 🔄 #1545
ibexa/product-catalog-date-time-attribute – #53 #54
ibexa/product-catalog-symbol-attribute – #17 #18
ibexa/scheduler #168 #169 🔄 #170
ibexa/segmentation #186 #187 🔄 #188
ibexa/share – #190 #191
ibexa/shipping #154 #155 🔄 #156
ibexa/site-context – – #121
ibexa/site-factory #172 #173 🔄 🩹 #174 🩹
ibexa/taxonomy ⚠️ #431 #432 #433
ibexa/translations-management – #174 #175
ibexa/user #128 #129 🔄 #130
ibexa/workflow #191 🩹 #192 🔄 🩹 #193 🩹

🔄 = this branch adds an upgrade migration (renames/FK-retargets existing schema), not just a fresh baseline.
🩹 = this branch also received a backported delta migration, converted from a legacy ibexa/installer upgrade/db/*.sql script (4.6.0 or later) that the original baseline-only migration didn't cover. See IBX-11939 upgrade-scripts backport report or that package's own PR description for details.
⚠️ = fixes a different, related problem (missing SchemaBuilderEvent support for a plain Doctrine ORM entity/table) — not a Doctrine Migrations baseline/upgrade migration like the rest of this table. See that package's own PR description for details.

Description:

Adds a Doctrine Migrations integration for Ibexa-internal migrations, kept entirely separate
from the project's own migrations.

1. An independent, Ibexa-only migration set

IbexaOnlyMigrationsRepository/IbexaOnlyDependencyFactory provide a DependencyFactory that
behaves like the application's own doctrine.migrations.dependency_factory (same Configuration
and logger) except it always runs against the ibexa.persistence.connection service and only
ever sees migrations tagged with IbexaMigrationTag::TAG. RegisterMigrationsPass wires tagged
migration services into this repository at compile time and nothing else — the application's own
doctrine.migrations.dependency_factory is never touched, so Ibexa's migrations never appear in
the project's own doctrine:migrations:* commands.

2. Write a migration

Extend AbstractSqlMigration for per-platform SQL, and implement IbexaMigrationInterface so the
migration is ordered correctly (by target Ibexa version, then creation date) relative to other
Ibexa migrations:

<?php

namespace Ibexa\Bundle\Scheduler\Migration;

use DateTimeImmutable;
use Doctrine\DBAL\Schema\Schema;
use Ibexa\Contracts\DoctrineMigrations\Migrations\AbstractSqlMigration;
use Ibexa\Contracts\DoctrineMigrations\Migrations\IbexaMigrationInterface;
use Ibexa\DoctrineMigrations\Migration\SqlPlatform;

final class InstallSchemaMigration extends AbstractSqlMigration implements IbexaMigrationInterface
{
    public function getDescription(): string
    {
        return 'Creates the ibexa/scheduler database schema';
    }

    public static function getTargetVersion(): string
    {
        return '6.0.0';
    }

    public static function getCreationDate(): DateTimeImmutable
    {
        return new DateTimeImmutable('2026-07-20 00:00:00');
    }

    public function up(Schema $schema): void
    {
        $this->abortIfUnsupportedPlatform(SqlPlatform::MYSQL, SqlPlatform::POSTGRESQL, SqlPlatform::SQLITE);

        if ($schema->hasTable('ibexa_scheduler_scheduled_entry')) {
            return; // already present (e.g. legacy SchemaBuilderEvent install) — nothing to do
        }

        if ($this->isMySQL()) {
            $this->addSqlFile(__DIR__ . '/sql/install-schema-mysql.sql');
        } elseif ($this->isPostgreSQL()) {
            $this->addSqlFile(__DIR__ . '/sql/install-schema-postgresql.sql');
        } elseif ($this->isSqlite()) {
            $this->addSqlFile(__DIR__ . '/sql/install-schema-sqlite.sql');
        }
    }
}

AbstractSqlMigration (extends Doctrine's own AbstractMigration) provides:

  • isMySQL() / isPostgreSQL() / isSqlite() / isPlatform(string $platform) — named checks
    instead of raw $this->platform instanceof ..., backed by SqlPlatform's string constants.
  • addSqlFile(string $file, string $delimiter = self::DEFAULT_SQL_STATEMENT_DELIMITER) — loads
    one or more SQL statements from an external file (separated by a delimiter line) and queues each
    one via addSql(), so a platform's SQL can live in its own .sql file instead of a PHP heredoc.
  • abortIfUnsupportedPlatform(string ...$supportedPlatforms) — aborts the migration with a clear
    message on any platform not listed, instead of silently queuing zero SQL and being recorded as
    successfully executed.

3. Register it

# services.yaml
Ibexa\Bundle\Scheduler\Migration\InstallSchemaMigration:
    autowire: true
    autoconfigure: false
    public: false
    arguments:
        $connection: '@ibexa.persistence.connection'
    tags:
        - { name: !php/const Ibexa\Contracts\DoctrineMigrations\Migrations\IbexaMigrationTag::TAG }

autoconfigure: false matters: if the application enables Doctrine's own
doctrine_migrations.enable_service_migrations option, an autoconfigured AbstractMigration
subclass gets auto-tagged doctrine_migrations.migration by Doctrine's own bundle — leaking it
into the project's own migration view regardless of the tag above.

4. ibexa:doctrine:migrations:* console commands

A full parallel copy of every doctrine:migrations:* command (migrate, status, execute,
generate, latest, up-to-date, diff, dump-schema, rollup, sync-metadata-storage,
version, current, versions), each constructed against IbexaOnlyDependencyFactory instead of
the application's own factory — so e.g. ibexa:doctrine:migrations:status reports only Ibexa's
tagged migrations, while doctrine:migrations:status continues to report only the project's own.
Every doctrine/migrations command class shares the same
DoctrineCommand::__construct(?DependencyFactory, ?string $name) constructor, so no new command
classes were needed — only new service definitions pointing at a different DependencyFactory.

For QA:

Run the existing suites: composer test, composer test-integration, composer phpstan, composer check-cs.

To verify the separation end-to-end in a real app:

bin/console ibexa:doctrine:migrations:status
bin/console doctrine:migrations:status

The first should list Ibexa's tagged migrations (e.g. this package's own test/fixture migrations,
or any real consumer package's migrations if installed); the second should show none of them.

Documentation:

See the usage examples in the Description above. AbstractSqlMigration's class docblock
(src/contracts/Migrations/AbstractSqlMigration.php) is the authoritative reference for its helper
methods.

Steveb-p added 10 commits June 29, 2026 02:40
Introduce a YAML definition format for declaring SQL statements (inline
or from files) with parameters and optional per-database-platform
(mysql/postgresql/sqlite) restrictions, loaded via SqlYamlDefinitionLoader
and executed through YamlSqlFileMigrationTrait. AbstractVersion now uses
this mechanism instead of requiring separate upForMysql()/upForPostgresql()
implementations per migration.
Change the YAML SQL definition structure from a flat list to a mapping
with "up" and "down" keys (each a list of entries), so migrations can
declare their rollback SQL alongside the forward SQL. AbstractVersion
now implements down() via the "down" section, throwing
IrreversibleMigration when a YAML file declares none.
AbstractVersion is the base class migration authors extend, so it
belongs in the public Contracts API rather than the Bundle
implementation namespace. It still uses YamlSqlFileMigrationTrait from
Bundle to load its YAML-declared SQL.
Superseded by the YAML-based AbstractVersion migration mechanism, which
covers per-platform SQL (including MySQL and PostgreSQL) via the
"platforms" key on individual YAML entries instead of a dedicated base
class per engine.
…able

The trait belongs alongside AbstractVersion in the public Contracts API
rather than the Bundle implementation namespace. AbstractVersion's
up()/down() are no longer final, so subclasses can extend the
YAML-declared SQL with additional PHP logic if needed.
@Steveb-p Steveb-p changed the title Doctrine migrations Doctrine migrations (6.0) Jul 16, 2026
@Steveb-p
Steveb-p marked this pull request as ready for review July 16, 2026 08:07
Steveb-p added 3 commits July 16, 2026 12:51
…ations

Registers a second, private ServiceMigrationsRepository service
(IbexaOnlyMigrationsRepository::SERVICE_ID) that never decorates an
inner MigrationsRepository, wired with the same tagged migrations as
the default combined one. Consumers that need strictly Ibexa-internal
migrations (e.g. installing Ibexa DXP itself) can reference this
service by ID instead of the default one, without affecting the
default repository's behavior.
Registers ibexa.doctrine_migrations.dependency_factory.ibexa_migrations_only
(IbexaOnlyDependencyFactory::SERVICE_ID), a private DependencyFactory
built entirely via inline service definitions in services.php as an
independent copy of the application's own
"doctrine.migrations.dependency_factory" — same Configuration and
logger, fetched via inline factory calls — but always running against
the "ibexa.persistence.connection" service (created by ibexa/core, so
assumed present) and with its MigrationsRepository always the
Ibexa-only one. Every other dependency (metadata storage, migrator,
plan calculator, ...) is freshly built from those and shared with
nothing else, so replacing it never mutates or is affected by the
shared application DependencyFactory. Being unfrozen, further
dependencies can still be swapped via setService()/setDefinition()
before first use.

RegisterMigrationsPass resolves the two abstract_arg placeholders
(standing in for the application's own DependencyFactory) nested
inside the inline Configuration/logger definitions, only when
"doctrine.migrations.dependency_factory" is actually present.
The class_exists(newName) ? newName : oldName order made PHPStan
(as of phpstan/phpstan 2.2.5) unable to verify the "new name" branch
as class-string<AbstractPlatform> whenever only the old-named class is
actually installed (e.g. DBAL 2.13.x, which ibexa/core's own
dependency constraints currently force in this repo). Checking the
class that's actually installed first resolves it, with no behavior
change: either order picks the same, correct class for the currently
installed DBAL version.
@Steveb-p Steveb-p changed the title Doctrine migrations (6.0) IBX-11939: Doctrine migrations (6.0) Jul 20, 2026
Without this, the install-time TaggedMigrationsRunner used the default
alphabetical-by-class comparator instead of ordering migrations by target
version and creation date, causing cross-package migrations to run out of
order during a full ibexa:install (e.g. a package's baseline running before
core's tables it depends on existed).
Steveb-p and others added 23 commits July 24, 2026 14:44
- Updated CI runner OS from ubuntu-24.04 to ubuntu-26.04 in backend-ci.yaml.
- Throw LogicException in RegisterMigrationsPass when doctrine.migrations.dependency_factory is missing.
- Added @phpstan-assert-if-true type assertion helper in IbexaMigrationComparator to eliminate inline @var annotations.
# Conflicts:
#	composer.json
#	phpunit.xml.dist
#	src/bundle/DependencyInjection/IbexaDoctrineMigrationsExtension.php
#	src/bundle/IbexaDoctrineMigrationsBundle.php
Introduce a YAML definition format for declaring SQL statements (inline
or from files) with parameters and optional per-database-platform
(mysql/postgresql/sqlite) restrictions, loaded via SqlYamlDefinitionLoader
and executed through YamlSqlFileMigrationTrait. AbstractVersion now uses
this mechanism instead of requiring separate upForMysql()/upForPostgresql()
implementations per migration.
Change the YAML SQL definition structure from a flat list to a mapping
with "up" and "down" keys (each a list of entries), so migrations can
declare their rollback SQL alongside the forward SQL. AbstractVersion
now implements down() via the "down" section, throwing
IrreversibleMigration when a YAML file declares none.
AbstractVersion is the base class migration authors extend, so it
belongs in the public Contracts API rather than the Bundle
implementation namespace. It still uses YamlSqlFileMigrationTrait from
Bundle to load its YAML-declared SQL.
Superseded by the YAML-based AbstractVersion migration mechanism, which
covers per-platform SQL (including MySQL and PostgreSQL) via the
"platforms" key on individual YAML entries instead of a dedicated base
class per engine.
…able

The trait belongs alongside AbstractVersion in the public Contracts API
rather than the Bundle implementation namespace. AbstractVersion's
up()/down() are no longer final, so subclasses can extend the
YAML-declared SQL with additional PHP logic if needed.
…helpers and addSqlFile()

Adds isMySQL()/isPostgreSQL()/isSqlite()/isPlatform() and addSqlFile() to a new
AbstractSqlMigration base class, so per-platform SQL can live in separate .sql files
instead of interleaved instanceof-branches in one PHP method.

Drops the unused YAML-driven migration mechanism (AbstractVersion,
YamlSqlFileMigrationTrait, SqlYamlDefinition*) that nothing in the ecosystem consumed yet,
and renames/relocates SqlYamlPlatform to SqlPlatform now that it is no longer YAML-specific.
- Updated CI runner OS from ubuntu-24.04 to ubuntu-26.04 in backend-ci.yaml.
- Throw LogicException in RegisterMigrationsPass when doctrine.migrations.dependency_factory is missing.
- Added @phpstan-assert-if-true type assertion helper in IbexaMigrationComparator to eliminate inline @var annotations.
…pers

getMysqlPlatformClass()/getPostgresqlPlatformClass()/getSqlitePlatformClass() used a
class_exists() ternary to pick between DBAL 2 and DBAL 3 platform class names, but this
package requires doctrine/dbal ^3.9 on this branch, so the DBAL 2 branch is unreachable.
Return the DBAL 3 class directly via ::class so PHPStan can verify the declared
class-string<AbstractPlatform> return type statically.
# Conflicts:
#	composer.json
#	src/bundle/IbexaDoctrineMigrationsBundle.php
Introduce a YAML definition format for declaring SQL statements (inline
or from files) with parameters and optional per-database-platform
(mysql/postgresql/sqlite) restrictions, loaded via SqlYamlDefinitionLoader
and executed through YamlSqlFileMigrationTrait. AbstractVersion now uses
this mechanism instead of requiring separate upForMysql()/upForPostgresql()
implementations per migration.
Change the YAML SQL definition structure from a flat list to a mapping
with "up" and "down" keys (each a list of entries), so migrations can
declare their rollback SQL alongside the forward SQL. AbstractVersion
now implements down() via the "down" section, throwing
IrreversibleMigration when a YAML file declares none.
AbstractVersion is the base class migration authors extend, so it
belongs in the public Contracts API rather than the Bundle
implementation namespace. It still uses YamlSqlFileMigrationTrait from
Bundle to load its YAML-declared SQL.
Superseded by the YAML-based AbstractVersion migration mechanism, which
covers per-platform SQL (including MySQL and PostgreSQL) via the
"platforms" key on individual YAML entries instead of a dedicated base
class per engine.
…able

The trait belongs alongside AbstractVersion in the public Contracts API
rather than the Bundle implementation namespace. AbstractVersion's
up()/down() are no longer final, so subclasses can extend the
YAML-declared SQL with additional PHP logic if needed.
…helpers and addSqlFile()

Adds isMySQL()/isPostgreSQL()/isSqlite()/isPlatform() and addSqlFile() to a new
AbstractSqlMigration base class, so per-platform SQL can live in separate .sql files
instead of interleaved instanceof-branches in one PHP method.

Drops the unused YAML-driven migration mechanism (AbstractVersion,
YamlSqlFileMigrationTrait, SqlYamlDefinition*) that nothing in the ecosystem consumed yet,
and renames/relocates SqlYamlPlatform to SqlPlatform now that it is no longer YAML-specific.
- Updated CI runner OS from ubuntu-24.04 to ubuntu-26.04 in backend-ci.yaml.
- Throw LogicException in RegisterMigrationsPass when doctrine.migrations.dependency_factory is missing.
- Added @phpstan-assert-if-true type assertion helper in IbexaMigrationComparator to eliminate inline @var annotations.
Several rebased commits' own diffs against backend-ci.yaml re-introduced the
4.6-specific ubuntu-26.04 runner version during auto-merge (no conflict was
raised since the context lines matched). Restored ubuntu-24.04, matching plain
6.0's own CI config.
@Steveb-p
Steveb-p force-pushed the feat/doctrine-migrations-6.0 branch from 7c6173b to 279292c Compare July 24, 2026 15:55
@Steveb-p
Steveb-p changed the base branch from 6.0 to base/ibx-11939-4.6-5.0-merged-6.0 July 24, 2026 15:55
Steveb-p added 2 commits July 27, 2026 13:27
Propagated from feat/doctrine-migrations-5.0. 5.0/6.0 require PHP
>=8.3, so SqlPlatform's string constants become a backed enum instead.
Every consumer package only ever references
SqlPlatform::MYSQL/POSTGRESQL/SQLITE as opaque tokens (never as raw
strings), so this is a purely internal type change - confirmed against
ibexa/scheduler via a temporary path-repository pin, whose own
phpstan and test suite both pass unchanged against this branch.

AbstractSqlMigration::isPlatform()/abortIfUnsupportedPlatform() and
DatabasePlatformResolver::resolve() retyped from string to SqlPlatform;
abortIfUnsupportedPlatform()'s error message now maps through
SqlPlatform::value since implode() can't stringify enum cases
directly. 4.6 keeps the plain string-constant class (PHP 7.4 doesn't
support enums).
@Steveb-p
Steveb-p changed the base branch from base/ibx-11939-4.6-5.0-merged-6.0 to 6.0 July 28, 2026 12:10
@Steveb-p
Steveb-p merged commit b8e92b8 into 6.0 Jul 28, 2026
9 checks passed
@Steveb-p
Steveb-p deleted the feat/doctrine-migrations-6.0 branch July 28, 2026 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant