skills/team/php-migration-manager/SKILL.md
Manages the full Laravel database migration lifecycle with safety checks and rollback planning. PHP analog of ef-migration-manager, alembic-migration-manager, and sqlx-migration-manager. Covers creating, reviewing, applying, and rolling back migrations; enforces a reversible down() method, expand-contract for zero-downtime changes, and guards against destructive operations in production. Use when creating or reviewing Laravel migrations, planning a schema change, applying or rolling back migrations, or designing a zero-downtime migration. Triggers on phrases like "laravel migration", "php database migration", "create migration laravel", "migrate rollback", "laravel schema change", "artisan migrate", "zero downtime migration laravel".
npx skillsauth add michaelalber/ai-toolkit php-migration-managerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
"A migration you cannot roll back is a deployment you cannot reverse."
"Schema changes are the riskiest deploys you make. Treat them that way."
Laravel migrations are version control for the database. This skill manages their full lifecycle — create → review → test rollback → apply → verify — with safety gates between each step. A migration is code that runs against production data exactly once per environment; a mistake is not a failed build, it is corrupted or destroyed data.
The central discipline is reversibility. Every up() has a matching down() that returns the schema
to its prior state. The central risk is destructive change on a live table: dropping a column,
renaming, or adding a non-nullable column with no default while the old code still runs. For those, the
expand-contract pattern (a.k.a. parallel change) replaces a single dangerous migration with a safe
sequence across multiple deploys.
Non-Negotiable Constraints:
up() has a tested down() — rollback is verified, not assumedmigrate:fresh / migrate:refresh / wipe are forbidden against any shared or production DBartisan migrate runsUPDATE on a large table| # | Principle | Description | Applied As |
|---|-----------|-------------|------------|
| 1 | Reversibility | down() exactly reverses up(). If a change is irreversible (data loss), say so explicitly and gate it. | up() adds a column → down() drops it; tested with migrate then migrate:rollback |
| 2 | Expand-contract | Breaking schema changes are split: expand (add new, dual-write), migrate data, contract (drop old) — across separate deploys. | Add email_verified_at nullable → backfill → enforce → later drop legacy verified flag |
| 3 | Additive first | New columns are nullable or have a default so old code keeps working during the deploy window. | $table->string('phone')->nullable(); not a non-null no-default add |
| 4 | Batched backfills | Data migrations chunk over rows; never one statement locking the whole table. | Model::query()->whereNull('x')->chunkById(1000, ...) |
| 5 | Online index creation | On large tables, create indexes without a long exclusive lock where the engine allows. | Postgres: raw CREATE INDEX CONCURRENTLY outside a transaction |
| 6 | One concern per migration | Each migration does one logical change; easier to review, apply, and roll back. | Separate "add column" and "backfill" migrations |
| 7 | No prod fresh/refresh | migrate:fresh, migrate:refresh, db:wipe drop tables — dev-only. | Guard with environment checks; never run against shared data |
| 8 | Review then apply | The migration is read, risk-rated, and rollback-tested in a scratch DB before it touches anything shared. | Run the REVIEW workflow, produce the Migration Review report, then apply |
| 9 | Foreign keys are explicit | FK constraints and their onDelete behavior are declared, and dropped in down() before the table/column. | $table->foreignId('user_id')->constrained()->cascadeOnDelete(); |
| 10 | Migrations are immutable once shipped | Never edit a migration that has run in any shared environment — write a new one. | A mistake in a shipped migration → a corrective new migration |
Use search_knowledge (grounded-code-mcp, collection="php").
| Query | When to Call |
|-------|--------------|
| search_knowledge("Laravel migration structure up down schema builder", collection="php") | When writing the migration |
| search_knowledge("Laravel migration rolling back rollback steps", collection="php") | When planning and testing rollback |
| search_knowledge("Laravel schema builder columns indexes foreign keys", collection="php") | When defining columns/indexes/constraints |
| search_knowledge("Laravel migration modifying columns change nullable", collection="php") | When altering existing columns (needs doctrine/dbal on older versions) |
| search_code_examples("Laravel migration create table foreignId", language="php") | When generating the migration body |
php artisan make:migration create_orders_table # new table
php artisan make:migration add_phone_to_users_table # alter
Write both up() and down(). Prefer additive, nullable changes. See
references/migration-safety-checklist.md for column-by-column guidance.
Read the migration and risk-rate it before applying. Classify each operation as
SAFE / CAUTION / DANGEROUS using references/dangerous-operations.md. Produce the Migration Review
report (template below). DANGEROUS operations require an expand-contract plan.
Apply and immediately roll back in a scratch database to prove down() works.
php artisan migrate --database=scratch
php artisan migrate:rollback --database=scratch --step=1
php artisan migrate:status --database=scratch
If down() errors or leaves residue, fix it before going further.
php artisan migrate --step # records each migration separately for granular rollback
php artisan migrate:status
Confirm the schema, run the backfill (batched), and re-run the app's test suite. Produce the Post-Apply Verification report.
<php-migration-manager-state>
phase: CREATE | REVIEW | TEST_ROLLBACK | APPLY | VERIFY | COMPLETE
migration: [migration file]
laravel_version: [detected]
operation_class: SAFE | CAUTION | DANGEROUS
expand_contract_required: true | false
down_method_present: true | false
rollback_tested: true | false
backfill_required: true | false
applied: true | false
last_action: [description]
</php-migration-manager-state>
## Migration Review: [migration name]
**Operation(s):** [add column / drop column / rename / index / FK / backfill]
**Risk class:** SAFE | CAUTION | DANGEROUS
**Reversible:** yes | no (explain)
### Findings
| Operation | Class | Risk | Mitigation |
|-----------|-------|------|------------|
| add `phone` nullable | SAFE | none | — |
| drop `legacy_flag` | DANGEROUS | data loss; old code reads it | expand-contract: stop reads → deploy → drop later |
### Rollback Plan
- `down()` reverses: [list]
- Irreversible parts: [list or "none"]
- Tested: yes | no
### Decision
- [ ] Apply as-is (SAFE)
- [ ] Apply with batched backfill (CAUTION)
- [ ] Split into expand-contract sequence (DANGEROUS)
## Post-Apply Verification: [migration name]
- [ ] `migrate:status` shows the migration as run
- [ ] Schema matches intent (`php artisan db:show` / describe table)
- [ ] Backfill completed in batches; row counts reconcile
- [ ] Application test suite green
- [ ] Rollback rehearsed in staging
- [ ] No long lock observed during apply (checked slow-query / lock logs)
migrate:fresh Against Shared Datamigrate:fresh, migrate:refresh, and db:wipe drop all tables. They are for local/CI databases
only. Against staging or production they are data destruction. Use migrate / migrate:rollback for
real environments.
down() and Test Itpublic function up(): void
{
Schema::table('users', fn (Blueprint $t) => $t->string('phone')->nullable());
}
public function down(): void
{
Schema::table('users', fn (Blueprint $t) => $t->dropColumn('phone'));
}
Then prove it: migrate → migrate:rollback --step=1 → migrate:status.
Dropping or renaming a column that running code still uses causes errors mid-deploy. Split it:
expand (add the new shape, dual-write), migrate (backfill), contract (drop the old shape in a
later deploy once no code references it). See references/dangerous-operations.md.
| # | Anti-Pattern | Why It Fails | Correct Approach |
|---|-------------|-------------|-----------------|
| 1 | Empty or missing down() | Cannot roll back a bad deploy | Always write and test down() |
| 2 | migrate:fresh in prod/staging | Drops every table — total data loss | migrate / migrate:rollback only on shared DBs |
| 3 | Non-null, no-default column on a live table | Old code inserts NULL → write failures during deploy | Add nullable / with default, backfill, then enforce |
| 4 | Renaming a column in one migration | Old code reads the old name mid-deploy | Expand-contract: add new, dual-write, drop old later |
| 5 | Unbounded backfill UPDATE | Locks the table; replication lag; timeouts | chunkById() batches with throttling |
| 6 | Editing a shipped migration | Environments diverge; some ran the old version | Write a new corrective migration |
| 7 | Index on a huge table inside a transaction | Long exclusive lock blocks writes | CREATE INDEX CONCURRENTLY (Postgres) outside a transaction |
| 8 | Mixing schema + heavy data change | Hard to review and roll back; long transaction | Separate schema and data migrations |
| 9 | FK dropped after its column in down() | down() fails on constraint dependency | Drop the FK before the column/table in down() |
| 10 | Applying before reviewing | Risk discovered in production | REVIEW + rollback test gate every apply |
down() fails on rollbackSymptom: migrate:rollback throws (e.g., dropping a column an FK still references).
Fix: in down(), drop constraints/indexes before the column or table they depend on; re-test the
rollback in the scratch DB until clean. Order matters: reverse of up(), dependencies first.
Symptom: writes stalled during apply; slow-query log shows a long ALTER.
Recovery: for the next change, switch to expand-contract; add indexes CONCURRENTLY; run backfills in
small batches off-peak. If mid-incident, consider killing the statement and rolling back the partial
migration, then re-plan.
change() fails: "Unknown database type" / doctrine/dbal missingSymptom: a column ->change() errors on Laravel < 11.
Fix: require doctrine/dbal (composer require doctrine/dbal) for column modifications on older versions;
on Laravel 11+ native column changes are supported without it. Confirm the modified definition restates
ALL attributes (a change() replaces, it does not merge).
| Skill | Relationship |
|-------|-------------|
| php-feature-slice | A new slice that needs schema changes uses this skill for the migration lifecycle. |
| php-architecture-checklist | The checklist flags missing down() and direct-SQL risks this skill prevents. |
| php-security-review | Reviews migrations for unsafe raw SQL and over-broad grants. |
| php-api-scaffolder | When an endpoint needs new tables/columns, sequence the migration before shipping the route. |
| tdd | Backfill logic and data transformations are driven test-first against a scratch database. |
development
Federal / government security overlay applied ON TOP OF a base language security review (dotnet/python/php/rust/react). Language-agnostic: adds NIST SP 800-53 control mapping, FIPS 140-2/3 cryptographic compliance (with a per-language crypto table), CUI handling, EO 14028 supply-chain requirements, and DOE Order 205.1B, and emits POA&M-ready findings with FIPS 199 impact levels. Use for federal/DOE/DOD/national-laboratory systems. Triggers on "federal security review", "NIST compliance", "NIST 800-53", "FISMA", "CUI", "FIPS audit", "DOE security", "POA&M", "ATO review". Do NOT use alone — run the matching <lang>-security-review FIRST; this overlay maps and extends it.
tools
OWASP-based security review of React / TypeScript front-end applications. Detects the framework (Vite/CRA/Next), entry points, and data flows, scans against the OWASP Top 10 (2025) mapped to React client-side patterns (XSS via raw HTML, URL/protocol injection, secrets in the bundle, insecure token storage, dependency CVEs, missing CSP, open redirects), and produces a manager-friendly executive summary plus a graded technical findings table. Use to audit React code for vulnerabilities. Triggers on "react security review", "frontend security audit", "audit react for vulnerabilities", "owasp react", "react xss", "react security posture", "npm audit review". For federal / gov / DOE / NIST / FIPS / CUI context, run security-review-federal after this base review. Do NOT use to grade architecture/structure — use react-architecture-checklist.
tools
Analyzes legacy React codebases and produces actionable modernization plans. Primary migration paths include class components to function components + hooks, Create React App to Vite, React 16/17 to 18 to 19, JavaScript to TypeScript, Enzyme to React Testing Library, legacy Redux to Redux Toolkit / Zustand / Context, and deprecated lifecycle/API removal. Does NOT perform the migration — assesses, quantifies risk, and plans. Triggers on phrases like "modernize react", "class to hooks", "upgrade react", "migrate CRA to vite", "react legacy migration", "react 17 to 18", "react js to typescript", "react technical debt", "enzyme to RTL".
development
Scaffolds feature-based React / TypeScript architecture using feature folders, presentational + container components, custom hooks, a typed data layer, and structural CQRS (query hooks vs mutation hooks). React analog of dotnet-vertical-slice and python-feature-slice — no DI framework; uses props/context for dependency injection and a query cache for server state. Use when creating feature-based React projects, adding React features, organizing components by feature rather than by technical type, or scaffolding a feature's data layer. Triggers on phrases like "scaffold react feature", "create react slice", "react feature folder", "react vertical slice", "add react feature", "react feature architecture", "organize react by feature".