fervo/release-phase-migrations-bundle
A Symfony bundle for Doctrine migrations that adds release phases, letting you group schema and data changes and run them in a controlled order during deployment. Tag migrations by phase, run the right set per release, and keep deployments predictable across environments.
Read this first: it is not a Laravel package. fervo/release-phase-migrations-bundle is a Symfony bundle that wraps doctrine:migrations:migrate in a database advisory lock, so that several app instances booting at once (Heroku release phase, Kubernetes rollout, ECS task replacement) cannot run migrations concurrently. There is no service provider, no config/*.php entry, and no Artisan command. If you are working in a Laravel codebase you have two realistic options:
Port the idea (usually the right call for a single-purpose, low-traffic bundle). In Laravel the equivalent is roughly:
// in a deploy script or a dedicated Artisan command
DB::transaction(function () {
DB::statement('SELECT pg_advisory_lock(?)', [918273645]);
try {
Artisan::call('migrate', ['--force' => true]);
} finally {
DB::statement('SELECT pg_advisory_unlock(?)', [918273645]);
}
});
Or use Cache::lock('migrations', 300)->block(120, fn () => Artisan::call('migrate', ['--force' => true])) if you already have a shared cache store.
Vendor the bundle's source into a Symfony service if you are in a mixed or Symfony-adjacent project.
If you are genuinely in Symfony:
composer require fervo/release-phase-migrations-bundle
Register it in config/bundles.php, point it at the Doctrine connection you want the lock taken on, and replace your release-phase command (doctrine:migrations:migrate) with the bundle's lock-aware command.
Where to look first, in this order:
Configuration.php) — it tells you which connection/key/timeout knobs actually exist.pg_advisory_lock, GET_LOCK, or a table row. This determines every caveat below.First use case: a Heroku release process type or a Kubernetes pre-install/pre-upgrade hook, where N pods start simultaneously and all of them want to migrate. The lock serialises them; the losers block (or fail fast) instead of racing.
Single migrating instance, everyone else waits. The canonical pattern is: every deploy job invokes the lock-aware migrate command, but only one acquires the lock. The rest block until it releases, then re-check the migration state and no-op. Design your timeout so that waiting is measured in seconds, not the length of a long migration.
Pair it with an idempotent release script. The lock only protects the migration step. Cache warmup, asset compilation, and config caching should either run after the migrate command in the same serialised block, or be provably safe to run concurrently. A common arrangement:
# release phase
bin/console cache:clear
bin/console fervo:migrations:migrate # lock-aware
bin/console cache:warmup
Use it as a hard gate in CI/CD, not as a safety net. Keep your normal single-job migration step in CI and treat the advisory lock as protection for the "many identical containers start at once" case. Relying on the lock to fix a genuinely concurrent-migration problem hides the underlying issue.
Multiple databases / connections. The lock is taken on a specific connection. If you migrate several entity managers or a tenant sharded database, you need one lock per logical target, and the lock must be on the same database where the migration runs. Don't take the lock on a "main" connection and migrate tenants through separate ones — you will get no mutual exclusion at all.
Separate the lock from the transaction. Advisory locks and DDL do not always compose well. Prefer session-level advisory lock → run migrations → release, rather than trying to make the lock part of the migration transaction. If migrations themselves use transactions (Doctrine's default), you get nested/implicit behaviour you did not intend.
Laravel translation of the same pattern — if you are porting, keep the structure, not just the SQL:
try/finally the unlock, and accept that a killed process drops a session lock automatically while a table-row lock does not.--force semantics; never prompt in a release phase.Advisory locks are PostgreSQL-specific. If the bundle uses pg_advisory_lock, it is a no-op or an error on MySQL. MySQL's GET_LOCK has similar semantics but a different lifetime model; MariaDB differs again. Check the driver before you assume the lock is doing anything.
PgBouncer in transaction-pooling mode silently breaks session locks. pg_advisory_lock is session-scoped; a pooler that hands your connection to someone else mid-session makes the lock meaningless or leaky. Use session pooling, or a direct connection, for the migration step.
A crashed process can leave an orphaned lock in table-row implementations, and a hung migration holds a session lock indefinitely in advisory implementations. Always know your escape hatch: SELECT * FROM pg_locks WHERE locktype = 'advisory'; to see who holds it, and pg_terminate_backend(pid) to clear it.
The lock does not make migrations safe to run twice concurrently — it makes them run once. If two deploys of different code versions overlap (blue/green, rolling update with old pods still serving), the lock serialises them but does not make a forward-then-backward migration sequence coherent. This is a schema/expand-contract problem, not a locking problem.
Staleness is still the real risk — 1.2.0 changes less than it looks. The 1.2.0 release notes are a single line, "Allow PHP 8": it relaxes the PHP constraint in composer.json so the bundle installs on modern runtimes. Nothing else moved. The Symfony and Doctrine version bounds, the command signatures, and the event wiring were not touched, so the compatibility concerns remain exactly as they were: Doctrine Migrations 3.x changed command signatures and event handling substantially, and Symfony 6/7 dropped several patterns this bundle likely relies on. Expect to pin doctrine/doctrine-migrations-bundle to a compatible range, or fork. Read the source (it is small, which is the one genuine advantage here) and vendor it or reimplement it rather than depending on it blindly — do not read "Allow PHP 8" as evidence of active maintenance or forward compatibility.
Debugging checklist when migrations appear to run concurrently anyway:
doctrine:migrations:migrate in a Procfile defeats the whole thing.Extension points worth knowing: the connection/lock service is normally a standalone DI service, so you can wrap it around other one-at-a-time release tasks (backfills, index builds, seeding) even if the migration command itself is too opinionated for your workflow. That is probably the most valuable thing to take from this bundle into a Laravel codebase: a small, reusable withAdvisoryLock(callable) helper, not the command.
How can I help you explore Laravel packages today?