Skip to main content

Cloudsoft Zone Build, deploy and scale with practical guides to PHP, Node.js, React, DevOps and AI automation. Explore the guides

Cloudsoft Zone
PHPDevelopmentDevOpsLaravelTutorial

Upgrading a Laravel app to PHP 8.5: compatibility, deprecations and tests

Move a Laravel 12 or 13 app to PHP 8.5 safely: supported versions, the deprecations that bite, Composer and OPcache changes, Rector and PHPStan settings, CI and rollout.

Cloudsoft Zone

Updated 13 min read

Diagram of five gated stages for a Laravel PHP 8.5 upgrade: inventory, packages, analysis, tests on 8.4 and 8.5, and a canary rollout
Image: Cloudsoft Zone
In this article

A Laravel application can move to PHP 8.5 with very little code change, provided it runs Laravel 12.40 or later (or any Laravel 13 release), its Composer dependencies allow PHP 8.5, and you treat deprecation notices as test failures for the length of the upgrade. This guide is the migration plan: the framework support matrix, the 8.5 deprecations that actually show up in Laravel codebases, Composer and extension checks, the Rector and PHPStan settings that find problems before tests do, a CI matrix, and a staged rollout. It does not repeat the new language features; those are covered in What's new in PHP 8.5.

Tested with: PHP 8.5 CLI (8.5.7), a fresh Laravel 13 application, PHPUnit 12.5, PHPStan 2 with Larastan 3 and phpstan-deprecation-rules, and Rector 2. Every deprecation message quoted below was produced by running the code, not copied from the RFC.

Where PHP 8.5 stands in September 2026

PHP 8.5.0 was released on 20 November 2025. According to the official supported versions page, 8.5 receives active support until 31 December 2027 and security fixes until 31 December 2029. The current release at the time of writing is 8.5.10 (27 August 2026), a bug-fix release. The most recent security release is 8.5.9 (30 July 2026), which fixed, among others, an out-of-bounds write in bccomp() (CVE-2026-17544), an SQL injection via E'...' backslash breakout in ext/pgsql (CVE-2026-17543), a bundled libgd issue (CVE-2026-9672) and a Phar crash via recursive symlinks (CVE-2026-7260). Earlier security releases in the branch were 8.5.1, 8.5.6 and 8.5.8. Treat 8.5.9 as your minimum and follow the PHP 8 changelog for anything newer.

If you are still on PHP 8.2, note that it leaves security support on 31 December 2026; our PHP 8.2 end-of-life checklist covers that deadline. Jumping straight from 8.2 or 8.3 to 8.5 is reasonable: read the 8.4 migration notes as well, because you inherit its changes too.

Which Laravel versions support PHP 8.5

The Laravel support policy table lists PHP 8.2 to 8.5 for Laravel 12 and PHP 8.3 to 8.5 for Laravel 13. Laravel 11 stops at PHP 8.4 and its security support ended on 12 March 2026; Laravel 10 ended in February 2025. If you are on either, upgrade the framework first and PHP second, never both in one deployment.

Matrix of Laravel 10 to 13 against PHP 8.1 to 8.5 showing Laravel 12 and 13 as the only supported releases that run on PHP 8.5
Only Laravel 12 (12.40 or later) and Laravel 13 are supported on PHP 8.5.

Laravel 12 needs a recent minor. The framework changelog shows the "PHP 8.5 Compatibility" pull request landed in v12.40.0 (25 November 2025), with more 8.5 deprecation fixes in v12.31.0, v12.34.0 and v12.43.0 (the null cache store). Laravel 12 also left bug-fix support on 13 August 2026, so it only receives security fixes until 24 February 2027. If you are planning an upgrade window anyway, moving to Laravel 13 at the same time is often the cheaper path; see our Laravel 12 to 13 upgrade guide, but ship the framework upgrade as its own release.

Your stack todayPath to PHP 8.5
Laravel 13, PHP 8.3 or 8.4Update dependencies, run the checks below, switch PHP.
Laravel 12 below 12.40composer update laravel/framework to the latest 12.x first, then PHP.
Laravel 11 or olderUpgrade the framework to 12 or 13 on your current PHP, release, then PHP 8.5.

Stage 1: inventory what will change

Start with facts, not assumptions. You need to know which PHP extensions you load, which packages cap their PHP version, and every place PHP is installed: web servers, queue workers, scheduler hosts, CI images, local Docker files and any one-off cron boxes.

Shell
php -v && php -m                      # on every server role, not just web
composer why-not php 8.5.0            # packages whose constraints exclude 8.5
composer outdated --direct            # direct dependencies with newer releases
grep -rnE "PDO::MYSQL_ATTR_|curl_close\(|imagedestroy\(|finfo_close\(|->setAccessible\(|\((integer|boolean|double|binary)\)" \
  app config database routes tests

composer why-not php 8.5.0 prints every installed package whose require.php constraint rules out 8.5 (for example requires php (8.1 - 8.4)). Each line is a package to update, replace or fork. The grep is a quick first pass for the deprecations that are most common in Laravel code; static analysis below does the thorough job.

Extensions: the OPcache change

The 8.5 backward-incompatible changes include one that breaks build scripts rather than code: OPcache is now always compiled into the PHP binary and always loaded. There is no opcache.so any more, and a zend_extension=opcache.so line now emits a warning. In practice, remove docker-php-ext-install opcache from Dockerfiles based on the official php:8.5 images, and drop separate php8.5-opcache packages from provisioning scripts. The opcache.enable and opcache.enable_cli settings still apply, so keep your tuning (our OPcache and JIT guide lists the values that matter).

Dockerfile
FROM php:8.5-fpm-bookworm

RUN apt-get update && apt-get install -y --no-install-recommends libicu-dev libzip-dev \
 && docker-php-ext-install intl pdo_mysql zip bcmath pcntl \
 && rm -rf /var/lib/apt/lists/*
# No "docker-php-ext-install opcache" on 8.5: OPcache is built in.
COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini

Also check PECL extensions such as redis, imagick, swoole or grpc: each needs a release built for PHP 8.5's internal API. If one has no 8.5-compatible release, that is a blocker for the whole upgrade, and it is better to find out in week one. If the inventory turns up an abandoned package or an extension with no 8.5 build, Laravel and PHP development help to replace or patch it keeps the upgrade from stalling on one dependency.

Stage 2: Composer constraints

Your composer.json require.php constraint should describe the lowest PHP you support, for example "php": "^8.3". You do not need to change it to start using 8.5. What matters during a mixed rollout is the lock file: if production will run 8.4 and 8.5 side by side for a while, pin Composer's platform to the lower version so the lock file only contains packages that install on both.

JSON
{
    "require": {
        "php": "^8.3",
        "laravel/framework": "^13.0"
    },
    "config": {
        "platform": {
            "php": "8.4.0"
        }
    }
}

Remove the platform override once every server runs 8.5. Run composer update with the real target PHP binary at least once in CI as well, because composer install on 8.5 will refuse a lock file containing a package that excludes it.

Stage 3: the deprecations that bite Laravel apps

PHP 8.5 adds a long list of deprecations. Deprecations do not break anything today, but they flood logs, fail strict test suites and become errors in PHP 9. These are the ones we see most often in Laravel code and its dependencies:

Deprecated in 8.5Where it hides in Laravel appsFix
Using null as an array offset, or as the key in array_key_exists()Lookups such as $labels[$request->input('type')], config maps keyed by optional valuesGuard the null case or cast deliberately: $labels[$type ?? '']
PDO::MYSQL_ATTR_* and other driver constants on PDOconfig/database.php (PDO::MYSQL_ATTR_SSL_CA)Use Pdo\Mysql::ATTR_SSL_CA
Non-canonical casts (integer), (boolean), (double), (binary)Older helpers and legacy modules(int), (bool), (float), (string)
curl_close(), imagedestroy(), finfo_close(), xml_parser_free()Hand-written HTTP clients, image and upload handlingDelete the call; the objects free themselves
ReflectionProperty::setAccessible() and friendsTest helpers that poke private propertiesDelete the call (no effect since 8.1)
Incrementing non-numeric strings ($code++ on "a9")Reference or invoice number generatorsstr_increment()
case ending in ;, backtick operator, __debugInfo() returning nullRare, but compile-time noise when presentColon, shell_exec(), return []

The database constant is the one almost every Laravel 11 or 12 application has. Running PDO::MYSQL_ATTR_SSL_CA on 8.5 prints "Constant PDO::MYSQL_ATTR_SSL_CA is deprecated since 8.5, use Pdo\Mysql::ATTR_SSL_CA instead". The Laravel 12 skeleton now ships a version check, which is the safest form while servers run mixed versions:

PHP
// config/database.php (excerpt)
use Pdo\Mysql;

// inside the 'mysql' and 'mariadb' connection arrays:
'options' => extension_loaded('pdo_mysql') ? array_filter([
    (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [],

The Pdo\Mysql class exists from PHP 8.4, so once your floor is 8.4 you can write Mysql::ATTR_SSL_CA directly, which is what the Laravel 13 skeleton does. Both constants have the same value, so nothing changes at runtime.

Two things make null offsets the most dangerous item on the list. First, they are runtime deprecations: they only appear when that line executes with a null value, so static tools miss many of them. Second, $array[$key] ?? 'default' still triggers the deprecation when $key is null; the null-coalescing operator protects against a missing key, not a null one.

Stage 3 continued: Rector and PHPStan

Rector ships a PHP 8.5 rule set (SetList::PHP_85) that renames casts, removes curl_close() and similar calls, replaces backticks with shell_exec(), fixes case semicolons, makes __debugInfo() return an array and casts null keys in array_key_exists(). One detail is easy to miss: Rector reads your target version from the composer.json PHP constraint, and with "php": "^8.3" it silently skips most 8.5 rules. Set the target explicitly:

PHP
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Php85\Rector\Class_\SleepToSerializeRector;
use Rector\Php85\Rector\Class_\WakeupToUnserializeRector;
use Rector\Set\ValueObject\SetList;
use Rector\ValueObject\PhpVersion;

return RectorConfig::configure()
    ->withPaths([
        __DIR__.'/app',
        __DIR__.'/config',
        __DIR__.'/database',
        __DIR__.'/routes',
        __DIR__.'/tests',
    ])
    // Only the PHP 8.5 deprecation fixes, not a full modernisation pass.
    ->withSets([SetList::PHP_85])
    // Without this, a "^8.3" constraint makes Rector skip most 8.5 rules.
    ->withPhpVersion(PhpVersion::PHP_85)
    ->withSkip([
        // __sleep/__wakeup are only soft-deprecated; rewriting them changes
        // serialised payloads already sitting in caches and queues.
        SleepToSerializeRector::class,
        WakeupToUnserializeRector::class,
    ]);

Run vendor/bin/rector process --dry-run, read the diff, then apply it in its own commit. If your Rector version warns that per-version sets are deprecated, withPhpSets(php85: true) is the alternative, but be aware it applies every rule from PHP 5.3 up to 8.5, which produces a far larger diff than an upgrade branch needs.

For PHPStan (with Larastan for Laravel), tell it which PHP version to analyse against and add the official deprecation rules. With phpVersion: 80500, PHPStan 2 reported the non-canonical cast, the backtick operator and curl_close() in our test file; at level 9 it also flagged the nullable key passed to array_key_exists(). It did not flag the null array offset, which is why the tests in stage 4 matter.

YAML
# phpstan.neon
includes:
    - vendor/larastan/larastan/extension.neon
    - vendor/phpstan/phpstan-deprecation-rules/rules.neon

parameters:
    level: 6
    paths:
        - app
    phpVersion: 80500

If you already run PHPStan with a baseline, regenerate nothing: new 8.5 findings should be fixed, not baselined. Our PHPStan tutorial covers levels and baselines in depth.

Stage 4: make deprecations fail the test suite

This is the step most upgrade guides skip, and in Laravel it is not obvious. PHPUnit 12 can fail a run on deprecations with failOnDeprecation="true". That works for plain unit tests. But in feature tests that boot the application, Laravel's own error handler catches PHP deprecations and sends them to the deprecations log channel, which defaults to null. We confirmed it: the same null-offset bug failed the unit test and passed the feature test silently.

Laravel's withoutDeprecationHandling() testing helper turns deprecations back into exceptions. Enable it in your base test case behind an environment flag, so the strict mode runs in the upgrade CI job without disturbing everyone else:

PHP
<?php

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        // During the PHP 8.5 upgrade, turn deprecations into test failures
        // instead of letting Laravel route them to the deprecations log.
        if (getenv('STRICT_DEPRECATIONS') === '1') {
            $this->withoutDeprecationHandling();
        }
    }
}
PhpStorm window with a PHPUnit test class open and the run panel below reporting eight passing tests with OK and exit code 0
A green PHPUnit run means little during an upgrade unless deprecations are configured to fail it.

Add the PHPUnit attributes so plain unit tests report the file and line that triggered each deprecation:

XML
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         failOnDeprecation="true"
         displayDetailsOnTestsThatTriggerDeprecations="true">
    <!-- testsuites, source and php sections unchanged -->
</phpunit>

Deprecations raised inside vendor/ are the package authors' problem, but they still fail the build. PHPUnit's <source> element accepts ignoreIndirectDeprecations="true" to report only deprecations triggered by your own code; use it if a dependency has no fixed release yet, and keep a list of what you are waiting for.

A CI matrix for the transition

Run the suite on both versions until the last server has moved. This GitHub Actions job uses shivammathur/setup-php, which supports 8.5:

YAML
name: tests
on: [push, pull_request]

jobs:
  php:
    runs-on: ubuntu-24.04
    strategy:
      fail-fast: false
      matrix:
        php: ['8.4', '8.5']
    env:
      STRICT_DEPRECATIONS: ${{ matrix.php == '8.5' && '1' || '0' }}
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: intl, pdo_mysql, bcmath, redis
          coverage: none
      - run: composer install --no-interaction --prefer-dist
      - run: vendor/bin/phpstan analyse --no-progress
      - run: php artisan test

Strict mode runs only on 8.5, so the 8.4 job stays a pure regression check. If your tests need MySQL or Redis, add them as job services exactly as you do today; the PHP version does not change that part.

What to test by hand

Automated suites rarely cover these areas well, and they are where 8.5 behaviour changes surface:

  • Queues and cached data: jobs serialised on 8.4 and processed on 8.5 (and the reverse during rollout). Dispatch a few of each job class on one version and process on the other.
  • Float to int conversions: 8.5 warns when a float that cannot be represented as an integer is cast to int, and when NAN is cast. Money and quantity code that does (int) round(...) on user input deserves a test with extreme values.
  • Output handlers: echoing from inside a user output handler is deprecated; check custom PDF or export code that uses ob_start() callbacks.
  • Scheduler and long-running workers: Horizon, Octane and queue:work processes must be restarted onto the new binary; a worker still running 8.4 code after deploy is a common source of confusing logs.

Stage 5: rollout

  1. Staging soak. Deploy the 8.5 build to staging with LOG_DEPRECATIONS_CHANNEL=daily and LOG_DEPRECATIONS_TRACE=true so runtime deprecations land in a log file with stack traces. Run real traffic, the scheduler and every queue for at least several days.
  2. One production role at a time. Queue workers first (easy to roll back, no user-facing latency), then a canary web server behind the load balancer, then the rest.
  3. Watch the right signals: error rate, p95 latency, worker memory, failed jobs, and the deprecations log. Compare against the same hours on the previous week, not just the previous hour.
  4. Keep the rollback image. Keep the last 8.4 image and its lock file deployable until the deprecations log has been quiet for a full release cycle.
  5. Clean up: remove the Composer platform override, drop the 8.4 CI job, and switch LOG_DEPRECATIONS_CHANNEL back to your normal setting.

Before go-live, check your deployment checklist still holds; our Laravel 13 production deployment checklist covers caches, workers and health checks that a runtime change touches.

Common mistakes

  • Upgrading Laravel and PHP in the same deployment. When something breaks you cannot tell which change caused it.
  • Trusting a green test suite while Laravel quietly logs deprecations to the null channel.
  • Running Rector without setting the target PHP version and concluding there is nothing to fix.
  • Forgetting non-web PHP: CI images, cron hosts, a forgotten php:8.3-cli image used for migrations.
  • Leaving zend_extension=opcache.so in a custom ini file and ignoring the startup warning.
  • Applying the __sleep() to __serialize() rewrite blindly, which changes the serialised format of objects already stored in caches, sessions or queues.

When not to upgrade yet

Stay on PHP 8.4 for now if a business-critical extension has no 8.5 build, if you are on Laravel 11 or older and cannot schedule the framework upgrade first, or if you are in a change freeze. PHP 8.4 has active support until 31 December 2026 and security fixes until 31 December 2028, so there is no emergency. What you should not do is stay on PHP 8.2 past the end of 2026.

Cloudsoft Zone upgrades Laravel applications to PHP 8.5 end to end: dependency and extension audit, Rector and PHPStan fixes, a strict-deprecation CI job and a staged rollout with a tested rollback. See Laravel and PHP development.

Frequently asked questions

  • Does Laravel 11 support PHP 8.5?

    No. The official support table lists PHP 8.2 to 8.4 for Laravel 11, and its security support ended on 12 March 2026. Upgrade to Laravel 12 (12.40 or later) or Laravel 13 first, release that, then move to PHP 8.5.

  • Do I need to change the PHP constraint in composer.json to use PHP 8.5?

    No. A constraint such as ^8.3 already allows 8.5. During a mixed rollout, set config.platform.php to the lowest version you run in production so the lock file installs on every server, and remove it afterwards.

  • Why do my Laravel tests pass on PHP 8.5 while the logs show deprecations?

    In feature tests Laravel handles PHP deprecations itself and writes them to the deprecations log channel, which is null by default. Call $this->withoutDeprecationHandling() in your base test case during the upgrade so they become test failures.

  • Will Rector fix PHP 8.5 deprecations automatically?

    Many of them: casts, curl_close(), backticks, case semicolons and __debugInfo(). Set withPhpVersion(PhpVersion::PHP_85) or it skips most rules. It cannot find null array offsets that only occur at runtime; tests and logs catch those.

  • Which PHP 8.5 patch release should production run?

    At least 8.5.9, the latest security release at the time of writing (30 July 2026), and ideally the newest 8.5 patch. Check the PHP 8 changelog on php.net for later security releases before you deploy.

Advertise on CloudSoftZone

Your next customer could start right here.

Introduce your tool, product or business to readers exploring technology.

Advertise with us

Explore placements and request a private quote

Sources

  1. PHP 8.5 migration guide: deprecated features — The PHP Group
  2. PHP 8.5 migration guide: backward incompatible changes — The PHP Group
  3. PHP 8 ChangeLog (8.5.9 and 8.5.10) — The PHP Group, 2026-08-27
  4. Supported versions — The PHP Group
  5. Laravel 13 release notes and support policy — Laravel
  6. Laravel framework 12.x changelog — Laravel on GitHub
  7. Rector documentation — Rector
  8. PHPStan config reference (phpVersion) — PHPStan
  9. PHPUnit 12.5 configuration: failOnDeprecation — PHPUnit
  10. setup-php GitHub Action — Shivam Mathur

Written with substantial AI assistance. Facts checked against cited sources. Editorial policy.

Comments

No comments yet. Be the first to join the conversation.

Leave a comment

Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.

Never published. Used only to contact you about this comment.

Loading the security check…

Comments are moderated before publication.