kitloong/laravel-migrations-generator
Generate Laravel migrations from an existing database, complete with indexes and foreign keys. Supports MariaDB, MySQL, PostgreSQL, SQL Server and SQLite, with options to include, exclude or target specific tables via Artisan.
1. Install as a dev-only dependency. The generator is a build-time tool; nothing at runtime depends on it, so it belongs in require-dev.
composer require --dev kitloong/laravel-migrations-generator
Auto-discovery registers the service provider on Laravel. On Lumen you must enable facades and register KitLoong\MigrationsGenerator\MigrationsGeneratorServiceProvider in bootstrap/app.php manually.
2. Point the configured connection at the real database. The command reads config/database.php / .env — it does not accept a DSN on the command line. Either set DB_DATABASE to the source database, or add a dedicated connection (e.g. legacy) and reference it with --connection=legacy. Prefer a restored dump or read replica over hitting production directly.
3. Run the first generation against a small slice, not the whole schema. Start with two or three related tables so you can read the output end-to-end:
php artisan migrate:generate --tables="users,posts,comments"
4. Where to look first. In rough order of usefulness:
php artisan help migrate:generate — the authoritative option list for your installed version, which drifts from online docs.--squash, --date, --default-index-names, --template-path are the ones worth reading in full).5. The first real use case: schema archaeology. The canonical day-one win is opening a legacy or inherited database and getting a readable, runnable Schema::create() representation of it — indexes, foreign keys, comments, views, and stored procedures included — instead of hand-transcribing SHOW CREATE TABLE output.
php artisan migrate:generate --path=database/migrations/_generated
Generate to a throwaway directory first so you can review and diff before anything lands in database/migrations.
When you have a working app whose schema has drifted away from (or never had) migrations, generate a single squashed baseline that a fresh environment can actually run:
php artisan migrate:generate \
--squash \
--skip-log \
--skip-vendor \
--date="2024-01-01 00:00:00" \
--default-index-names \
--default-fk-names \
--use-db-collation
--squash collapses everything into one file, which sidesteps per-table file ordering and FK sequencing entirely. --skip-log is essential here: without it the generator writes rows into the migrations table, and a fresh clone would then consider the baseline already applied and create nothing. Verify with:
DB_DATABASE=scratch_db php artisan migrate:fresh --seed
The mirror image of the above. Here the tables already exist in every environment you'll deploy to, so you want the migration recorded as applied rather than executed:
php artisan migrate:generate --squash --log-with-batch=0 --skip-vendor
Batch 0 makes it the earliest batch, so ordinary migrate runs and migrate:rollback of later batches behave sanely. If you can't guarantee the migration will never run against a populated DB, add --with-has-table so the generated up() is guarded by Schema::hasTable().
For large schemas, generate per domain rather than all at once, and stage the review:
php artisan migrate:generate --tables="billing_invoices,billing_lines" --path=database/migrations/billing
php artisan migrate:generate --tables="users,teams,memberships" --path=database/migrations/tenancy
The catch: foreign keys are generated after all tables, so any FK pointing at a table outside the --tables list will fail at migrate time. Either include the referenced tables in the same invocation, or pass --skip-foreign-keys and hand-write the constraints afterwards.
The generator handles views and procs with dedicated filename patterns (_create_[name]_view.php, _create_[name]_proc.php) and stamps them one second after the table migrations so dependencies resolve. Use the filename options when your project has naming conventions or CI checks on migration filenames:
php artisan migrate:generate \
--table-filename="[datetime]_create_[name]_table.php" \
--view-filename="[datetime]_create_[name]_view.php" \
--fk-filename="[datetime]_add_foreign_keys_to_[name]_table.php"
Drop --skip-views / --skip-proc only when the target DB user actually holds the privileges to read view and routine definitions — a read-only reporting account often can't, and you'll get silently empty output rather than an error.
Because output is deterministic given --date, you can regenerate into a temp directory and diff against committed migrations to detect hand-edits or out-of-band DDL:
php artisan migrate:generate --squash --skip-log \
--date="2024-01-01 00:00:00" \
--path=storage/app/schema-check
git diff --exit-code -- storage/app/schema-check
This only works if the source database is itself built from the committed migrations — otherwise you're diffing the app against itself.
If your team mandates typed properties, docblocks, declare(strict_types=1), or a specific down() convention, publish the package stubs and point at them:
php artisan migrate:generate --template-path=resources/stubs/migrations-generator
This is the cheapest way to make generated output merge cleanly with hand-written migrations, and it survives version bumps better than post-processing with sed.
--path plus a git diff turns generation into a reviewable PR rather than a surprise.--date for any committed baseline. It removes timestamp collisions on repeated runs and keeps the file ordering stable across machines.varchar(255)) along with the good ones.migrate:fresh in a scratch DB as the acceptance test for any generated batch. If migrate:fresh doesn't reproduce the source schema, the generated set is wrong — usually FK ordering or a skipped view.Artisan::call('migrate:generate', [...]) in a project command lets you bake in your options and add a post-generation cleanup step.Logging is the #1 source of confusion. By default the generator records the migrations it writes into the migrations table. That's correct when the tables already exist and you're building a history; it's wrong when you want fresh environments to actually run them. Rule of thumb: --skip-log for greenfield baselines, default logging (ideally --log-with-batch=0) for existing databases. Getting this backwards produces either "nothing happens on a fresh install" or "table already exists."
Foreign keys only work if the whole graph is in the run. All tables are created first, then FK constraints are emitted as separate migrations. Any FK referencing an excluded table breaks the migration. Use --skip-foreign-keys as the escape hatch, or add the missing tables via --tables.
Index and FK names are the biggest portability trap. Database-generated names (users_email_address_unique) are captured verbatim unless you pass --default-index-names / --default-fk-names, which fall back to Laravel's naming convention. If you plan to replay these migrations on a different engine, prefer the default-names flags; if you have externally-referenced constraint names (other tools, FK hints), keep the DB names instead. Don't mix.
Collation silently changes. Without --use-db-collation, columns are emitted with Laravel's default charset/collation, not the source DB's. On a utf8mb3 legacy schema this changes comparison semantics and index key lengths. Pass the flag, or explicitly normalise — but decide deliberately.
--skip-vendor is close to mandatory. Telescope, Horizon, Spatie packages, Pulse, Sanctum and friends own tables in your DB. Generating migrations for them creates conflicts with the packages' own published migrations and duplicates them on migrate:fresh.
Long index prefixes on MySQL. Wide composite indexes or indexes on varchar(255) under utf8mb4 can exceed the engine's key length when reproduced literally. The generator emits index lengths where it can, but composite cases still need a manual pass.
PostgreSQL specifics. Native ENUM types are types, not column modifiers — output may need DB::statement wrapping or migration to a string/check-constraint. Schema-qualified databases need the right search_path; schema-name resolution was fixed in 7.1.1, so pin ≥ 7.1.1 if you use multiple schemas.
SQL Server specifics. User-defined types (added 6.5.1) and newer sqlsrv data types (6.6.0) are mapped, but custom types may land in the migration as raw strings. Computed/virtual/stored generated columns are supported (6.6.0, and built-in generation in 7.1.2) — verify the expressions survive the round trip.
SQLite is the weakest target. Schema introspection is limited; FKs, some defaults, and collations may be incomplete. Use it for a rough starting point only, and always validate with migrate:fresh.
Default value escaping changed in Laravel 12.20. Release 7.2.0 adjusted default escaper output. If you're on a mixed Laravel version fleet, generated files differ between developers — pin the package version in CI to keep diffs stable.
Enum constraint parsing was fixed in 7.2.0. Older versions mangled multi-value enums; if you see truncated enum lists, upgrade.
The --date off-by-one is intentional. Views and foreign keys are stamped +1 second so they sort after tables. Don't "fix" it in a post-processing script.
Anonymous migration classes (6.6.1+) require Laravel 9+. On older Laravel/PHP you need a 6.x release; 7.3.0 added Laravel 13 support, so check the compatibility matrix before upgrading. composer install --no-dev on production is safe — nothing references the package at runtime.
Missing directories. --path directories are created if absent (fixed 6.11.1), but nested paths on older 6.x releases fail. Upgrade or mkdir -p first.
Debugging failed generation. The command fails quietly for privilege problems — empty views, missing routines. Cross-check counts against the source:
SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE();
Then compare with the number of files produced. A mismatch means privilege or filter issues, not a generator bug.
Review checklist before committing generated output: foreign key onDelete/onUpdate behaviour, index names and duplicates, column collations, unsigned/autoIncrement on non-standard keys, default values for JSON/TEXT/BLOB columns (often unrepresentable), and every comment() — comments are captured (6.6.2) but are noisy in diffs.
Extension points worth knowing: --template-path (custom stubs), the publishable package config, and --table-filename/--view-filename/--proc-filename/--fk-filename for naming conventions. Beyond that, wrap the Artisan command rather than patching the package — it's a dev dependency that gets upgraded, and forks rot fast.
How can I help you explore Laravel packages today?