Files
timebank-cc-public/references/plans/LARAVEL_UPGRADE_PLAN_CLAUDE.md
T

13 KiB

Claude's plan: the next Laravel upgrade (11 → 12, then 13)

Author: Claude Code · Written 2026-10-15 · Status: proposal, not started

Written to compare with Mistral Vibe's plan (LARAVEL_UPGRADE_PLAN_VIBE.md). Every fact about this codebase below was checked on 2026-10-15 against branch laravel-11 (8e01ebc4) and Packagist. How: see Appendix.


1. Recommendation in short

  1. Go to Laravel 12 first, and plan Laravel 13 as a second project right after it. Laravel 12 can keep our heavily customised WireChat 0.2. Laravel 13 needs WireChat 0.6, the largest single risk in either upgrade. Doing them separately keeps each step small and each rollback simple.
  2. Do the risky preparation on Laravel 11 first, as normal small deploys: Carbon 3, removing two unused packages, and taking the abandoned flatpickr package into our own code. The Laravel 12 upgrade itself is then a small change.
  3. Reuse what worked for Laravel 11: scripts/upgrade.sh, a local rehearsal on a copy of the database, a rehearsal on a fictitious second platform, and a staged rollout from a separate branch.
  4. Timing matters. Laravel 11 has had no security fixes since March 2026. Laravel 12 gets security fixes only until 2027-02-24, so Laravel 13 has to follow soon after.

2. Where we stand

Production Laravel 11.57.0 since 2026-10-15, PHP 8.3.35, Livewire 3.8
Other instances to be listed (PHP version per server matters for Laravel 13)
Newest Laravel 12 12.70.0, needs PHP 8.2+, Carbon 3
Newest Laravel 13 13.36.0, needs PHP 8.3+, Carbon 3, Symfony 7.4/8
Support (verify on laravel.com/docs/master/releases) 11: security fixes ended March 2026 · 12: security fixes until 2027-02-24 · 13: about a year longer than 12

3. What blocks the upgrade, in this codebase

3.1 Packages

Most packages already have a release for Laravel 12 and 13 in the major version we use now (Livewire 3, Jetstream 5, Fortify, Sanctum, Scout 10, spatie permission 6, medialibrary 11, Reverb, WireUI 2, laravel-localization 2, dompdf, excel 3, and others). The exceptions:

Package Use here Laravel 12 Laravel 13 What to do
asdh/laravel-flatpickr <x-flatpickr> in 21 views; we already override its component class no release (stops at Laravel 11) no release Move it into the app (one class, 2 views, CSS/JS), drop the package
nesbot/carbon pinned at ^2.72 needs Carbon 3 Carbon 3 Upgrade on Laravel 11 first (Laravel 11 supports Carbon 3)
psr/simple-cache pinned at ^1.0 in our composer.json (left over from the skeleton) Laravel 12.29+ needs ^3.0 ^3.0 Remove the pin or set ^3.0
robsontenorio/mary not used (only config/mary.php) needs 2.x needs 2.x Remove the package and its config
laradumps/laradumps (dev) no ds() calls needs 5.x needs 5.x Remove
cviebrock/eloquent-sluggable, -taggable 24 files 12.x 13.x Version bump (these majors only follow Laravel)
nunomaduro/collision (dev) test runner 8.6+ (we have 8.5) 8.x Update within 8.x
staudenmeir/* (3 packages) relations newer minor newer minor Update within the current line
namu/wirechat chat; 45 override and published view files 0.2.11 works 0.6 required Laravel 13 only: a project of its own
laravel/tinker, barryvdh/laravel-debugbar (dev) current line works new major Laravel 13 only

3.2 Carbon 3: the main code risk

Carbon 3 changed diffIn*(): the results are now signed (negative when the argument is in the past) and floats. Laravel 11 accepts Carbon 3, so this can be fixed and deployed before Laravel 12.

  • 28 diffIn*() calls in 20 files need review.
    • Example that would break: now()->diffInSeconds(Carbon::parse($lastLoginAt)) in InactiveProfileWarning2Mail and InactiveProfileWarningFinalMail becomes negative.
    • Calls that already pass false (signed) or cast to (int) need a check for the float, not the sign.
  • 172 Carbon::parse and 45 Carbon::now calls: mostly unaffected, but run the full suite and look at date displays (transaction lists, statements, inactivity mails, call expiry).

3.3 Laravel 12 upgrade-guide items checked against the code

Change in Laravel 12 Here
Carbon 3 required yes, see 3.2
image validation rule excludes SVG not affected: the one upload rule uses mimes:jpg,jpeg,png,svg
local disk default root moves to storage/app/private not affected: config/filesystems.php sets root => storage_path('app')
HasUuids generates UUIDv7 not used
mergeIfMissing with nested arrays not used
Concurrency result keys not used
Schema::getTables() and similar, multi-schema not used
DatabaseTokenRepository expiry in seconds no custom repository
Route precedence for duplicate names, container default values, Str::is with newlines low; covered by the test suite and smoke tests

To do in Phase 2: read the full official guide once more, and diff the laravel/laravel skeleton 11 → 12 (config files) against ours.

3.4 Tests

  • 466 test methods, 656 tests including data sets, green on Laravel 11.
  • PHPUnit 10.5 can stay for Laravel 12.
  • 20 test files still use @test docblock annotations. They work on PHPUnit 10 and 11, but PHPUnit 12 ignores them, so converting them to #[Test] attributes is preparation for later.

4. Phases

Phase 0: Before starting (no code changes)

  1. Finish the Laravel 11 follow-ups: a quiet prod log, main fast-forwarded to laravel-11, snapshot removed (see the post-upgrade reminder in shared memory).
  2. List every instance and its PHP version. Laravel 13 needs PHP 8.3 everywhere.
  3. Decide: Laravel 12 then 13 (recommended), or straight to 13 (see section 6).

Phase 1: Preparation on Laravel 11 (small changes, normal deploys)

Each step is its own commit on main, tested with the full suite, and deployed with the normal deploy.sh, so production gets smaller changes one at a time instead of one big jump.

  1. Carbon 3.
    • nesbot/carbon to ^3.8.
    • Review all 28 diffIn*() calls: keep the meaning ("how long ago" stays positive, whole numbers where shown to people), with abs() and (int) or floor() where needed.
    • Add tests for the inactivity mails and call expiry, which compute durations.
  2. Composer clean-up.
    • Remove the psr/simple-cache pin.
    • Remove robsontenorio/mary (and config/mary.php) and laradumps/laradumps.
  3. Flatpickr into the app.
    • Move the component (our override in app/Overrides/Asdh), its two views and its CSS/JS into the app.
    • Register it under the same name flatpickr, so the 21 views keep <x-flatpickr> unchanged.
    • Remove the package, the override and its classmap entry.
    • Test every date picker: transactions table, select period, profile form, posts, calls.
  4. Updates within the current lines.
    • Collision to 8.6+, the staudenmeir packages, and the newest Livewire 3.8.x.
    • composer update within the constraints, then the full suite.

Phase 2: Laravel 12 (branch laravel-12, staged rollout)

  1. Branch laravel-12 from main once Phase 1 is on main.

  2. Composer:

    • laravel/framework ^12.0, cviebrock/eloquent-sluggable ^12.0, cviebrock/eloquent-taggable ^12.0.
    • composer update -W. Write down every major version that changed in the lock file.
  3. Upgrade guide and skeleton: apply section 3.3, and diff the Laravel 11 and 12 skeleton config files against ours.

  4. Our overrides. For every package with an override or published views (Jetstream HasProfilePhoto, Matchish, WireChat views, WireUI), diff the vendor original between the old and new version and port the changes, as was done for WireChat 0.2.11.

  5. Tests:

    • Full suite green.
    • Render the main Livewire pages.
    • Smoke test by hand: log in, switch profile, payment, chat with an image, poll, search, mailing preview, PDF statement, date pickers.
    • All 4 themes.
  6. Rehearsals with scripts/upgrade.sh:

    • a clone of main with a copy of the database (success path, rollback before and after going live)
    • the fictitious Uuro platform (TIMEBANK_CONFIG=uuro, own config, own theme)
    • a routine deploy afterwards

    The script is version-neutral. It reads the PHP requirement from the target lock file, but check its texts ("Laravel 10 -> 11") and the version check in step 6.

  7. Docs:

    • references/UPGRADE_LARAVEL_11_TO_12.md (the 10 → 11 guide with the changes)
    • versions in AGENTS.md
    • the post-upgrade checklist
  8. Rollout:

    • the development server first, then production, then other instances: --target origin/laravel-12
    • fast-forward main once all instances are upgraded or ready

Phase 3: Laravel 13 (separate project, start right after Laravel 12 is live)

  1. PHP 8.3 on every instance (production already has 8.3.35).
  2. WireChat 0.2 → 0.6, the biggest item:
    • Read the changelogs of 0.3 to 0.6.
    • Re-port our 45 override and view files.
    • Check our _finishUpload override (Livewire 3.8 signed uploads), private attachments and the listeners that work across profile switches.
    • Extend the chat tests first, so regressions show.
    • This can be prepared on Laravel 12 if WireChat 0.6 also supports 12 (it does: ^11|^12|^13).
  3. Package majors: tinker 3, debugbar 4 (dev), sluggable and taggable 13; check Symfony 7.4/8, Monolog 3.10 and Guzzle.
  4. Read the official 12 → 13 upgrade guide. I do not list its items from memory here; they have to be checked against the code, as in 3.3.
  5. Same method as Phase 2: branch laravel-13, rehearsals, staged rollout.
  6. Finish before 2027-02-24, when Laravel 12 gets its last security fixes.

5. Risks and how to limit them

Risk Impact Limit it by
Carbon 3 changes durations silently (negative or fractional) Wrong texts in mails ("-3 days ago"), wrong expiry decisions Phase 1 on Laravel 11 with tests per diffIn*() call; check mails per theme
Flatpickr removal breaks date pickers Forms where a date cannot be entered Same tag name; test every page with a picker
A package's new major changes behaviour (sluggable, taggable) Wrong slugs or tags Read their changelogs; tests on posts, tags, calls
Override drift (vendor views and classes) Chat or profile photo regressions Diff vendor originals old to new, as for WireChat 0.2.11
WireChat 0.6 (Laravel 13) Chat broken Separate phase, tests first, prepare on Laravel 12
A server with older PHP Laravel 13 cannot run there List PHP versions in Phase 0; upgrade.sh refuses too old PHP
A long-running branch drifts from main Rebase conflicts during the rollout Short phases; do not rebase after a server upgraded from the branch
Two AI agents in one folder Conflicting edits during the upgrade Worklog entries; one agent per phase step; the user dispatches

6. Alternatives considered

  • Straight to Laravel 13 in one project. One rollout instead of two, and the longest support. But it combines Carbon 3, all package majors, the Laravel 12 and 13 framework changes and WireChat 0.6 in one step, so a failure is harder to pin down and a rollback undoes more. Reasonable only if Phase 1 is done and WireChat 0.6 has been tried separately first, so in practice it is the same work in a different order.
  • Laravel 12 only, and stay there. Its security support ends 2027-02-24. Not an end state.
  • Doing Phase 1 inside the Laravel 12 branch. Possible, but then production gets Carbon 3, flatpickr and the framework at once. Phase 1 on Laravel 11 gives smaller, easier-to-check production changes.

7. Effort (working time, rough)

Phase Effort
0 an hour (an inventory and a decision)
1 1 to 2 days (Carbon review is most of it), plus normal deploys
2 2 to 3 days, including rehearsals, plus a rollout evening per instance
3 3 to 5 days, mainly WireChat 0.6, plus rehearsals and rollout

8. Open questions for the user

  1. Which instances exist besides timebank.cc, and on which PHP version?
  2. Is there still a development server to upgrade first?
  3. Agree with Laravel 12 first and 13 straight after, or prefer straight to 13?
  4. Is Mary perhaps planned for future UI work? (It is not used now.)

Appendix: how the facts were checked

  • Package support:
    • The Packagist metadata of every package in composer.json was read with .agents/archive/laravel-compat-check.py (output: .agents/archive/laravel-compat-2026-10-15.txt).
    • For each package it checks the newest version supporting Laravel 12 and 13, overall and within the major version in use.
  • Dependency solving: composer update --dry-run -W in a copy of composer.json/composer.lock with laravel/framework ^12.0 and nesbot/carbon ^3.8. It stopped at cviebrock/eloquent-sluggable (needs 12.x) and psr/simple-cache (^1.0 pin versus Laravel 12.29+).
  • Code impact: grep counts in app/, resources/views/, routes/, database/ and tests/:
    • <x-flatpickr>: 21 files
    • diffIn*(): 28 calls in 20 files
    • Mary: no use
    • ds(): none
    • WireChat overrides: 45 files
    • @test annotations: 20 files
  • Not verified here: Laravel 13's exact upgrade-guide items and support end date (to be read from the official documentation in Phase 3).