Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Release Phase Migrations Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. 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.

  2. 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:

  • The bundle's console command class — that is the whole product. Everything else is DI glue.
  • The config tree (Configuration.php) — it tells you which connection/key/timeout knobs actually exist.
  • The lock implementation — confirm whether it uses 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.

Implementation Patterns

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:

  • Give the lock a stable, hard-coded integer key (a hash of the app name), not a random one.
  • Always try/finally the unlock, and accept that a killed process drops a session lock automatically while a table-row lock does not.
  • Expose --force semantics; never prompt in a release phase.
  • Log lock acquisition and wait time — the first thing you want in a postmortem is "how long did pod 3 wait".

Gotchas and Tips

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:

  1. Confirm the command wrapper is actually the one being invoked in the release phase — a stale doctrine:migrations:migrate in a Procfile defeats the whole thing.
  2. Confirm both processes connect to the same database and same role with the same search path.
  3. Confirm the lock key is constant across deploys and not derived from something per-process (hostname, PID, timestamp).
  4. Check the connection is not being closed and reopened between lock acquisition and migration.

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.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings