sfynx-project/tool-migration-bundle
A Symfony bundle from the Sfynx project that adds migration tooling to your application. It helps organize, generate, and run versioned migrations via the console, making schema and data changes easier to manage across environments.
Treat this as a Symfony 2/3-era bundle, not a Laravel package. There is no ServiceProvider, no facade, no vendor:publish, and no config/migration.php. If you are in Laravel, your realistic options are (a) use Laravel/Doctrine migrations and ignore it, or (b) mine the core classes and drive them from a small Artisan command. Everything below assumes you're evaluating it seriously enough to do (b) or to run it inside a Symfony app.
Where to look first, in this order:
Command/ — the console commands (migrate, generate, status, version) are the actual product surface. Read them before anything else; they tell you the real option names.DependencyInjection/Configuration.php — the authoritative list of config keys (migrations path, versions table name, column, namespace). Don't trust README examples over this file.Version entity/manager — this is the bookkeeping table. Find the exact CREATE TABLE it runs and the ordering it applies.up/down, and whether they receive a connection/manager argument), because that dictates what your migration files can do.Minimal bootstrap (Symfony):
composer require sfynx-project/tool-migration-bundle
Check the required symfony/* constraints in its composer.json before you commit to this — they will almost certainly cap you at Symfony ~2.8/3.x and PHP 7.1. Then register the bundle in AppKernel, point the config at a directory, and create the versions table (either the bundle's own setup command or a one-shot SQL from the Version class). Run the status command against an empty database first so you see the table is reachable before you write a single migration.
First real use case: baseline an existing schema. Generate one migration that is a no-op (or only records the current state), run it, and you now have a version row without touching the database. Every later migration is then additive and predictable.
Versioned files, ordered by name. Migrations are discovered from a directory and ordered by the version prefix in the filename. Use zero-padded, sortable prefixes (20171121000000_AddUserTable.php) so lexicographic ordering and chronological ordering agree. If the generate command emits its own timestamp format, keep it — don't hand-mix formats.
One concern per migration. Schema and data migrations get separate files. A schema migration that also backfills rows is the migration you will have to debug at 2am with a half-applied table.
Write down() even if you never call it. It forces you to state the inverse of what up() does, which is how you catch destructive DROP statements before they ship. If the bundle's rollback is unreliable for your driver, down() still documents intent.
Idempotent up(). Check for column/table existence before creating, and guard backfill statements with a WHERE that makes a second run a no-op. Combined with the implicit-commit behaviour of MySQL DDL (below), this is what lets you re-run a failed migration safely.
Gate deploys on status. In CI/CD, run the status command and fail the pipeline if anything is pending, then run migrate as an explicit deploy step with a lock (advisory lock, or a deploying row) so two app servers can't migrate concurrently.
Bridging into Laravel. The cleanest pattern is to pull out the manager + configuration classes as a plain library and expose one Artisan command:
// app/Console/Commands/MigrateTool.php
public function handle()
{
$manager = new \Sfynx\MigrationBundle\Manager\MigrationManager(/* path, table */);
$manager->migrate(); // whatever the internal execute method is named
}
Read the console command's execute() body and copy its sequence rather than inventing one — the order of "fetch applied versions → diff → sort → execute → record version" matters, and the status output is generated from the same state.
Forward-only is a valid policy. For production, use the bundle only for migrate and status; forbid down outside local dev. This sidesteps the bundle's weakest area (rollback correctness across drivers) and matches how most teams run Doctrine migrations anyway.
It is unmaintained since 2017. No security fixes, no Symfony 4/5/6 support, one star. Installing it on a modern stack will likely require Composer aliasing ("symfony/framework-bundle": "3.4 as 6.x") or a fork. If you only need the versioning table and the diff logic, forking the Manager and Version classes into your own namespace is a more honest long-term decision than pinning.
Command name collisions. If Doctrine migrations is also installed, both bundles may register a migrate-family command. Whichever loads last wins silently. Check bin/console list and rename one via the command definition.
Implicit commits on MySQL. DDL auto-commits, so a migration with three ALTER TABLEs that fails on the third leaves you half-migrated with no version row recorded. Keep DDL migrations single-statement where possible, or make every statement individually re-runnable.
Autoloading of migration files. Depending on your config, migration classes are either required directly from the file path or expected to be autoloadable under a configured namespace. If you get Class not found, the migration directory is not in Composer's autoload map — check DependencyInjection/Configuration.php for the namespace key and align it with composer.json.
Config typos fail silently. A misspelled table_name commonly results in the bundle seeing zero applied versions and re-running everything. After first setup, inspect the versions table directly and confirm a row appears; don't trust the success message alone.
Version ordering is string comparison. If the generator emits 1, 2, …, 10, migration 10 sorts before 2. Either zero-pad or confirm the sort is numeric — read the comparator, don't assume.
Debugging. Run commands with -v/-vv; wrap the manager call in a try/catch that prints $e->getMessage() plus the migration version being attempted, because the default error output often omits which file failed. When state is confused, the source of truth is the versions table plus the filesystem — reconcile those two by hand.
Extension points before forking. Override the manager service in your container services.yml (same service id), or register your own command that calls the manager, before you edit vendor code. Fork only when you need Symfony-version compatibility or transaction semantics fixes — at which point, honestly, compare the cost against just adopting Doctrine migrations.
How can I help you explore Laravel packages today?