nowo-tech/migrations-kit-bundle
Nowo Tech Migrations Kit Bundle provides a practical toolkit for managing PHP database migrations. It bundles conventions and utilities to help teams create, organize, and execute migrations consistently across development and production environments.
This is a Symfony bundle, but the useful part — schema-introspection guards and array-based column/index definitions for idempotent Doctrine migrations — is plain PHP over a Doctrine DBAL Connection. That means a Laravel developer can consume it, with one caveat up front: the Symfony auto-wiring (bundle class, DI extension, tagged services) does nothing for you. You either (a) use the helper classes directly by constructing them with your Doctrine connection, or (b) bind them in a service provider.
Minimal path:
composer require nowo-tech/migrations-kit-bundle plus doctrine/dbal ^3|^4 and doctrine/migrations (Laravel ships DBAL already for some features, but not the migrations runner).src/ first — not the docs. You want the small set of classes exposing tableExists(), columnExists(), indexExists() and the array-definition applier. Note their constructor signature; if it's Connection-only, you can new them inline in a migration and skip the container entirely.AbstractMigration, up(Schema)/down(Schema)), not Laravel's Illuminate\Database\Migrations\Migration. Keep the two systems' tracking tables separate (doctrine_migration_versions vs migrations) so php artisan migrate doesn't try to replay Doctrine versions.Duplicate column name and rewrite the body as a guard — check existence, then addSql(). That single change is where the kit pays for itself.If you only ever write fresh Schema::create() migrations on a database you control, you don't need this. You need it when migrations are re-run against databases with unknown drift: multi-tenant fan-out, per-customer schemas, restored dumps, or a team that hand-patches prod.
Guard-then-DDL, one concern per block. The core pattern the package exists for:
public function up(Schema $schema): void
{
$sm = $this->connection->createSchemaManager();
if (! $sm->tablesExist(['orders'])) {
return; // nothing to patch yet
}
$columns = $sm->listTableColumns('orders');
if (! isset($columns['external_ref'])) {
$this->addSql('ALTER TABLE orders ADD external_ref VARCHAR(64) DEFAULT NULL');
}
}
The kit's helpers collapse the createSchemaManager() / listTableColumns() / isset() noise into named checks, which is the entire value proposition — the checks become readable intent instead of introspection boilerplate.
Array-based definitions for sets of columns. Instead of one if per column, declare a map and let the helper apply only the missing members:
$wanted = [
'external_ref' => ['type' => Types::STRING, 'length' => 64, 'notnull' => false],
'synced_at' => ['type' => Types::DATETIMETZ_IMMUTABLE, 'notnull' => false],
'sync_state' => ['type' => Types::SMALLINT, 'default' => 0, 'notnull' => true],
];
// apply only columns not present in the current schema
Verify the exact key names (type, length, notnull, default, index, …) against the bundle's documented array format before you build a shared base class on top of it — this is the one place where guessing costs you a broken migration.
Base migration class. Put the guard helpers behind a final abstract class IdempotentMigration extends AbstractMigration in your app with addColumnIfMissing(), addIndexIfMissing(), dropColumnIfExists(). Keeps third-party types out of every file and gives you one place to adapt when the DBAL major version moves.
Drift auditing outside migrations. Wrap the same checks in an artisan command that compares expected tables/columns (a config array) against live introspection and reports diffs. This is the highest-leverage secondary use: you get a "schema doctor" for staging without writing the introspection layer yourself.
Down migrations deserve the same treatment. DROP COLUMN on an already-dropped column errors just as loudly. Guard both directions; a half-applied rollback is worse than a half-applied forward migration.
Backfill/seed migrations. For data migrations that must be re-runnable, pair the column check with an idempotent UPDATE ... WHERE external_ref IS NULL — never INSERT without a matching existence probe on the natural key.
Kernel, no config/bundles.php. If a helper needs a Connection, resolve DB::connection()->getDoctrineConnection() (or build a Doctrine\DBAL\DriverManager connection from config/database.php) and inject it manually. Don't try to boot the Symfony extension.getSchemaManager() is deprecated/removed in favour of createSchemaManager(), and listTableColumns() returned-by-name vs list semantics shifted. The package supports both majors, but your own wrapper code must not hard-code the DBAL 3 shape — pin the DBAL major in composer.json and test against it rather than trusting "supports 3–4".SchemaManager snapshot taken before your first addSql() will happily tell you a column is absent after you added it. Re-introspect per block, or re-run the check after every DDL statement in the same up().CREATE INDEX name), never by whatever the platform auto-assigned, or indexExists() will lie on one of your environments.lower_case_table_names. A columnExists('ExternalRef') check can miss a column that exists as externalref. Normalise before comparing.down() will cleanly undo a partially applied up().listTableColumns() per column in a loop over 40 columns is 40 round-trips of metadata. Fetch the table's column list once, then loop in PHP.sql output / --dry-run on execute) tells you what your guards actually emitted; a silent no-op is far more common than a crash and looks identical to "migration did nothing."How can I help you explore Laravel packages today?