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.
Updated 13 min read

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.

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 today | Path to PHP 8.5 |
|---|---|
| Laravel 13, PHP 8.3 or 8.4 | Update dependencies, run the checks below, switch PHP. |
| Laravel 12 below 12.40 | composer update laravel/framework to the latest 12.x first, then PHP. |
| Laravel 11 or older | Upgrade 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.
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 testscomposer 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).
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.iniAlso 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.
{
"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.5 | Where it hides in Laravel apps | Fix |
|---|---|---|
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 values | Guard the null case or cast deliberately: $labels[$type ?? ''] |
PDO::MYSQL_ATTR_* and other driver constants on PDO | config/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 handling | Delete the call; the objects free themselves |
ReflectionProperty::setAccessible() and friends | Test helpers that poke private properties | Delete the call (no effect since 8.1) |
Incrementing non-numeric strings ($code++ on "a9") | Reference or invoice number generators | str_increment() |
case ending in ;, backtick operator, __debugInfo() returning null | Rare, but compile-time noise when present | Colon, 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:
// 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
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.
# phpstan.neon
includes:
- vendor/larastan/larastan/extension.neon
- vendor/phpstan/phpstan-deprecation-rules/rules.neon
parameters:
level: 6
paths:
- app
phpVersion: 80500If 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
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();
}
}
}
Add the PHPUnit attributes so plain unit tests report the file and line that triggered each deprecation:
<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:
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 testStrict 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
NANis 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:workprocesses 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
- Staging soak. Deploy the 8.5 build to staging with
LOG_DEPRECATIONS_CHANNEL=dailyandLOG_DEPRECATIONS_TRACE=trueso runtime deprecations land in a log file with stack traces. Run real traffic, the scheduler and every queue for at least several days. - 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.
- 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.
- 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.
- Clean up: remove the Composer
platformoverride, drop the 8.4 CI job, and switchLOG_DEPRECATIONS_CHANNELback 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-cliimage used for migrations. - Leaving
zend_extension=opcache.soin 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.3already allows 8.5. During a mixed rollout, setconfig.platform.phpto 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
deprecationslog channel, which isnullby 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,casesemicolons and__debugInfo(). SetwithPhpVersion(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.
Your next customer could start right here.
Introduce your tool, product or business to readers exploring technology.
Explore placements and request a private quote
Sources
- PHP 8.5 migration guide: deprecated features — The PHP Group
- PHP 8.5 migration guide: backward incompatible changes — The PHP Group
- PHP 8 ChangeLog (8.5.9 and 8.5.10) — The PHP Group, 2026-08-27
- Supported versions — The PHP Group
- Laravel 13 release notes and support policy — Laravel
- Laravel framework 12.x changelog — Laravel on GitHub
- Rector documentation — Rector
- PHPStan config reference (phpVersion) — PHPStan
- PHPUnit 12.5 configuration: failOnDeprecation — PHPUnit
- 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.
Related tools
- Directory profile
GitHub
Git-based platform for hosting code, reviewing pull requests, tracking issues and running CI/CD workflows, with free and per-user paid plans.
A tool connected to a service selected for this article. Compare its fit for your project; inclusion is not an endorsement.
- Directory profile
Postman
API platform for designing, testing, documenting and monitoring APIs, with desktop and web apps, a CLI and a free plan.
A tool connected to a service selected for this article. Compare its fit for your project; inclusion is not an endorsement.
More from Cloudsoft Zone

TutorialPHP
PHPStan tutorial: catch PHP bugs before production with levels
Install PHPStan 2.3, choose a rule level, adopt a baseline for legacy code and fail CI only on new errors, with real output from a worked PHP 8.5 example.
By Cloudsoft Zone

Resource guidePHP
PHP 8.2 reaches end of life on 31 December 2026: an upgrade checklist
PHP 8.2 stops receiving security fixes on 31 December 2026. Here is the current support table from php.net, how to choose between PHP 8.4 and 8.5, and a step-by-step checklist for a safe upgrade.
By Cloudsoft Zone

TutorialPHP
Build a read-only PHP MCP server with MySQL and the official SDK
Expose product lookup, bounded low-stock searches and an inventory summary to AI clients with the official PHP MCP SDK, PDO and local STDIO transport.
By Cloudsoft Zone
