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

v1.0.0 2026-02-23 10:53 UTC

This package is auto-updated.

Last update: 2026-02-23 10:55:25 UTC


README

๐Ÿ›ก๏ธ laravel-migration-guard

Catch dangerous database migrations before they reach production.

The strong_migrations equivalent for Laravel. Static analysis. Zero configuration. Framework-native.

Tests PHP Version Laravel Latest Version on Packagist Total Downloads License Stars Issues PRs Welcome Pest

Demo

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 migrate and warns before any dangerous migration runs
  • โœ… Is active only when APP_ENV is production or staging
  • โœ… Is completely silent in local and testing environments
  • โœ… 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; only Schema::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:

  1. Deploy 1: Remove all code references to the column (models, queries, $fillable, $casts)
  2. 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:

  1. Add the column as ->nullable() (instant, no lock)
  2. Backfill existing rows: User::whereNull('status')->update(['status' => 'active'])
  3. Add the NOT NULL constraint 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 migrate hook
  • migration:guard:analyse with table, JSON, GitHub output
  • migration:guard:ignore command
  • 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: CONCURRENTLY index builds, VACUUM considerations

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:fix command that rewrites the migration file with the safe pattern

Contributing

Contributions are welcome. Adding a new check requires only:

  1. Create a class implementing CheckInterface in src/Checks/
  2. Register it in MigrationGuardServiceProvider::register()
  3. Add the check ID to the checks array in config/migration-guard.php
  4. 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

Report a Bug ยท Request a Feature ยท Sponsor