The Problem It Solves

The official Laravel upgrade guide is excellent and completely generic. It lists every breaking change in a release, and you have to work out which ones apply to your code. On a large project that is an afternoon of grepping, and the one you miss is the one that takes production down at 2am.

Preflight inverts it: instead of reading the whole guide and checking your code against it, you run one command and get back only the changes that actually appear in your codebase, with the file and line for each.

php artisan upgrade:check 13

Laravel Preflight is a free, MIT-licensed package covering 70+ documented breaking changes across Laravel 9, 10, 11, 12 and 13. It is read-only — it never modifies your code — and it has passed 2,400 downloads on Packagist.

Installing It

composer require jaydeep/laravel-preflight

The service provider is auto-discovered, so there is nothing to register. Requirements are deliberately wide, because a tool that tells you how to get off Laravel 8 has to run on Laravel 8:

RequirementSupported
PHP7.4 and 8.x
Laravel8 through 13
LicenseMIT

Running the Check

# Check against a specific target version
php artisan upgrade:check 13
php artisan upgrade:check 11

# Also write Markdown and HTML reports
php artisan upgrade:check 13 --report

Preflight reads your current version out of composer.json, so you only give it the target. Crucially it handles multi-version jumps in one run — going from 10 to 13 checks the 11, 12 and 13 changes together, which is what you want, because those intermediate breaks are exactly the ones people skip.

--report writes to storage/upgrade-report.md and storage/upgrade-report.html. The Markdown one is what I paste into the upgrade ticket as a checklist.

Reading the Output

Findings come with three severity levels, and the distinction is the useful part:

SeverityMeaningAction
CRITICALThis will break. The code cannot run on the target version.Fix before upgrading
WARNINGDeprecated, or behaviour changes silentlyFix during the upgrade
INFOWorth knowing — a new option or a changed defaultRead it

The exit code makes it scriptable:

  • 0 — compatible, nothing found.
  • 1 — issues detected.

The design goal was zero false positives — only flag things that are actually present in your code. A scanner that reports the entire upgrade guide is no better than the upgrade guide. If Preflight reports something you do not have, that is a bug worth filing.

What It Knows About

The changes people actually trip over, by release:

Laravel 9 (from 8)

PHP 8.0 requirement, SwiftMailer removed in favour of Symfony Mailer, fruitcake/laravel-cors deprecated, facade/ignition replaced, the model $dates property deprecated, Flysystem 3.x, dispatch_now() removed, mail config default key changes.

Laravel 10 (from 9)

PHP 8.1 requirement, Bus::dispatchNow() removed, assertDeleted() removed, $dates gone completely, Predis 2.x, native return types enforced, getQueueableRelations() return type.

Laravel 11 (from 10)

PHP 8.2 requirement, the slim skeleton — Http/Kernel.php and Console/Kernel.php removed — consolidated service providers, routes/api.php and routes/channels.php no longer auto-loaded, Carbon 3.x.

The Laravel 11 skeleton change is the one that catches the most people. If you customised app/Http/Kernel.php — and almost everyone has, to register middleware — that file no longer exists and your customisations have to move to bootstrap/app.php. Preflight flags every middleware registration it finds so you have the full list before you start.

Laravel 12 (from 11)

doctrine/dbal dropped, Model::reguard() removed, Response::json() strict validation, Collection::groupBy() key preservation, whereRelation() signature changes, Str::password() removed, spatie/laravel-ignition ^2.0.

Laravel 13 (from 12)

VerifyCsrfToken renamed to PreventRequestForgery, cache serializable_classes configuration, DB::upsert() validation changes, cache key prefix format updates, polymorphic pivot naming conventions, JobAttempted event property changes, array_first() and array_last() helper conflicts.

That last one is a good example of why a scanner beats a guide. If your project defines its own array_first() helper — which was common practice for years — Laravel 13 will now collide with it. Nothing in your code looks wrong. Preflight finds it.

How It Detects Things

Different breaking changes live in different places, so there are four analysers:

AnalyserLooks atFinds
CodeAnalyzerapp/, routes/, config/, database/, resources/, tests/Removed methods, renamed classes, changed signatures
ComposerAnalyzercomposer.jsonDeprecated and removed packages, version constraints
ConfigAnalyzerconfig/*.phpMissing, renamed or restructured config keys
EnvAnalyzer.env and .env.exampleNew required environment variables

The EnvAnalyzer is the quiet hero. New required environment variables are the classic “works on my machine, 500s in production” upgrade failure, because your local .env got updated during testing and the server’s did not.

Using It in CI

Running it on every pull request turns the upgrade from an event into a continuously known quantity:

# .github/workflows/upgrade-readiness.yml
name: Upgrade readiness

on:
  pull_request:
  schedule:
    - cron: '0 6 * * 1'      # every Monday morning

jobs:
  preflight:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
      - run: composer install --no-interaction --prefer-dist
      - run: php artisan upgrade:check 13 --report
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: upgrade-report
          path: storage/upgrade-report.html

Note the continue-on-error pattern you might want here: while you are still on the old version, a non-zero exit is expected. Let the job run and upload the report as an artifact without failing the build, then flip it to blocking once the count reaches zero.

A Sane Upgrade Workflow

  1. Run Preflight against the target version on your current code. Read the whole report before touching anything.
  2. Fix everything CRITICAL on the current version. Most breaking changes have a forward-compatible fix that works on both versions — swapping dispatch_now() for dispatchSync(), for example. Ship those in normal pull requests.
  3. Get the report to zero criticals. Now the actual version bump is a small change instead of a big one.
  4. Bump one major version at a time. 10 to 13 in one commit is a bad afternoon. 10 to 11, deploy, 11 to 12, deploy is boring — which is the goal.
  5. Run your test suite between each step, and re-run Preflight against the next target.

Step 2 is where the value is. Preflight lets you do the risky work incrementally, on a version you are already running, instead of all at once on a version you are not.

What It Will Not Catch

  • Third-party package incompatibility. Preflight checks your code and your composer.json constraints. Whether some package supports Laravel 13 yet is a question for composer why-not.
  • Behavioural changes with no syntax change. If a method keeps its signature but returns something subtly different, static analysis cannot see it. Your test suite has to.
  • Dynamically constructed calls. A method name assembled from a variable will not be matched.
  • Your own custom framework extensions. If you have overridden framework internals, you are outside what any general tool can reason about.

A clean Preflight report plus a green test suite is a genuinely good place to upgrade from. Neither alone is.