chr15k / laravel-schema-audit
Performs static analysis of your Laravel migrations to detect schema inconsistencies before they ship.
Requires
- php: ^8.2
- illuminate/console: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- nikic/php-parser: ^5.0
Requires (Dev)
- laravel/pint: ^1.30
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^5.0
- pestphp/pest-plugin-phpstan: ^5.0
- pestphp/pest-plugin-rector: ^5.0
- pestphp/pest-plugin-type-coverage: ^5.0
- phpstan/phpstan: ^2.2
- rector/rector: ^2.5
README
Laravel Schema Audit
Catch schema problems before production
Laravel Schema Audit statically reconstructs your database schema from its migration history and catches structural issues before they reach production.
No database connection. No migration execution. Just fast, CI-friendly analysis that detects duplicate and redundant indexes, invalid foreign keys, mismatched types, invalid references, missing primary keys, and more.
Built for Laravel and designed to work with real-world migration code, including common conventions and conditional schema logic.
Requirements
- PHP 8.2+
- Laravel 11, 12, or 13
Installation
composer require chr15k/laravel-schema-audit --dev
Optionally publish the config file:
php artisan vendor:publish --tag=schema-audit-config
Usage
php artisan schema:audit
By default this reads database/migrations and prints a styled report. The command exits with a non-zero status when findings are present, making it suitable for CI.
# scan a different migration directory php artisan schema:audit --path=/path/to/migrations # machine-readable output for CI and automation php artisan schema:audit --json # inspect the reconstructed schema without running rules php artisan schema:audit --schema-only
Important
Schema Audit treats all provided migration paths as one database schema. Run separate audits for applications or connections with independent databases.
What gets checked
| Rule | What it flags |
|---|---|
UnindexedForeignKeyRule |
A foreign key with no covering index. Driver-aware based on whether the target database automatically indexes foreign key columns. |
DuplicateIndexRule |
The same index (same columns, same uniqueness) declared more than once. |
DuplicateForeignKeyRule |
The same foreign key (same column, same referenced table) declared more than once. |
RedundantIndexRule |
A single-column index already covered by a composite index's leading column. |
DanglingForeignKeyRule |
A foreign key referencing a table that doesn't exist anywhere in the schema — a typo, or a table renamed/dropped without updating the reference. |
MismatchedForeignKeyRule |
A foreign key whose column type doesn't match the referenced table's primary key type (e.g. foreignId() pointing at a plain increments() primary key). |
MissingPrimaryKeyRule |
A table with no identifiable primary key — no id()/increments()-style column and no explicit primary() call. |
InvalidReferencedKeyRule |
A foreign key referencing a column with no primary or unique key on the parent table. |
Note
Rules run against the schema reconstructed from your migration history. For ordinary migrations, findings are concrete. Where runtime conditionals affect schema changes, affected findings are marked conditional because the resulting schema cannot be determined statically with certainty.
Enforce your own schema policies
Built-in rules catch common database problems. Custom rules let your team enforce application-specific schema standards in CI.
For example, you might want to prevent developers from adding expensive column types to high-traffic tables:
final readonly class NoTextColumnsOnHighTrafficTablesRule extends Rule { protected function check(AuditContext $context): iterable { foreach ($context->schema->tables() as $table) { if (! in_array($table->name, ['orders', 'events', 'sessions'], true)) { continue; } foreach ($table->columns() as $column) { if ($column->method === ColumnMethod::Text) { yield $this->warning( table: $table->name, columns: $column->name, message: "Avoid TEXT columns on high-traffic tables.", location: $column->location, guard: $column->guard, ); } } } } }
Register it alongside the built-in rules:
'rules' => [ Rules\UnindexedForeignKeyRule::class, Rules\DuplicateIndexRule::class, App\SchemaRules\NoTextColumnsOnHighTrafficTablesRule::class, ],
This turns Schema Audit into more than a collection of database checks: your team can codify its own schema rules and make them part of the CI pipeline.
See GUIDE.md for writing custom rules and advanced usage.
Configuration
// config/schema-audit.php use Chr15k\SchemaAudit\Rules; return [ 'paths' => [ database_path('migrations'), ], 'driver' => env('DB_CONNECTION', 'mysql'), 'rules' => [ Rules\UnindexedForeignKeyRule::class, Rules\DuplicateIndexRule::class, Rules\DuplicateForeignKeyRule::class, Rules\RedundantIndexRule::class, Rules\DanglingForeignKeyRule::class, Rules\MissingPrimaryKeyRule::class, Rules\MismatchedForeignKeyRule::class, Rules\InvalidReferencedKeyRule::class, ], 'report_conditional_findings' => true, ];
Note
paths — migration directories to analyze. Use --path to override for a single run.
driver — target database driver. Some rules are driver-specific, such as whether foreign keys automatically create indexes.
report_conditional_findings — report findings affected by conditional schema logic. Set to false to suppress them.
rules — enable, disable, or replace audit rules.
Documentation
See the Guide for:
- supported schema operations
- static value resolution
- conditional migrations
- limitations and unsupported operations
- multiple database connections
- writing custom rules
Limitations
Schema Audit uses static analysis rather than executing migrations. It supports Laravel's Schema Builder and common Laravel conventions, but runtime-generated schema changes and database-specific SQL cannot always be reconstructed.
Conditional schema changes are handled conservatively and affected findings are marked conditional.
See the Guide for supported operations, conditional migrations, static value resolution, and unsupported cases.
