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

Migrations Kit Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. 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).
  2. Open the repo's 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.
  3. Decide your migration runner. The bundle assumes Doctrine Migrations (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.
  4. First real use case: take an existing migration that already blew up once in staging with 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.

Implementation Patterns

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.

Gotchas and Tips

  • The bundle registration is inert in Laravel. No 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.
  • DBAL 3 vs 4 API split. 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".
  • Never cache introspection mid-migration. A 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().
  • Index names are not portable. MySQL, Postgres and SQLite generate different implicit names for the same logical index. Check by column set (or by your explicit CREATE INDEX name), never by whatever the platform auto-assigned, or indexExists() will lie on one of your environments.
  • Identifier case folding. Postgres folds unquoted identifiers to lowercase; MySQL varies by lower_case_table_names. A columnExists('ExternalRef') check can miss a column that exists as externalref. Normalise before comparing.
  • DDL is not transactional on MySQL. The guard makes that survivable — a failed mid-migration re-run just skips what already landed — but don't assume down() will cleanly undo a partially applied up().
  • Introspection is slow on wide schemas. 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.
  • Debug with generated SQL, not by running. Doctrine Migrations' SQL preview (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."
  • PHP floor is 8.2. If the host app is on 8.1 you cannot install it — check before promising the refactor.
  • Zero dependents, near-zero stars, recent release. Treat the array-definition key names and helper signatures as the contract and pin the version. Extension points are minimal by design, so budget for forking the two or three helper classes into your own namespace if the bundle stalls — the logic is thin enough that this is cheap insurance.
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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource