zenstruck/migrations-bundle
A Symfony bundle for managing database migrations. It provides commands and configuration to create, run, and track schema changes, helping keep your application's database structure in sync across environments.
This bundle is deprecated and archived, so treat this as an inheritance/maintenance guide, not a greenfield recommendation. If you are evaluating it for a new project, stop here and use doctrine/doctrine-migrations-bundle directly (see Gotchas for the migration path).
If you have inherited a project that depends on it:
Confirm what is actually installed before reading docs:
composer show zenstruck/migrations-bundle
composer why zenstruck/migrations-bundle
The Packagist name is zenstruck/migrations-bundle (repo is kbond/ZenstruckMigrationsBundle). Because it is archived and has ~zero dependents, expect the installed version to be old and pinned against an old Symfony/Doctrine pair.
Register the bundle. It is a wrapper around DoctrineMigrationsBundle, so the bundle registration order matters — the Doctrine migrations bundle must be present, and this bundle typically needs to be registered after it in AppKernel/bundles.php so its compiler pass can decorate/replace the migration services.
Look here first, in this order:
vendor/zenstruck/migrations-bundle/README.md — the actual usage contract (short, and the only reliable source given the age).DependencyInjection/ and any CompilerPass/ — this is where the "container-aware" magic lives. The pass is what tags migrations and injects the container.Command/ — the bundle likely overrides the Doctrine migrations:generate/migrations:diff commands to emit container-aware classes. Check whether you should be using its command instead of Doctrine's.Migration/ (e.g. a ContainerAwareMigration). This is what your migration classes should extend.First use case: a data migration that needs a service, which is the entire reason this bundle exists. Plain Doctrine migrations are dumb classes instantiated by the migrations library with no access to your DI container. This bundle makes a migration resolvable as a service so you can do things like:
// conceptual shape — verify the exact base class name in your installed version
class Version20240101000000 extends ContainerAwareMigration
{
public function up(Schema $schema): void
{
$slugger = $this->container->get('app.slugger');
// backfill slugs for existing rows
}
}
Service-backed migrations. The core pattern is: the migration class is registered in the container (via the bundle's compiler pass) and can pull collaborators. Day-to-day this means you use it for backfills, denormalizations, and one-off data fixes — not for schema changes, which should stay in plain doctrine:migrations migrations.
Prefer constructor/setter injection over $this->container->get(). If the base class exposes the container, resist using it as a service locator. Where the bundle allows it, fetch the specific dependency once and store it, so the migration stays testable and the dependency is explicit:
public function setSlugger(SluggerInterface $slugger): void { $this->slugger = $slugger; }
Workflow for a data migration:
Command/) so the generated class extends the container-aware base.->iterate() / pagination rather than loading the whole table, because these migrations run in a single CLI process with a live container and no request timeout.doctrine:migrations:migrate as usual; the DI wiring is invisible at runtime.Keep schema and data separate. Use doctrine:migrations:diff for schema, and this bundle only for migrations that genuinely need services. Mixing both in one class makes rollback reasoning hard.
Integration tips:
cli environment; ensure any service you inject is public or explicitly wired for CLI, and that it does not depend on request-scoped state.down() (or explicitly throw) so migrations:execute --down behaves predictably.app.slugger changes behavior six months later, an old migration that re-runs on a fresh database (CI, new dev machines) will produce different data.composer require may resolve to ancient constraints; composer why-not will tell you what is blocking an upgrade. Plan removal rather than extension.doctrine:migrations:migrate on a new database depends on current application code. This is the main reason the pattern fell out of favor.$this->container->get('...') throws "service not found" or a private-service error, it is almost always visibility, not wiring. Debug with debug:container <id> and debug:container --tag=<the bundle's migration tag> to confirm your migration was actually tagged by the compiler pass. If the tag is missing, the bundle is not registered late enough, or the migration class does not extend the expected base.migrations:generate, you may be calling Doctrine's command and getting a non-container-aware stub. Check debug:container --tag=console.command for duplicates and use the fully qualified command name if needed.doctrine/doctrine-migrations-bundle and drop the base class. For migrations that needed services, either (a) inline the logic, (b) use raw SQL/entity manager calls available through the migration's connection, or (c) for Doctrine Migrations 3.x, use the migration factory / a small custom factory to inject what you need. Do this bundle-removal as its own change, verify with a doctrine:migrations:migrate from an empty database in CI, and then delete the dependency.How can I help you explore Laravel packages today?