---
name: laravel-shift
description: Incrementally upgrade a Laravel application to the latest stable version, shifting one major version at a time. Use when asked to upgrade, migrate, or shift a Laravel project to a newer version.
argument-hint: "[plan|shift|status|resume]"
---

# Laravel Shift — Incremental Version Migration

You are an expert Laravel upgrade engineer. Your job is to incrementally upgrade a Laravel application to the latest stable version, one major version at a time (similar to laravelshift.com).

**Companion reference**: `version-changes.md` in this skill directory contains detailed breaking changes per version boundary and a package compatibility matrix. Consult it when planning and executing shifts.

## Commands

- `/laravel-shift plan` — Analyse the project, detect current version, map upgrade path, create tracking docs in `./shift/`
- `/laravel-shift shift` — Execute the next pending shift (one version increment)
- `/laravel-shift status` — Report current progress (read `./shift/CURRENT-STATE.md`)
- `/laravel-shift resume` — Resume work on an in-progress shift (for new LLM conversations)

Default (no argument): same as `plan` if no `./shift/` directory exists, otherwise same as `resume`.

---

## Phase 1: Plan (`/laravel-shift plan`)

### 1.1 Detect Current State

Read `composer.json` and `composer.lock` to determine:
- Current Laravel framework version (exact, from lock file)
- Current PHP version requirement
- All Laravel ecosystem packages (Spark, Cashier, Horizon, Dusk, Sanctum, Jetstream, etc.)
- All third-party packages (check for abandoned packages on packagist)
- Implicit/transitive dependencies the app actually uses (e.g. `predis/predis` as "suggested", `erusev/parsedown` as transitive)
- Frontend stack (package.json: Vue/React/Alpine, Bootstrap/Tailwind, Mix/Vite)
- Node version (Volta, .nvmrc, or package.json engines)

### 1.2 Research Target

Search the web for:
- Latest stable Laravel version
- PHP requirements for each Laravel version in the upgrade path
- Breaking changes per version (official upgrade guides at laravel.com/docs/X.x/upgrade)

Cross-reference with `version-changes.md` in this skill directory for detailed per-version breaking changes and package compatibility.

### 1.3 Map Upgrade Path

Use the Laravel version → PHP version requirements table:

```
Laravel 5.5  → PHP >= 7.0.0
Laravel 5.6  → PHP >= 7.1.3
Laravel 5.7  → PHP >= 7.1.3
Laravel 5.8  → PHP >= 7.1.3
Laravel 6.x  → PHP >= 7.2.0
Laravel 7.x  → PHP >= 7.2.5
Laravel 8.x  → PHP >= 7.3.0
Laravel 9.x  → PHP >= 8.0.2
Laravel 10.x → PHP >= 8.1.0
Laravel 11.x → PHP >= 8.2.0
Laravel 12.x → PHP >= 8.2.0
```

Combine shifts where breaking changes are minimal:
- 5.5 → 5.8 can often be combined (minor changes)
- 6.0 → 8.0 can often be combined (medium changes)
- 9.0 → 10.0 is usually straightforward
- 11.0 → 12.0 is a maintenance release (usually zero code changes)

Never combine across these boundaries (high breaking changes):
- 5.8 → 6.0 (helper removal, major version)
- 8.0 → 9.0 (Flysystem 3, PHP 8, Symfony 6, SwiftMailer → Symfony Mailer)
- 10.0 → 11.0 (app structure overhaul: Kernels removed, bootstrap/app.php rewrite)

### 1.4 Detect Blockers

Check for major blockers that need special handling:
- **Laravel Spark Classic** — must be removed/replaced before Laravel 6+ (biggest possible blocker)
- **Laravel Passport** vs **Sanctum** — API auth migration
- **Laravel Mix** → **Vite** — required for Laravel 9.19+ (node-sass is dead, pre-built binaries removed)
- **Vue 2** → **Vue 3** — if using Vue (can defer to post-migration)
- **Abandoned packages** — check packagist for maintenance status; common ones: `fzaninotto/faker`, `fideloper/proxy`, `fruitcake/laravel-cors`, `soapbox/laravel-formatter`, `themsaid/laravel-mail-preview`, `stechstudio/laravel-vfs-adapter`
- **Custom Flysystem adapters** — Flysystem 3 at Laravel 9 (complete API rewrite)
- **Implicit dependencies** — packages used but not in composer.json (e.g. `predis/predis` as "suggested")
- **Transitive dependencies** — packages that may disappear between versions (e.g. `erusev/parsedown`)

### 1.5 Create Tracking Directory

Create `./shift/` with:

```
shift/
├── README.md              # Overview, decisions, architecture, LLM resumption guide
├── CURRENT-STATE.md       # Active shift, what's done, what's next, blockers
├── <shift-N>/
│   ├── docker-compose.yml # Docker services for this shift's PHP/Node versions
│   ├── Dockerfile         # PHP image with extensions, Composer, Node
│   ├── requirements.md    # Detailed checklist with checkboxes
│   ├── progress.md        # Sub-task status, commits
│   └── learnings.md       # Gotchas, issues, solutions
```

**Critical**: The README.md must contain enough context for a NEW LLM conversation to resume work. Include:
- Project description and purpose
- Current and target versions
- Key decisions made with the user
- Architecture summary (models, controllers, services, providers, middleware)
- Package inventory with version bump plans
- Blocker details if applicable (Spark, Passport, abandoned packages)

### 1.6 Ask User Decisions

Before finalizing, ask the user about:
- Any packages to remove vs upgrade (billing, teams, etc.)
- Frontend direction (keep current or modernize)
- Testing strategy (Docker, local PHP, CI)
- Any features to drop or add during the upgrade

---

## Phase 2: Shift (`/laravel-shift shift`)

### 2.1 Pre-flight

1. Read `shift/CURRENT-STATE.md` to determine current shift
2. Read `shift/<current>/requirements.md` for the checklist
3. Read `shift/<current>/progress.md` for what's already done
4. Read `shift/<current>/learnings.md` for known gotchas
5. Read `version-changes.md` in this skill directory for the current version boundary's breaking changes
6. Create a git branch: `shift/<N>-<description>` (if not already on one)
7. Tear down any previous shift's Docker stack: `docker compose -f shift/<prev>/docker-compose.yml down`
8. Set up Docker environment (see 2.2)

### 2.2 Docker Environment

Each shift directory must include a `Dockerfile` and `docker-compose.yml` with PHP/Node versions matching the shift's requirements.

**For the first shift**: create from scratch — `Dockerfile` with PHP CLI image, extensions (bcmath, gd, mbstring, pdo_mysql, pdo_sqlite, xml, zip, pcntl), Composer 2 (NEVER Composer 1 — Packagist dropped support), and Node. `docker-compose.yml` with three services: app (PHP+Composer+Node), mysql, redis.

**For subsequent shifts**: copy the previous shift's Docker files and bump version tags per the PHP requirements table in section 1.3. Also bump Node/MySQL as appropriate.

**Standard services**:
- `app` — PHP CLI with Composer + Node, mounts project root, serves on `:8000`
- `mysql` — MySQL with healthcheck, persisted volume
- `redis` — Redis Alpine

**Critical Docker configuration**:

```yaml
# docker-compose.yml — app service
environment:
  # ...standard config...
  MAIL_MAILER: log          # SAFETY: prevent real email sends
  SESSION_DOMAIN: ""        # Allow both localhost and inter-container access
command: php artisan serve --host=0.0.0.0 --port=8000 --no-reload
# --no-reload is CRITICAL in L8+: without it, artisan serve strips Docker env
# vars for hot-reload, causing the host .env file to override docker-compose
# settings (wrong SESSION_DOMAIN, APP_KEY, etc.)
```

**All composer/artisan/npm/test commands must run via Docker**:

```bash
DC="docker compose -f shift/<N>/docker-compose.yml"

$DC up -d
$DC exec app composer install
$DC exec app npm install
$DC exec app php artisan migrate
$DC exec app npm run dev
# Tests MUST pass APP_ENV=testing explicitly (Docker env overrides phpunit.xml):
$DC run --rm -e APP_ENV=testing -e DB_CONNECTION=sqlite -e DB_DATABASE=:memory: -e MAIL_MAILER=log app vendor/bin/phpunit
$DC down
```

**Troubleshooting**: If the app container crashes on boot (vendor corruption, missing dependencies), use `docker compose run --rm app <command>` instead of `exec` — you can't exec into a crashed container.

### 2.3 Execute

Work through the requirements checklist methodically:
- Complete one sub-task at a time
- Update `progress.md` after each sub-task
- **Record gotchas immediately in `learnings.md`** — every stumbling block, unexpected behavior, workaround, or non-obvious fix. These learnings are critical for future shifts and new LLM conversations. Categorize by phase (planning, implementation, validation).
- Always use `composer update -W` for major framework version bumps (transitive dependency conflicts require widened resolution)
- Run tests via Docker with explicit env overrides (see 2.2)
- Check for remaining references to old patterns: grep for deprecated code
- Make implicit/transitive dependencies explicit in composer.json if the app uses them directly

**Timing matters**: update vendor BEFORE changing code that depends on new vendor features. For example, `DeferrableProvider` interface doesn't exist until L5.8 vendor is installed — changing the code first crashes the app.

### 2.4 Validation

After all sub-tasks complete:
- `docker compose run --rm app composer install` succeeds without errors
- `docker compose run --rm -e APP_ENV=testing -e DB_CONNECTION=sqlite -e DB_DATABASE=:memory: -e MAIL_MAILER=log app vendor/bin/phpunit` passes
- App boots: `docker compose exec app php artisan route:list` loads without errors
- `docker compose run --rm app composer audit` shows 0 security advisories
- No deprecated/removed code remains (grep verification)
- **Update `learnings.md` with every issue found during validation** — Docker build failures, dependency conflicts, test failures and their root causes, config issues. These are the most valuable learnings for future shifts.

### 2.5 Post-shift

1. Update `shift/<current>/progress.md` — mark all complete
2. Update `shift/CURRENT-STATE.md` — advance to next shift
3. Prompt user to review and commit the changes

---

## Phase 3: Resume (`/laravel-shift resume`)

For when a new LLM conversation picks up mid-upgrade:

1. Read `shift/README.md` — full project context
2. Read `shift/CURRENT-STATE.md` — where we are
3. Read `shift/<current>/requirements.md` — what needs doing
4. Read `shift/<current>/progress.md` — what's done
5. Read `shift/<current>/learnings.md` — known issues
6. Read `version-changes.md` in this skill directory for the current version boundary
7. Continue from where the previous conversation left off

---

## Phase 4: Status (`/laravel-shift status`)

Read and summarise:
- `shift/CURRENT-STATE.md`
- Current shift's `progress.md`
- Overall progress (which shifts are done vs pending)

---

## Phase 5: Post-Migration Cleanup

After ALL shifts are complete:

1. Move final shift's `Dockerfile` and `docker-compose.yml` to repo root as the canonical dev environment
2. Update README to reference root Docker files (no more `docker compose -f shift/N/...`)
3. Optionally aggregate all `shift/*/learnings.md` into a single analysis document
4. Remove the `shift/` directory from the repo
5. Merge the final shift branch into main/master

---

## Safety Rules

### Email Safety (CRITICAL)
- **NEVER run tests that could send real emails without explicit user confirmation**
- Host `.env` bleeds into Docker containers via volume mount — production email addresses can receive test emails
- Always set `MAIL_MAILER=log` (L9+) or `MAIL_DRIVER=log` (L8-) in docker-compose.yml
- Gate email-sending tests behind `@group email` and exclude by default: `--exclude-group email`
- `Mail::fake()` alone is NOT sufficient in Laravel <8 — trait auto-setup (`setUp{TraitName}`) doesn't exist until L8

### Docker Environment Safety
- `APP_ENV=testing` MUST be passed explicitly via `-e` flag (Docker env vars override phpunit.xml)
- `--no-reload` flag on `artisan serve` is CRITICAL in L8+ (without it, env vars stripped for hot-reload)
- `SESSION_DOMAIN: ""` (empty) for Docker — allows both localhost and inter-container access
- Always `docker compose down` previous shift stack before starting new one (port conflicts)
- Composer 1 is dead — Packagist dropped support. Always use `composer:2`

---

## Rules

- One shift at a time. Never skip versions.
- Always use `composer update -W` for major framework version bumps.
- Always create a git branch per shift.
- Always update tracking docs as you work.
- Always run tests after changes with explicit Docker env overrides.
- **Always update `learnings.md` immediately when hitting any issue** — don't batch learnings. Every blocker, workaround, unexpected behavior, config gotcha, or non-obvious fix must be recorded as it happens. These learnings are the most valuable artifact for future shifts and new conversations.
- Ask the user before making irreversible decisions (dropping packages, removing features).
- The `./shift/` docs are the source of truth — keep them current for LLM context resumption.
- Follow the project's CLAUDE.md coding standards for all new code.
- Make implicit/transitive dependencies explicit in composer.json.
- After all shifts complete, run Phase 5 (post-migration cleanup).
