Running Migrations
Run pending migrations safely with preflight checks, advisory locking, dry runs, and production flags.
Running Migrations
Run all pending migrations:
php vortos migrateProduction deployment command:
php vortos migrate --force --no-interactionPreview without executing:
php vortos migrate --dry-run --forcePreflight
Before executing SQL, Vortos checks pending module migrations against the live schema.
If a pending migration owns objects that already exist, migration stops:
Schema drift detected. No migrations were executed.
→ App\Migrations\Version20260505114121 (Authorization/001_authorization_rbac.php)
Drift: compatible existing schema
existing table: role_permissions
existing table: user_roles
If the existing schema is correct, run:
php vortos migrate:adopt VERSION --verifyThis prevents duplicate table errors and avoids hiding drift behind IF NOT EXISTS.
Post-Deploy Verification
After deployment, run migrate:verify to confirm the live schema matches what the migrations say it should:
php vortos migrate:verify ✔ App\Migrations\Version20260505114121
✔ App\Migrations\Version20260507083200
All 2 executed migration(s) verified — schema is clean.Exit code 0 means every executed framework migration's tables and columns are present. Exit code 1 means drift — the output lists exactly what is missing:
✘ App\Migrations\Version20260505114121 [partial]
missing table: role_permissionsAdd --json for machine-readable output in CI pipelines. Only framework module migrations are verified — user-authored migrations are skipped.
PostgreSQL Schema Bootstrap
When your DATABASE_URL uses a pgsql:// or postgres:// scheme, vortos:migrate automatically runs:
CREATE SCHEMA IF NOT EXISTS vortosbefore executing any migrations. You do not need to create the schema manually or add it to your deployment scripts.
vortos:migrate:fresh drops the vortos schema completely (including all its tables) before re-running all migrations from scratch. This gives a guaranteed clean slate in development and CI.
Opting out
Add ?vortos_prefix=true to your DSN to use underscore prefix mode (vortos_table_name) instead of schema mode. No schema is created in prefix mode. See the DBAL docs for details.
Locking
On PostgreSQL, vortos:migrate uses an advisory lock so two deployment runners cannot migrate at the same time.
php vortos migrate --lock-timeout=60If the lock cannot be acquired:
Another migration process is already running.--no-lock exists for unusual local/debug workflows. Do not use it in production.
ORM and Framework Migrations Together
vortos:migrate runs all pending migrations in a single command, regardless of their source:
- Framework module tables come from SQL stubs published via
vortos:migrate:publish— the generated classes now carry a#[DeployPhase(MigrationPhase::Expand)]attribute (orContractwhen the SQL is destructive), so they passvortos:migrate:analyzeout of the box - ORM entity tables come from diffs generated via
vortos:orm:diff
Both land in the same migrations/ directory and are run together by vortos:migrate. There is no separate ORM migration runner — one command covers everything.
Standard workflow when using Doctrine ORM:
# 1. Publish any new framework module migrations
php vortos migrate:publish
# 2. Generate an ORM diff for entity changes
php vortos vortos:orm:diff
# 3. Run everything — both framework tables and ORM entity tables
php vortos migrateReview the generated migration file in migrations/ before running step 3. The diff command generates plain SQL — it does not execute anything.
One migrations/ directory
Framework table migrations and ORM entity migrations are indistinguishable to vortos:migrate — it runs them in timestamp order. Keep generated migration files in version control alongside hand-written ones.
Creating Migrations
vortos:migrate:make
Generates an empty Doctrine migration class ready to be filled in:
php bin/console vortos:migrate:make CreateOrdersTableCreates migrations/VersionYYYYMMDDHHIISS.php with empty up() and down() stubs.
--aggregate flag
For DBAL aggregate tables, pass --aggregate with the table name. The migration is pre-filled with id and lock_version — the two columns every aggregate table requires:
php bin/console vortos:migrate:make orders --aggregateGenerated up():
CREATE TABLE IF NOT EXISTS orders (
id VARCHAR(36) NOT NULL,
lock_version INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (id)
)Add your domain columns below lock_version, then run vortos:migrate.
DBAL only
The --aggregate flag is for DBAL-backed aggregates only. ORM users generate migrations via vortos:orm:diff — Doctrine reads lock_version from the #[ORM\Version] annotation automatically.