Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .github/workflows/doctrine.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,41 @@
### Runs pending Doctrine migrations and validates the resulting schema
### against the entity metadata.
###
### #### MariaDB leg
###
### The supported, deployment-target leg. CI matrix runs the full
### migrations:migrate + schema:validate cycle against MariaDB 10.11 and
### 11.4. A green run here means upgrades will work in production.
###
### #### Postgres leg (EXPERIMENTAL — schema-portability gate only)
###
### NOT a claim that this project runs on Postgres. Postgres is NOT a
### deployment target, NOT exercised by PHPUnit, and NOT covered by the
### rest of CI. The leg exists only to catch regressions in the narrow
### slice of portability we have so far achieved:
###
### - Entity metadata (column types, index names, quoted identifiers)
### stays platform-neutral.
### - The consolidated migration's up() emits portable DDL via Schema
### tool API.
###
### Known gaps that still block real Postgres deployment:
###
### - Native MariaDB SQL in entity listeners (`MultiTenantRepositoryTrait`,
### `RelationsChecksumCalculator`) — Postgres would fail at runtime.
### - Doctrine cache, fixture loading, and most application queries are
### untested against Postgres.
###
### A failure on this leg is a blocker — new schema changes must stay
### portable; widening the MariaDB-only surface requires explicit review.
###
### #### Assumptions
###
### 1. A docker compose service named `phpfpm` can be run and `bin/console`
### can be run inside the `phpfpm` service.
### 2. A `mariadb` service is reachable from `phpfpm`.
### 3. `docker-compose.postgres.yml` adds a `postgres` service and layers
### `pdo_pgsql` onto `phpfpm`, used by the Postgres leg below.

on: pull_request

Expand Down Expand Up @@ -55,3 +85,45 @@ jobs:

- name: Validate Doctrine schema
run: docker compose run --rm -e APP_ENV=prod -e MARIADB_VERSION phpfpm bin/console doctrine:schema:validate

# EXPERIMENTAL: schema-portability regression gate only. Postgres is NOT
# a supported deployment target — see the header comment at the top of
# this file for the full caveat. A green run means entity metadata and
# the consolidated migration's up() are portable; it does NOT mean the
# application runs on Postgres.
validate-doctrine-schema-postgres:
runs-on: ubuntu-latest
name: Validate Schema (postgres:16, experimental)
env:
# docker-compose.postgres.yml adds the postgres service and layers
# pdo_pgsql onto phpfpm. COMPOSE_FILE stacks it on top of the default.
COMPOSE_FILE: docker-compose.yml:docker-compose.postgres.yml
DATABASE_URL: "postgresql://db:db@postgres:5432/db?serverVersion=16&charset=utf8"
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Cache vendor (prod)
uses: actions/cache@v5
with:
path: vendor
key: vendor-prod-php8.4-${{ hashFiles('composer.lock') }}
restore-keys: vendor-prod-php8.4-

- name: Setup network
run: docker network create frontend

# Build the phpfpm-with-pdo_pgsql image declared in
# docker-compose.postgres.yml; without this, `docker compose run` reuses
# the upstream itkdev/php8.4-fpm:latest image which lacks pdo_pgsql.
- name: Build phpfpm with pdo_pgsql
run: docker compose build phpfpm

- name: "[prod] Composer install"
run: docker compose run --rm -e APP_ENV=prod phpfpm composer install --no-dev -o

- name: Run Doctrine Migrations
run: docker compose run --rm -e APP_ENV=prod -e DATABASE_URL phpfpm bin/console doctrine:migrations:migrate --no-interaction

- name: Validate Doctrine schema
run: docker compose run --rm -e APP_ENV=prod -e DATABASE_URL phpfpm bin/console doctrine:schema:validate
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

- Rewrote the consolidated end-of-2.8 migration to Doctrine's Schema tool API;
added a `NoAddSqlInMigrationRule` PHPStan rule to enforce the convention on future migrations.
- Added a Postgres `Validate Schema` job to the Doctrine workflow as a regression gate against
entity/migration drift from Postgres compatibility; uses the new `docker-compose.postgres.yml` override.
- Consolidated 25 historical 2.x Doctrine migrations into a single end-of-2.8 schema migration;
upgraders run `doctrine:migrations:rollup` (see `UPGRADE.md` step 3).
- Restored three deprecated `Template` properties (`icon`, `resources`, `description`) as
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
},
"autoload-dev": {
"psr-4": {
"App\\PhpStan\\": "tools/phpstan/",
"App\\Tests\\": "tests/"
}
},
Expand Down
67 changes: 67 additions & 0 deletions docker-compose.postgres.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
### Postgres compose override — EXPERIMENTAL
#
# NOT a supported runtime configuration. Postgres is not a deployment
# target for this project. This file exists to drive the
# schema-portability regression gate in .github/workflows/doctrine.yaml
# and to let developers reproduce that gate locally.
#
# What works:
# - The consolidated end-of-2.8 migration's up() emits portable DDL via
# Doctrine's Schema tool API and applies cleanly on Postgres 16.
# - `doctrine:schema:validate` reports the resulting schema in sync
# with the entity metadata.
#
# What does NOT work (running the app on Postgres will fail):
# - Native MariaDB SQL in `MultiTenantRepositoryTrait` and
# `RelationsChecksumCalculator` — runtime queries break on Postgres.
# - PHPUnit, fixtures, Doctrine cache and most application queries are
# untested against Postgres.
#
# Treat this file as test-infrastructure, not a deployment artefact.
#
# Local equivalent of the CI gate:
#
# COMPOSE_FILE=docker-compose.yml:docker-compose.postgres.yml \
# docker compose up -d postgres
# COMPOSE_FILE=docker-compose.yml:docker-compose.postgres.yml \
# DATABASE_URL=postgresql://db:db@postgres:5432/db?serverVersion=16 \
# docker compose run --rm -e DATABASE_URL -e APP_ENV=prod \
# phpfpm bin/console doctrine:migrations:migrate --no-interaction
#
# pdo_pgsql isn't in the base phpfpm image, so this override layers it on
# via dockerfile_inline (Compose v2.27+).
services:
postgres:
image: postgres:16-alpine
networks:
- app
ports:
- "5432"
environment:
- POSTGRES_USER=db
- POSTGRES_PASSWORD=db
- POSTGRES_DB=db
healthcheck:
test: ["CMD-SHELL", "pg_isready -U db -d db"]
interval: 5s
timeout: 3s
retries: 10
start_period: 5s

phpfpm:
# Unique tag so this override doesn't share an entry with the upstream
# `itkdev/php8.4-fpm:latest` from docker-compose.yml — without this,
# `docker compose run` may reuse the upstream image without pdo_pgsql.
image: display-phpfpm-postgres:local
build:
context: .
dockerfile_inline: |
FROM itkdev/php8.4-fpm:latest
USER root
RUN apt-get update -qq && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends php8.4-pgsql && \
rm -rf /var/lib/apt/lists/*
USER deploy
depends_on:
postgres:
condition: service_healthy
63 changes: 63 additions & 0 deletions docs/adr/010-schema-tool-migrations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# ADR 010 - Schema tool migrations, no native SQL for DDL

Date: 27-05-2026

## Status

Proposed

## Context

The consolidated end-of-2.8 migration was rewritten from raw `addSql` strings to
Doctrine's Schema tool API (`$schema->createTable()`, `$table->addColumn()`,
`$table->addForeignKeyConstraint()`, …). The same change replaced free-form
type-name strings (`'string'`, `'datetime_immutable'`, …) with `Doctrine\DBAL\Types\Types::*`
constants.

This unblocks the schema-portability regression gate in
`.github/workflows/doctrine.yaml`: the consolidated migration now applies cleanly
on Postgres 16 from the same code path that runs on MariaDB.

Postgres is still **not** a deployment target. Native MariaDB SQL remains in
entity listeners (`MultiTenantRepositoryTrait`, `RelationsChecksumCalculator`),
and runtime queries are untested against Postgres. The conversion is scoped to
migrations for now — but if migrations regress on portability, the schema-level
work is wasted and the gate stops being meaningful.

## Decision

All future migrations must go through Doctrine's Schema tool API. Raw
`$this->addSql(...)` calls inside `migrations/` are disallowed for DDL.

Enforcement:

- `App\PhpStan\NoAddSqlInMigrationRule` (project-local PHPStan rule) fails CI on
any `addSql` call in `migrations/`.
- The `Validate Schema (postgres:16, experimental)` job in
`.github/workflows/doctrine.yaml` runs `doctrine:migrations:migrate` against
Postgres 16 and fails the build on platform-specific DDL.

Type references in migrations must use the `Doctrine\DBAL\Types\Types::*`
constants (plus `UlidType::NAME`, `RRuleType::RRULE` for the two custom types in
this project) rather than free-form strings, so type renames propagate via the
type system.

One existing exception is pre-existing technical debt called out explicitly:

- Entity listeners and a small set of runtime repository methods still execute
native MariaDB SQL. Out of scope for this ADR; tracked separately.

## Consequences

- Schema-level migration code stays portable to any database Doctrine supports.
Real Postgres deployment still requires resolving the runtime-SQL debt above.
- Adding a new migration requires using the Schema tool API. Common operations
(CREATE TABLE, ALTER TABLE, indexes, foreign keys) have direct equivalents.
Genuine MariaDB-specific changes that the Schema tool can't express need an
explicit ADR/review to widen the MariaDB-only surface — not a silent
`addSql`.
- The Postgres CI job is treated as a portability regression gate, not a
full runtime test. A green run means migrations and entity metadata are
portable; it does not mean the application works on Postgres.
- Renames of Doctrine type names propagate to migrations via the type system
rather than requiring grep-and-replace across migration files.
Loading
Loading