Skip to content

Rewrite migrations to Doctrine Schema tool + PHPStan enforcement#442

Merged
turegjorup merged 11 commits into
release/3.0.0from
feature/migrations-schema-tool
May 27, 2026
Merged

Rewrite migrations to Doctrine Schema tool + PHPStan enforcement#442
turegjorup merged 11 commits into
release/3.0.0from
feature/migrations-schema-tool

Conversation

@turegjorup

@turegjorup turegjorup commented May 6, 2026

Copy link
Copy Markdown
Contributor

Note

Stacked on #441 (now merged). Targets release/3.0.0.

Why now

A major version is the natural cut-point for changing migration conventions, and the consolidated 3.0 migration is the entire current migration population — so it's also a one-time chance to rewrite everything in one place.

Concretely, migrations now go through Doctrine's Schema tool API ($schema->createTable(), $table->addColumn(), $table->addForeignKeyConstraint(), …) instead of raw addSql(...) strings. The emitted DDL is identical for MariaDB but the migration is now generated through the platform abstraction, so the same code produces native DDL on any database Doctrine supports.

This PR lays the groundwork for portability — it does not claim it. Runtime is still MariaDB-only and native SQL in entity listeners is deferred. CI now exercises Postgres as a schema-portability regression gate only (see below); it does not validate that the application runs on Postgres.

Summary

  • Rewrites Version20260506215847.php from 154 raw addSql calls to the equivalent Schema tool builders. A small applyTableOptions() helper at the bottom collapses the engine/charset/collation triplet that would otherwise repeat 29 times.
  • Replaces every type-name string literal ('string', 'datetime_immutable', …) with the corresponding Doctrine\DBAL\Types\Types::* constant — plus UlidType::NAME and RRuleType::RRULE for the two custom types — so renames propagate via the type system rather than across free-form strings.
  • Adds App\PhpStan\NoAddSqlInMigrationRule, a project-local PHPStan rule that flags any $this->addSql(...) call inside migrations/. PHPStan path coverage extended to migrations/.
  • Wires the rule via tools/phpstan/ + composer autoload-dev so it's available at lint time but never loaded into the runtime container.
  • Picks up Consolidate 25 historical 2.x migrations into one end-of-2.8 schema #441/feat: prep db identifiers for cross-platform (Postgres) portability #444 into the resolved migration: 15 <table>_changed_idx index names and the backtick-quoted user table, applied to the Schema-tool body without re-introducing raw SQL.
  • Adds a Postgres schema-portability CI gate (Validate Schema (postgres:16, experimental) in .github/workflows/doctrine.yaml) that runs the consolidated migration against Postgres 16 and validates the resulting schema. The new docker-compose.postgres.yml override declares the postgres service and layers pdo_pgsql onto phpfpm via dockerfile_inline, so the same setup works locally.
  • ADR 010 documents the convention (Schema tool API, no addSql for DDL) and the rule going forward: future schema changes must keep the Postgres gate green.
  • CHANGELOG entries under [Unreleased].

On Rector

No off-the-shelf Rector rule covers either conversion: Rector\Transform\Rector\Attribute\AttributeKeyToClassConstFetchRector handles #[Column(type: 'string')] on entity attributes but not method-call arguments, and there's no rule for addSql → Schema tool. The string-to-constant conversion in this PR was scripted with a small Python regex pass and verified by re-running migrate + schema:validate. A custom Rector rule was considered overkill for a single migration; the PHPStan rule prevents the worse problem (raw addSql) on future migrations.

Test plan

  • Fresh install end-to-end on MariaDB 11.4: drop test DB → create → migrations:migrateschema:validate exits 0
  • 2.x → 3.0 upgrade simulation: applied 25 latest-2.8.x migrations to fresh DB → swap files → migrations:rollupschema:validate exits 0
  • Postgres 16 portability gate: migrations:migrate applies the consolidated Schema-tool migration cleanly (264 queries) → schema:validate in sync, no MariaDB-specific DDL leaked
  • mysqldump --no-data diff vs the previous raw-SQL version on MariaDB: only column reordering and index reordering (Schema tool emits columns in addColumn() insertion order). No functional differences. The single FK name divergence on interactive_slide_config (FK_138E544D9033212A vs FK_D30060259033212A) is a pre-existing artifact from the table rename that was already present in Consolidate 25 historical 2.x migrations into one end-of-2.8 schema #441 and didn't break CI there.
  • Full PHPUnit suite: 155 tests / 637 assertions green (post-release/3.0.0 sync brought in 6 new InstantBook tests)
  • PHPStan green; rule verified to fire on a smoke-test migration with addSql('SELECT 1') (then deleted)
  • CI green on this PR (incl. the new Postgres job)

Out of scope (deferred)

  • Native SQL in entity listeners (MultiTenantRepositoryTrait, RelationsChecksumCalculator) — same conversion needs doing there too, but this PR is scoped to migrations.
  • Running PHPUnit / fixtures / runtime queries against Postgres — the CI gate is schema-level only.
  • Switching the deployment target away from MariaDB.

🤖 Generated with Claude Code

turegjorup and others added 4 commits May 7, 2026 00:48
Replaces every raw `$this->addSql(...)` call in the consolidated
end-of-2.7 migration with the equivalent Schema tool builder API
(`$schema->createTable()`, `$table->addColumn()`,
`$table->addForeignKeyConstraint()`, `$schema->dropTable()`). The
emitted DDL is identical for MariaDB but is now generated through
Doctrine's platform abstraction, so the same migration produces
platform-native DDL on any database Doctrine supports.

Verified end-to-end on MariaDB:
- Fresh install: drop → migrate → schema:validate exits 0
- Upgrade from 2.7: 25 latest-2.x applied → swap files → rollup →
  schema:validate exits 0
- mysqldump diff against the previous raw-SQL version: only column
  ordering and index ordering changes (the Schema tool emits columns
  in addColumn() insertion order). No functional differences.
- Full PHPUnit suite green (143 tests / 607 assertions).

A small `applyTableOptions()` helper at the bottom of the class
collapses the engine/charset/collation triplet that would otherwise
repeat 29 times. Other Doctrine-supported platforms ignore these
MariaDB-specific options.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `App\PhpStan\NoAddSqlInMigrationRule`, a project-local PHPStan
rule that flags any `$this->addSql(...)` call in files under
`migrations/`. Future migrations are forced through Doctrine's Schema
tool API (`$schema->createTable()`, `$table->addColumn()`, …),
keeping the consolidated 3.0 migration's portability property as the
project evolves.

Wiring:
- Rule lives in `tools/phpstan/`, mapped via composer's autoload-dev
  to `App\PhpStan\` so it's available at lint time but never loaded
  into the runtime container (Symfony's `App\:` resource scan only
  covers `src/`).
- `phpstan.dist.neon` registers the rule and adds `migrations/` to
  the analysed paths.

Native SQL in entity listeners is intentionally out of scope here;
that conversion is deferred. The rule's path-based scoping means it
only enforces the convention for migrations, not the rest of the
codebase.

Verified the rule fires by adding a temporary `_RuleSmokeTest.php`
migration with a single `addSql('SELECT 1')` call — PHPStan reported
the expected `migrations.noAddSql` error and exited non-zero.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the convention shift under [Unreleased]: future migrations
must use Doctrine's Schema tool API; the consolidated 3.0 migration
already does, and the new PHPStan rule enforces it for everything that
follows. Calls out that this lays the groundwork for portability without
claiming it — runtime is still MariaDB-only and CI does not yet cover
other platforms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the type-name string literals passed to `$table->addColumn()`
with the matching `Doctrine\DBAL\Types\Types::*` constants — and
`UlidType::NAME` / `RRuleType::RRULE` for the two custom types in this
codebase. Refactor-safe (renames at the type-class level propagate via
IDE rename), and signals at a glance that these are Doctrine type
identifiers rather than free-form strings.

No Rector rule covers this conversion for `addColumn()` directly —
`Rector\Transform\Rector\Attribute\AttributeKeyToClassConstFetchRector`
handles `#[Column(type: 'string')]` on entity attributes but not
method-call arguments. A custom Rector rule would be overkill for one
migration; conversion was scripted via a small Python regex pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@turegjorup
turegjorup requested a review from tuj May 6, 2026 22:57
@turegjorup turegjorup self-assigned this May 6, 2026
@turegjorup turegjorup changed the title feat: rewrite migrations to Doctrine Schema tool + PHPStan enforcement WIP: rewrite migrations to Doctrine Schema tool + PHPStan enforcement May 6, 2026
turegjorup and others added 2 commits May 27, 2026 11:43
… entries

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@turegjorup
turegjorup marked this pull request as ready for review May 27, 2026 09:45
Base automatically changed from feature/consolidate-migrations to release/3.0.0 May 27, 2026 11:15
@turegjorup turegjorup changed the title WIP: rewrite migrations to Doctrine Schema tool + PHPStan enforcement Rewrite migrations to Doctrine Schema tool + PHPStan enforcement May 27, 2026
turegjorup and others added 5 commits May 27, 2026 13:31
…ations-schema-tool

# Conflicts:
#	CHANGELOG.md
#	migrations/Version20260506215847.php
Adds a second validate-doctrine-schema job that runs the consolidated
Schema-tool migration and doctrine:schema:validate against a
postgres:16 service. Treated as a blocker for new schema changes —
widening the MariaDB-only surface (entity listeners, raw down() SQL,
etc. — all pre-existing) requires explicit review.

The base phpfpm image ships without pdo_pgsql, so the new
docker-compose.postgres.yml override layers it on via dockerfile_inline
in addition to declaring the postgres service. Both files carry
EXPERIMENTAL caveats: the gate guards entity/migration portability,
not a runtime claim that the app works on Postgres.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous attempt fell through to the upstream itkdev/php8.4-fpm:latest
image (no pdo_pgsql) because `docker compose run` doesn't auto-build
when an `image:` is already declared upstream. Give the override its
own image tag (display-phpfpm-postgres:local) and add an explicit
`docker compose build phpfpm` step before the run commands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The consolidated migration's down() uses $schema->dropTable() — fully
Schema tool API like up(). Removed the bullet from ADR 010, the
doctrine.yaml header, and docker-compose.postgres.yml that incorrectly
listed it as a known portability gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@turegjorup
turegjorup merged commit 8566816 into release/3.0.0 May 27, 2026
22 checks passed
@turegjorup
turegjorup deleted the feature/migrations-schema-tool branch May 27, 2026 14:53
tuj pushed a commit to itk-dev/os2display-api-service that referenced this pull request Jun 2, 2026
- Rename 15 `changed_idx` indexes to `<table>_changed_idx` (Postgres
  scopes index names schema-wide; MariaDB scopes per-table — the shared
  name collides on Postgres).
- Quote `user` table identifier in entity metadata
  (`#[ORM\Table(name: '`user`')]`). Doctrine emits the platform-native
  quote on every reference (`` `user` `` on MariaDB, `"user"` on
  Postgres), so no table rename is needed.
- Add `validate-doctrine-schema-postgres` CI job that applies entity
  metadata to a Postgres 16 service container via
  `doctrine:schema:update --force --complete` and then runs
  `doctrine:schema:validate`. Gates against future entity-level
  Postgres regressions.

Lands on 2.7 so the consolidated 3.0 migration (os2display#441 / os2display#442) can emit
the portable shape from the start, keeping `migrations:rollup` honest
for 2.x → 3.0 upgraders.

Verified locally on MariaDB: migration applies cleanly,
`doctrine:schema:validate` in sync, full PHPUnit suite green
(133 tests / 516 assertions, identical to pristine release/2.7.0).
Verified on Postgres 16: `doctrine:schema:update --force --complete`
applies the metadata in 265 queries, `doctrine:schema:validate` in
sync. down/up cycle of the new migration also verified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants