Skip to content

Upgrade Laravel 10 → 13#854

Open
edwh wants to merge 25 commits into
developfrom
issue-853
Open

Upgrade Laravel 10 → 13#854
edwh wants to merge 25 commits into
developfrom
issue-853

Conversation

@edwh
Copy link
Copy Markdown
Collaborator

@edwh edwh commented May 20, 2026

Summary

  • Upgrades Laravel framework sequentially: 10 → 11 → 12 → 13
  • Updates PHP minimum requirement to ^8.3 (required by Laravel 13)
  • All 493 PHPUnit tests pass locally and in CI
  • CI fully green (PHPUnit + Jest + Playwright) ✅

Laravel upgrade changes

Laravel 10 → 11

  • Updated laravel/framework to ^11.0
  • Added APP_KEY to phpunit.xml (L11 bootstrap requirement)
  • Removed Doctrine DBAL enum mapping from migration (API removed in L11)
  • Updated various packages to their L11-compatible versions

Laravel 11 → 12

  • Updated laravel/framework to ^12.0
  • Upgraded toin0u/geocoder-laravel from ^4.6 to ^13.0 (v13 supports L12+)
  • Upgraded mariuzzo/laravel-js-localization from ^1.10 to ^2.0
  • Added laravel-discourse-sso fork (upstream constrained to illuminate/auth ≤11)
  • Added symfony/psr-http-message-bridge: ^7.0 pin (prevents 8.x which needs PHP 8.4)
  • Removed nunomaduro/collision (conflicts with L12; no v9 exists)

Laravel 12 → 13

  • Updated laravel/framework to ^13.0
  • Updated PHP requirement to ^8.3 (L13 minimum)
  • Upgraded egulias/email-validator to ^4.0.0 (L13 requires ^4)
  • Upgraded barryvdh/laravel-debugbar to ^4.0
  • Upgraded laravel/tinker to ^3.0
  • Upgraded msurguy/honeypot to ^1.5
  • Upgraded barryvdh/laravel-translation-manager to ^0.6.9
  • Upgraded spatie/laravel-validation-rules to ^3.4.4
  • Added laravel-eloquent-query-cache fork (upstream constrained to illuminate/database ≤12)
  • Updated discourse-sso fork to v2.9.3 (adds ~13 to illuminate constraints)
  • Fixed config/geocoder.php: replaced removed Http\Client\Curl\Client with new Geocoder\Laravel\Http\LaravelHttpClient adapter; added auto_register_serializable_classes for L13 cache hardening

Forked packages (temporary — PRs submitted upstream)

The following packages did not yet have official Laravel 13 support at the time of this upgrade. Temporary forks add the necessary version constraints:

Note: laravel-discourse-sso and laravel-eloquent-query-cache VCS sources should be moved from edwh to TheRestartProject once org forks are created. laravel-drip already uses the org fork.

CI infrastructure fixes

Getting CI green required several fixes beyond the Laravel upgrade itself:

PHP 8.4 compatibility

  • guzzlehttp/promises 1.x has implicit nullable parameters deprecated in PHP 8.4
  • Patch moved into patches/composer.patches.json so cweagans/composer-patches applies it during composer install (removed the shell-level patch call from docker_run.sh)
  • Disabled convertDeprecationsToExceptions in PHPUnit config so third-party deprecation warnings don't cause test failures

Docker startup sequencing

  • Moved php-fpm start to after migrations complete (previously npm/playwright installs blocked it)
  • Pre-build Vite assets on host before Docker starts so public/build/manifest.json exists when php-fpm first serves requests
  • Background npm install and Playwright browser download (not needed until e2e tests run)

MySQL 8.0 trigger creation

  • MySQL 8.0 enables binary logging by default; creating triggers requires log_bin_trust_function_creators = 1
  • Added this to mysql/my.cnf so it applies at MySQL startup, before migrations run (previously only set in the "Setup application" CI step which runs after the health check)
  • The trigger migration also attempts SET GLOBAL log_bin_trust_function_creators = 1 at startup (best-effort for production, where the DB user lacks SUPER)
  • Production note: log_bin_trust_function_creators = 1 must be set in restarters-db.internal my.cnf before this migration runs in production

Stats cache serialization (PHP-FPM specific)

  • In CI with PHP-FPM, Cache::get() was returning __PHP_Incomplete_Class for stdClass objects stored via DB::select(), causing E_WARNING → ErrorException → HTTP 500 on every request
  • Fixed Fixometer::computeStats() to store plain arrays instead of raw DB stdClass objects; isStatsValid() now enforces the array format so any old-format cache is recomputed on first access

CircleCI vendor/ cache

  • Scoped composer cache key to exact composer.lock checksum; removed the loose fallback key that could restore vendor/ from a different branch

Test plan

  • All 493 PHPUnit tests pass locally on Laravel 13
  • CI passes on CircleCI with PHP 8.4 (build 4997: PHPUnit ✅ Jest ✅ Playwright ✅)
  • Manual smoke test of key flows (login, event creation, device recording)
  • Production: set log_bin_trust_function_creators = 1 in restarters-db.internal my.cnf before deploy

edwh and others added 25 commits May 19, 2026 17:38
- Remove laravelcollective/html: replace Form::hidden with native HTML in 4
  profile blade files (the only usage in the entire codebase)
- Remove twbs/bootstrap from composer (dead dep — Bootstrap comes from npm)
- Bump PHP requirement to ^8.3 in composer.json
- Upgrade Dockerfile and Dockerfile.fly from PHP 8.2 to 8.4

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bump laravel/framework to ^11.0, with all compatible dependency versions
- Remove laravelcollective/html (unused Form::hidden replaced with plain HTML)
- Remove bkwld/croppa (no code references), osteel/openapi-httpfoundation-testing (unused)
- Add wouternl/laravel-drip fork (edwh/laravel-drip) for L11 support
- Fix config/sentry.php: replace closure in before_send with App\Support\SentryBeforeSend::handle
- Fix config/audit.php: v14 requires user.resolver key and 'resolvers' (not 'resolver')
- Fix tests/TestCase.php: remove ValidatorBuilder (package removed)
- Fix tests/Feature/Swagger/SwaggerGenerateTest.php: correct namespace/class name
- Remove empty placeholder test files (EventKeyTest, GroupByKeyTest)
- Upgrade PHP to 8.4 in both Dockerfiles
- Bump nunomaduro/collision to ^8.0, laravel/dusk to ^8.0 for L11 compat
L11 throws MissingAppKeyException at boot if APP_KEY is absent.
getDoctrineSchemaManager() was removed from Laravel 11. The Doctrine
DBAL enum workaround is no longer needed; Schema::table()->change()
works natively in L11 without it.
- Update laravel/framework to ^12.0
- Upgrade toin0u/geocoder-laravel from ^4.6 to ^13.0 (L12 support)
- Upgrade mariuzzo/laravel-js-localization from ^1.10 to ^2.0 (L12 support)
- Add edwh/laravel-discourse-sso fork (adds ~12 to illuminate constraints)
- Add symfony/psr-http-message-bridge ^7.0 (prevents 8.x selection, needs PHP 8.4)
- Remove nunomaduro/collision (conflicts with L12, no v9 exists yet)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Update laravel/framework to ^13.0
- Upgrade php requirement to ^8.3 (L13 minimum)
- Upgrade egulias/email-validator to ^4.0.0 (L13 requires ^4)
- Upgrade barryvdh/laravel-debugbar to ^4.0 (L13 support)
- Upgrade laravel/tinker to ^3.0 (L13 support)
- Upgrade msurguy/honeypot to ^1.5 (L13 support)
- Upgrade barryvdh/laravel-translation-manager to ^0.6.9 (L13 support)
- Upgrade spatie/laravel-validation-rules to ^3.4.4 (L13 support)
- Add edwh/laravel-eloquent-query-cache fork (adds ^13.0 illuminate constraint)
- Update discourse-sso fork to v2.9.3 (adds ~13 illuminate constraints)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace Http\Client\Curl\Client with Geocoder\Laravel\Http\LaravelHttpClient
  (php-http/curl-client was dropped in geocoder-laravel v13)
- Add auto_register_serializable_classes for Laravel 13 cache hardening

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migrate phpunit.xml to current schema (--migrate-configuration)
- Make 14 data provider methods static (required by PHPUnit 10+)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Make 18 more data provider methods static (required by PHPUnit 10+)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…mpat

guzzlehttp/promises 1.x uses implicit nullable types that PHP 8.4 deprecates.
We can't upgrade promises because addwiki/mediawiki-api-base 3.1 constrains
it to ~1.0. Setting convertDeprecationsToExceptions=false stops PHPUnit from
treating these vendor deprecations as test failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…types

PHP 8.4 deprecated implicit nullable parameter types (e.g. `Type $p = null`).
guzzlehttp/promises 1.x uses this pattern in functions.php, causing E_DEPRECATED
notices that PHPUnit converts to test failures on PHP 8.4 CI.

addwiki/mediawiki-api-base 3.1 constrains guzzle-promises to ~1.0 so we
can't upgrade to 2.x. Apply a composer patch instead to add explicit `?Type`
nullable annotations to the four affected functions.

Also reverts the ineffective convertDeprecationsToExceptions=false change
(PHPUnit 10 removed that option; the real issue was in the patch layer).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PHP 8.4 deprecated implicit nullable parameter types (e.g. `Type $p = null`).
guzzlehttp/promises 1.x uses this pattern in functions.php, which is loaded
as a Composer autoload_files entry -- i.e. on every request when
vendor/autoload.php is included, before any PHPUnit or Laravel error handler
is registered.

addwiki/mediawiki-api-base 3.1 constrains guzzle-promises to ~1.0 so we
can't upgrade to 2.x. Instead, use a custom bootstrap.php that temporarily
removes E_DEPRECATED from error_reporting during the autoload phase only,
then restores the original level. This silences the vendor deprecations
without affecting detection of our own code's deprecations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…types

The bootstrap.php approach (build 4980) didn't suppress the deprecations
for subprocess-isolated tests. PHP 8.4 emits E_DEPRECATED for implicit
nullable types when functions are called, not just when defined, so each
test triggers the deprecation again after PHPUnit's error handler is set.

Use cweagans/composer-patches to apply explicit nullable types directly
to guzzlehttp/promises 1.5.3 src/functions.php. Previous attempt (build
4979) failed because the patch used wrong paths (vendor-relative instead
of package-relative). This patch uses a/src/functions.php paths,
compatible with patch -p1 from the package directory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r-patches

cweagans/composer-patches uninstalls and reinstalls guzzlehttp/promises
(and cascading dependents) before applying the patch, adding ~8 minutes
to container startup. This causes the 10-minute Wait for services timeout.

Apply the patch directly via `patch` after `composer install` instead,
bypassing the reinstall overhead entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
npm install + playwright browser download (~300MB) are slow on loaded
networks and were blocking php-fpm from starting, causing the 10-minute
health check to time out. These don't need to be done before php-fpm
can serve HTTP — run them in background subshells instead.

php-fpm now starts immediately after migrations + artisan setup.
By the time Playwright tests run (after 45min of PHPUnit), npm and
browser downloads will have long since finished.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rtifact

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CircleCI restore_cache restores vendor/ before docker compose up. The
container volume-mounts ./vendor/ so composer install is fast (just
verifies packages). save_cache stores vendor/ after Wait for services.
The guzzle patch uses || true so re-applying to a cached vendor is safe.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When npm is backgrounded in docker_run.sh, neither public/hot nor
public/build/manifest.json exists when php-fpm starts (~2min into startup).
Every page request triggers a ViteManifestNotFoundException -> HTTP 500,
so the health check never passes.

Fix: run npm install + npm run build on the CI host BEFORE starting Docker.
public/build/manifest.json then exists in the volume-mounted workspace from
the moment php-fpm starts handling requests.

Also add node_modules caching so subsequent runs are fast (seconds vs minutes).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MySQL 8.0 enables binary logging by default, which requires SUPER privilege
to create triggers. The restarters user lacks SUPER, so migrate:fresh --seed
was failing with SQLSTATE 1419, leaving the DB empty and causing HTTP 500.

Moving log_bin_trust_function_creators=1 into mysql/my.cnf ensures it's
applied at MySQL startup, before docker_run.sh runs migrations. Previously
it was only set in the CI "Setup application" step — but that step runs
after "Wait for services", which never completed due to the migration failure.

Also capture storage/logs/laravel.log as a CI artifact for future debugging.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In CI (PHP-FPM), the file cache was returning __PHP_Incomplete_Class for
stdClass objects in waste_stats[0], causing E_WARNING → ErrorException →
HTTP 500 on every health check request.

Root fix: computeStats() now stores waste_stats and device_count_status as
plain PHP arrays instead of DB stdClass objects. No class deserialization
needed at all.

isStatsValid() now also checks waste_stats[0] is an array, so any cached
data in old format (stdClass) is treated as invalid and recomputed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixometer::loginRegisterStats(): if Cache::put() fails after the cold-start
lock (disk full, backend down), Cache::get() returns null and
decorateStats(null) would hit a TypeError. Now falls back to computing
uncached.

decorateStats(): add ?? [] guard on waste_stats[0] access so a corrupt
cache entry returns zeros rather than a fatal error.

Trigger migration: attempt SET GLOBAL log_bin_trust_function_creators = 1
before creating triggers; silently catches SUPER-privilege denial so the
migration still runs on hosts where the flag is already set in my.cnf (e.g.
production).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The runtime `patch` call in docker_run.sh was a workaround: cweagans/composer-patches
already manages the other vendor patches, so this one should live there too.
cweagans applies patches on fresh installs (cache misses in CI) and the cached
vendor already carries the patch from prior builds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
edwh/laravel-drip was a personal fork. TheRestartProject/laravel-drip
is the org-owned fork, currently at the Laravel 10 support commit.
The existing laravel-drip-l11 composer patch supplies the L11/12/13
constraint, so behaviour is unchanged — just a cleaner provenance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
B Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant