smartteam/migrations-bundle
A Symfony bundle for managing database migrations, helping you version, organize, and apply schema changes consistently across environments. It integrates migration tooling into your Symfony workflow so teams can track database evolution alongside application code.
What it actually is. smartteam/migrations-bundle is a maintenance fork of zenstruck/migrations-bundle. The original was a thin Symfony wrapper around doctrine/migrations that let you run pending migrations from a controller (web UI) or a Symfony command without shelling out to the Doctrine CLI, and it logged applied versions in its own database table. That is the whole value proposition — not migration tooling itself, but invocation and audit trail.
Read the adoption decision first. Last release January 2018, 0 stars, 0 dependents, and the description says it exists to keep an old bundle installable. Before writing any code:
composer show smartteam/migrations-bundle — check the required doctrine/dbal, doctrine/migrations, and Symfony version constraints. This tells you immediately whether it can coexist with anything modern in your composer.json.DependencyInjection/Configuration.php in the repo — that is the authoritative config tree, not the README.Zenstruck\Bundle\MigrationsBundle names while the package is named smartteam/.... That mismatch matters (see Gotchas).composer.json constraints and a rename, which means zero bug fixes since 2018.Install and register (Symfony 3/4 era — this is what the package targets):
composer require smartteam/migrations-bundle
Symfony 3: add the bundle class to app/AppKernel.php. Symfony 4: add it to config/bundles.php. If you want the web UI, import its routing resource in config/routes.yaml (or app/config/routing.yml) under a path you control, e.g. /admin/migrations.
First use case worth trying. A read-only "migration status" screen in an admin panel: list applied vs. pending versions, with a dry-run button that prints the SQL. This is low-risk and gives you a feel for the manager service before you let it mutate a production schema. Only after that should you wire the "run" action.
Where to look next, in order: the manager service (usually Migrations/MigrationManager, exposed via the bundle's service id), the version entity/mapping and its table name, and the console command definition — the command's options (--dry-run, target version, down) map 1:1 onto the manager API and are your best documentation.
Honest recommendation: if you are starting a new project, use doctrine/doctrine-migrations-bundle plus a small admin controller that calls doctrine:migrations:migrate --no-interaction through Symfony\Component\Process\Process. It is maintained, supports Doctrine Migrations 3.x, and covers every feature this bundle offers. Adopt this fork only if you are maintaining a legacy app that already depends on it and cannot yet migrate off.
Thin wrapper over the manager. Never let controllers touch Doctrine's Configuration/DependencyFactory directly. Inject the bundle's manager service and call its up() / down() / status() methods (verify exact signatures in the repo). Keep every call site in one small application-level service, e.g. App\Migration\Runner, so a future swap to doctrine-migrations is a single-file change.
final class Runner
{
public function __construct(private object $migrations) {}
/** @return string[] versions executed */
public function up(bool $dryRun = false): array
{
return $this->migrations->up($dryRun); // confirm signature against the source
}
}
Admin screen pattern. One route for status (GET), one for execute (POST). CSRF token on the POST, ROLE_ADMIN (or IS_AUTHENTICATED_FULLY) on both, and render the dry-run SQL output in a <pre> before offering the execute button. That two-step flow is the main reason to prefer this bundle over the CLI for non-engineers.
Deploy-hook pattern. The genuinely useful production integration is a post-deploy step that runs migrations outside the web request:
bin/console app:migrations:migrate --no-interaction.symfony/lock with a Redis/DB store, or MySQL GET_LOCK) so two deploy workers never migrate simultaneously.Dry-run-first workflow. The bundle's dry-run mode is its best feature. Make it the default in every non-production environment and require an explicit flag (--execute) to actually apply. Log the executed version list to your normal application logger from the command, since the bundle's own audit table only records what it applied.
CI/testing pattern. Use an in-memory SQLite connection and boot the kernel in a test, run the manager, then assert both the schema and the bundle's version table contents. Keep migrations themselves as plain doctrine/migrations classes with no container dependencies — if a migration needs the container, the bundle's web-invocation path will construct it differently than the CLI does and it will break intermittently.
Extension via decoration, not editing. Wrap/decorate the manager service in your own services.yaml to add locking, logging, or Slack/Teams notification on completion. That survives a composer update and keeps the fork's code pristine.
Coexistence rule. Pick one migration runner. Either the Doctrine CLI owns migrations and this bundle is read-only status, or this bundle owns them and nobody ever types doctrine:migrations:migrate. Mixed ownership is the single most common way teams get a "migration already applied / relation already exists" incident.
It is abandoned, and that has concrete consequences. No support for Doctrine Migrations 2.x/3.x AbstractMigration changes (return types on up()/down(), the removal of Configuration, DependencyFactory replacing constructors). No Symfony 5+/6+ support, no PHP 8 attributes, no autowiring-friendly service definitions. In practice you will pin doctrine/dbal and doctrine/migrations to old majors, which then blocks unrelated packages from upgrading. Run composer why-not doctrine/migrations "^3.0" early to see the real blast radius before committing.
Bundle name vs. package name mismatch. Forks often ship SmartTeam\MigrationsBundle as the package but keep Zenstruck\Bundle\MigrationsBundle as the bundle class/namespace, or vice versa. In Symfony 3, Bundle::getName() derives the config root key (zenstruck_migrations: vs. smartteam_migrations:); if your YAML uses the wrong root, the config is silently ignored and defaults are used. Symptom: your migrations_dir setting appears to do nothing. Verify with bin/console config:dump-reference <root_key>.
Two competing version tables. Doctrine Migrations tracks applied versions in migration_versions; this bundle historically tracked them in its own table. If anyone runs the Doctrine CLI once, the two tables diverge and the bundle will try to re-run already-applied migrations (or refuse to run pending ones). Decide which table is authoritative, and if you keep both, reconcile them with a one-off script and then enforce the single-runner rule in code review.
Web-invoked migrations are fragile. They run under PHP-FPM's max_execution_time, the web user's DB credentials, and no --no-interaction protection. A long index build times out mid-migration and leaves the schema half-changed. Always dry-run first, and reserve the web UI for small, fast, idempotent migrations.
MySQL/MariaDB DDL is not transactional. A failing migration does not roll back in MySQL. Write migrations that are re-runnable (guard with CREATE TABLE IF NOT EXISTS, check information_schema before adding an index) so a retry doesn't cascade into a second failure.
Config quirks to check in the repo. Typical keys are migrations directory, migrations namespace, version table name, and enabled environments. Override the table name to something namespaced (app_migration_log) so it doesn't collide if you later add the standard Doctrine bundle. If the UI shows an empty list, it is nearly always a wrong migrations_dir/namespace, not a broken manager.
Debugging order of operations. (1) Dump the service definitions: bin/console debug:container | grep -i migration. (2) Query the version table directly. (3) Run with dry-run/SQL output enabled and confirm the manager is picking up the files you think it is. (4) Check var/log for a swallowed exception — old bundles frequently catch and re-render rather than throw.
No security surface assumed. The original bundle's UI relied on the host app's firewall. There is no built-in authorization. If you expose the route, put it behind both a firewall and a path prefix removed from public reachability (e.g. bind /admin to an internal ingress rule), because a GET that applies migrations is a trivially abusable endpoint.
Migration escape hatch. If you inherit this bundle and want out: leave the code in place, add your own app:migrations:migrate command backed by doctrine/doctrine-migrations-bundle, point the admin UI at that command, and let the old bundle sit unused for one release before removing it. That gives you a rollback window without a big-bang rewrite.
How can I help you explore Laravel packages today?