malikad778 / laravel-migration-guard
Catch dangerous database migrations before they reach production. The strong_migrations equivalent for Laravel. Zero configuration. Framework-native.
Installs: 134
Dependents: 0
Suggesters: 0
Security: 0
Stars: 7
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/malikad778/laravel-migration-guard
Requires
- php: ^8.2
- illuminate/console: ^10.0|^11.0|^12.0
- illuminate/database: ^10.0|^11.0|^12.0
- illuminate/events: ^10.0|^11.0|^12.0
- illuminate/notifications: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
- nikic/php-parser: ^5.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0
- pestphp/pest: ^2.0|^3.0
- pestphp/pest-plugin-laravel: ^2.0|^3.0
README
๐ก๏ธ laravel-migration-guard
Catch dangerous database migrations before they reach production.
The strong_migrations equivalent for Laravel. Static analysis. Zero configuration. Framework-native.
The Problem
Every Laravel team doing zero-downtime deployments has eventually had a migration incident. These operations succeed without errors in development, then cause production outages anywhere from immediately to hours later:
| Operation | What breaks in production |
|---|---|
dropColumn() |
Old app instances still query the dropped column โ immediate DB errors during the deployment window |
NOT NULL without default |
Full table rewrite on MySQL < 8.0 โ locks reads and writes for minutes on large tables |
renameColumn() |
Old instances use old name, new instances use new name โ one of them is always wrong |
addIndex() without INPLACE |
MySQL < 8.0 holds a full write lock while building the index โ minutes on busy tables |
change() column type |
Full table rewrite, potential silent data truncation (e.g. VARCHAR(50) โ VARCHAR(40)) |
Schema::rename() |
Every Eloquent model and raw query referencing the old table name breaks immediately |
truncate() in a migration |
Production data permanently destroyed โ migrations are the wrong place for data deletion |
Rails developers have had strong_migrations (4,000+ GitHub stars) for years. The Laravel ecosystem has no maintained equivalent. Every team solves this by hand: code review checklists, tribal knowledge, and hoping nobody forgets to check.
laravel-migration-guard eliminates that risk by making artisan migrate production-aware โ without changing your workflow.
Installation
composer require --dev malikad778/laravel-migration-guard
The package auto-discovers via Laravel's package discovery. No manual registration required.
Optionally publish the config file:
php artisan vendor:publish --tag=migration-guard-config
That's it. Out of the box, with zero configuration, the guard:
- โ
Hooks into
artisan migrateand warns before any dangerous migration runs - โ
Is active only when
APP_ENVisproductionorstaging - โ
Is completely silent in
localandtestingenvironments - โ
Outputs warnings inline before execution, allowing you to abort with
Ctrl+C
How It Works
The package uses static analysis โ it parses your migration PHP files into an Abstract Syntax Tree (AST) using nikic/php-parser and walks the tree looking for dangerous method call patterns.
This means:
- No database connection needed โ analysis works against raw PHP files in any environment, including CI/CD pipelines
- Sub-millisecond per file โ PHP AST parsing is extremely fast; 200 migration files takes under a second
- Only the
up()method is analysed โdown()rollbacks are intentionally excluded Schema::create()is excluded โ creating a fresh table with no existing rows is always safe; onlySchema::table()operations are checked
Analysis Pipeline
Migration file
โ
PHP-Parser AST
โ
Extract up() method body
โ
Walk AST nodes (Schema::table / Schema::create context tracked)
โ
Run registered check visitors
โ
Collect Issue objects (severity, table, column, message, safe alternative)
โ
Console / JSON / GitHub Annotation reporter
Safety Checks
Nine checks are included. All enabled by default, individually configurable.
| Check ID | Severity | What It Detects |
|---|---|---|
drop_column |
๐ด BREAKING | dropColumn() or dropColumns() on an existing table |
drop_table |
๐ด BREAKING | Schema::drop() or Schema::dropIfExists() |
rename_column |
๐ด BREAKING | renameColumn() on any table |
rename_table |
๐ด BREAKING | Schema::rename() |
modify_primary_key |
๐ด BREAKING | dropPrimary() or primary() on an existing table |
truncate |
๐ด BREAKING | DB::table()->truncate() inside a migration |
add_column_not_null |
๐ก HIGH | Column added without ->nullable() or ->default() |
change_column_type |
๐ก HIGH | ->change() modifying an existing column type |
add_index |
๐ต MEDIUM | Index added to a critical or large table |
Check Details & Safe Alternatives
Drop Column โ BREAKING
// โ DANGEROUS
Schema::table('invoices', function (Blueprint $table) {
$table->dropColumn('amount');
});
Why: During a zero-downtime deployment, old app instances run alongside the new schema. Any query touching the dropped column fails immediately with a database error.
Safe approach:
- Deploy 1: Remove all code references to the column (models, queries,
$fillable,$casts) - Deploy 2: Drop the column after confirming no running instance references it
Add NOT NULL Column Without Default โ HIGH
// โ DANGEROUS โ locks the table on MySQL < 8.0
Schema::table('users', function (Blueprint $table) {
$table->string('status');
});
// โ
SAFE
Schema::table('users', function (Blueprint $table) {
$table->string('status')->nullable();
});
Why: MySQL < 8.0 requires a full table rewrite when adding a NOT NULL column without a default. On a large table this blocks all reads and writes for minutes.
Safe approach:
- Add the column as
->nullable()(instant, no lock) - Backfill existing rows:
User::whereNull('status')->update(['status' => 'active']) - Add the
NOT NULLconstraint in a separate migration after backfill completes
Rename Column / Table โ BREAKING
// โ DANGEROUS
Schema::table('users', function (Blueprint $table) {
$table->renameColumn('name', 'full_name');
});
Schema::rename('users', 'customers');
Why: Old instances use the old name, new instances use the new name โ one is always wrong during the deployment window. Eloquent models, raw queries, and $fillable arrays all break.
Safe approach: Add new column โ copy data โ update code โ deploy โ drop old column in a follow-up migration.
Add Index (on critical/large tables) โ MEDIUM
// โ ๏ธ RISKY on tables with millions of rows
Schema::table('orders', function (Blueprint $table) {
$table->index('user_id');
});
// โ
SAFE โ use native syntax for online index creation
DB::statement('ALTER TABLE orders ADD INDEX idx_user_id (user_id) ALGORITHM=INPLACE, LOCK=NONE');
Why: MySQL < 8.0 holds a full write lock while building an index. MySQL 8.0+ and PostgreSQL support online index builds but require specific syntax that Laravel migrations do not use by default.
Change Column Type โ HIGH
// โ DANGEROUS
Schema::table('users', function (Blueprint $table) {
$table->string('bio', 100)->change(); // was VARCHAR(255)
});
Why: A full table rewrite is required in most databases. Implicit type coercions can silently corrupt data (e.g. VARCHAR(255) โ VARCHAR(100) truncates existing values). Indexes on the column may be dropped.
Safe approach: Add new column of the correct type โ migrate data โ update code โ deploy โ drop old column.
Example Warning Output
$ php artisan migrate
Running migrations...
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MIGRATION GUARD โ BREAKING โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
File : 2024_01_15_000001_drop_amount_column.php
Line : 12
Check : drop_column
Table : invoices
Column : amount
Dropping column 'amount' from 'invoices' is dangerous.
Running app instances may still query this column.
Safe approach:
1. Remove code references to 'amount' in this deployment.
2. Drop the column in a follow-up deployment.
Continue anyway? [y/N]
Configuration
<?php
// config/migration-guard.php
return [
// Environments where guard is active.
// Empty array = always active.
'environments' => ['production', 'staging'],
// 'warn' -> display warning, let developer abort with Ctrl+C
// 'block' -> throw exception, halt migration immediately
'mode' => env('MIGRATION_GUARD_MODE', 'warn'),
// Toggle individual checks on or off.
'checks' => [
'drop_column' => true,
'drop_table' => true,
'rename_column' => true,
'rename_table' => true,
'add_column_not_null' => true,
'change_column_type' => true,
'add_index' => true,
'modify_primary_key' => true,
'truncate' => true,
],
// Tables that always trigger extra scrutiny for index checks.
'critical_tables' => [
// 'users', 'orders', 'payments',
],
// Row count threshold for automatic large-table detection (requires live DB connection).
'row_threshold' => env('MIGRATION_GUARD_ROW_THRESHOLD', 500000),
// Suppress a specific check on a specific table or column.
// Use after confirming the operation is safe for your situation.
'ignore' => [
// ['check' => 'drop_column', 'table' => 'legacy_logs'],
// ['check' => 'add_column_not_null', 'table' => 'users', 'column' => 'migrated_at'],
],
];
Environment Variable Overrides
| Variable | Description |
|---|---|
MIGRATION_GUARD_MODE |
warn or block. Overrides config file. |
MIGRATION_GUARD_DISABLE |
Set to true to disable entirely (e.g. in CI seed steps). |
MIGRATION_GUARD_ROW_THRESHOLD |
Row count above which a table is treated as critical for index checks. Default: 500000. |
Artisan Commands
php artisan migration:guard:analyse
Standalone command for CI/CD pipelines. Analyses all pending migrations and outputs a report without running them. Exits with code 1 if dangerous operations are found.
# Analyse all pending migrations (default)
php artisan migration:guard:analyse
# JSON output โ for GitLab Code Quality or custom tooling
php artisan migration:guard:analyse --format=json
# GitHub Actions annotations โ inline PR diff comments
php artisan migration:guard:analyse --format=github
# Control when CI fails
php artisan migration:guard:analyse --fail-on=breaking # default
php artisan migration:guard:analyse --fail-on=high # BREAKING + HIGH
php artisan migration:guard:analyse --fail-on=any # all severities
php artisan migration:guard:analyse --fail-on=none # never fail (report only)
# Analyse all migrations, not just pending
php artisan migration:guard:analyse --pending-only=false
# Analyse a specific file or directory
php artisan migration:guard:analyse --path=database/migrations/2024_01_15_drop_column.php
Exit codes:
| Code | Meaning |
|---|---|
0 |
No issues found, or all issues below --fail-on threshold |
1 |
One or more issues at or above threshold |
2 |
Analysis error (parse failure, permission error) |
php artisan migration:guard:ignore
Adds a suppression entry to config/migration-guard.php for a specific check and table โ or check, table, and column.
# Suppress an entire table for a check
php artisan migration:guard:ignore drop_column legacy_logs
# โ Added: ignore drop_column on table 'legacy_logs'
# Suppress a specific column on a specific table
php artisan migration:guard:ignore add_column_not_null users migrated_at
# โ Added: ignore add_column_not_null on table 'users' column 'migrated_at'
Valid check IDs: drop_column, drop_table, rename_column, rename_table, add_column_not_null, change_column_type, add_index, modify_primary_key, truncate
JSON Output Schema
[
{
"check": "drop_column",
"severity": "breaking",
"file": "2024_01_15_000001_drop_amount_column.php",
"file_path": "/var/www/database/migrations/2024_01_15_000001_drop_amount_column.php",
"line": 12,
"table": "invoices",
"column": "amount",
"message": "Dropping column 'amount' from 'invoices' is dangerous.",
"safe_alternative": "Remove code references first. Drop in a follow-up deployment."
}
]
CI/CD Integration
GitHub Actions
# .github/workflows/migration-guard.yml
name: Migration Safety Check
on: [pull_request]
jobs:
migration-guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- run: composer install --no-interaction --prefer-dist
- run: php artisan migration:guard:analyse --format=github --fail-on=breaking
The --format=github flag produces GitHub Actions annotation syntax, placing inline warnings directly on the pull request diff at the relevant migration file line.
GitLab CI
migration-guard:
stage: test
script:
- composer install --no-interaction
- php artisan migration:guard:analyse --format=json > migration-guard-report.json
artifacts:
reports:
codequality: migration-guard-report.json
Package Architecture
src/
โโโ Checks/
โ โโโ CheckInterface.php
โ โโโ AbstractCheck.php โ isIgnored(), extractColumnsFromArgs() helpers
โ โโโ DropColumnCheck.php
โ โโโ DropTableCheck.php
โ โโโ RenameColumnCheck.php
โ โโโ RenameTableCheck.php
โ โโโ AddColumnNotNullCheck.php
โ โโโ ChangeColumnTypeCheck.php
โ โโโ AddIndexCheck.php โ live DB row count query (v1.1.0+)
โ โโโ ModifyPrimaryKeyCheck.php
โ โโโ TruncateCheck.php
โโโ Issues/
โ โโโ Issue.php โ readonly DTO: checkId, severity, table, columnโฆ
โ โโโ IssueSeverity.php โ enum: BREAKING | HIGH | MEDIUM
โโโ Reporters/
โ โโโ ReporterInterface.php
โ โโโ ConsoleReporter.php
โ โโโ JsonReporter.php
โ โโโ GithubAnnotationReporter.php
โโโ Commands/
โ โโโ AnalyseCommand.php โ migration:guard:analyse
โ โโโ IgnoreCommand.php โ migration:guard:ignore
โ โโโ DigestCommand.php โ migration:guard:digest (v1.2.0)
โ โโโ FixCommand.php โ migration:guard:fix (v2.0.0)
โโโ Listeners/
โ โโโ MigrationStartingListener.php
โโโ MigrationAnalyser.php โ core: parse โ traverse โ collect issues
โโโ MigrationNodeVisitor.php โ AST visitor: tracks Schema::table context
โโโ MigrationContext.php โ current table name + isCreate flag
โโโ MigrationGuardServiceProvider.php
Requirements
| Version | |
|---|---|
| PHP | 8.2 or higher |
| Laravel | 10.x, 11.x, 12.x |
| MySQL | 5.7+ or 8.0+ |
| PostgreSQL | 13+ |
| SQLite | 3+ |
nikic/php-parser |
^5.0 (installed automatically) |
Comparison: strong_migrations vs laravel-migration-guard
| Feature | strong_migrations | laravel-migration-guard |
|---|---|---|
| Drop column detection | โ | โ |
| Drop table detection | โ | โ |
| Rename detection | โ | โ |
| NOT NULL without default | โ | โ |
| Index safety | โ | โ |
| CI/CD JSON output | โ | โ |
| GitHub Annotations | โ | โ |
| Per-table suppression | โ | โ |
| Per-column suppression | โ | โ |
| Warn vs Block mode | โ | โ |
| Zero config defaults | โ | โ |
| Framework | Rails only | Laravel only |
Roadmap
v1.0.0 โ Launch (current)
- All 9 checks fully implemented and tested
artisan migratehookmigration:guard:analysewith table, JSON, GitHub outputmigration:guard:ignorecommand- Full documentation
v1.1.0 โ Database Awareness
- Query the live database to get actual row counts for index safety thresholds
- Show estimated lock duration based on table size
- PostgreSQL-specific checks:
CONCURRENTLYindex builds,VACUUMconsiderations
v1.2.0 โ Reporting
- Weekly migration safety digest: summary of all migrations run in the past 7 days
- Slack / email notification when dangerous migrations are bypassed in production
- Audit log of every migration run with who triggered it
v2.0.0 โ Safe Alternative Code Generation
- For each detected issue, generate the safe equivalent migration stub automatically
migration:guard:fixcommand that rewrites the migration file with the safe pattern
Contributing
Contributions are welcome. Adding a new check requires only:
- Create a class implementing
CheckInterfaceinsrc/Checks/ - Register it in
MigrationGuardServiceProvider::register() - Add the check ID to the
checksarray inconfig/migration-guard.php - Write unit tests covering both the unsafe pattern and the safe equivalent (false positive tests are required)
# Run the test suite
./vendor/bin/pest
# Run with coverage
./vendor/bin/pest --coverage
License
MIT โ free forever. See LICENSE.md.
Made for the Laravel ecosystem ยท Inspired by strong_migrations
