PHP 8.5 at six months: what we've learned shipping it to real clients
PHP 8.5 has been in production since November. Here's our balance: what's actually making a difference, what's still hype, and what to break before hitting the upgrade button.
What it promised and what it delivered
PHP 8.5 shipped on 20 November 2025. We've been running it on client servers for six months (some in the first week, some only in the March refresh cycle), so we've got data rather than demos.
Short version: it's the cleanest minor in the 8.x line for a low-drama upgrade. More stable out of the gate than 8.3, less disruptive than 8.4 in breaking changes, and with a couple of features that are genuinely landing in real code rather than just tweets.
Let's go feature by feature: what we use, what we don't, and what to break before migrating.
What's actually landing in client code
The pipe operator |>
The change that surprised us most. On paper it looked like RFC eye-candy; in real code it's replacing nested array_map chains that took effort to read.
Before:
$name = strtoupper(trim(str_replace('-', ' ', $slug)));
After:
$name = $slug
|> fn($s) => str_replace('-', ' ', $s)
|> trim(...)
|> strtoupper(...);
Not revolutionary. But in input normalisation pipelines, validation chains and API payload builders, it reads better, and the next PR author doesn't have to count nested brackets. We're adopting it in new code; old code stays as is, because refactoring for sport is the most expensive disease in the trade.
clone with
For readonly classes (which we've leaned on without remorse since 8.2), clone with cleans up the "build a new object with one field changed" pattern:
$order_v2 = clone $order with { status: 'confirmed' };
Without this, every class needed a withStatus() method or a constructor with N arguments. This one we are retrofitting into existing code, especially DTOs and value objects. Where there was withX(), now clone with.
#[\NoDiscard]
Our favourite and the one with least publicity. An attribute that marks a function as "the return value matters, don't drop it":
#[\NoDiscard("The transaction may have failed")]
public function execute(): Result { ... }
If someone calls $tx->execute(); ignoring the result, PHP warns via error_reporting. This is gold for payment libraries, locks, queues and anything that can fail quietly. We're adding it to the critical methods of the financial services in Atellum (our in-house POS) — the classic "I called the method and didn't check the result" bug almost vanishes on its own.
URI extension
Until now, parsing URLs in PHP was a mosaic of parse_url(), urlencode() and a prayer. The new URI extension implements RFC 3986 and the WHATWG URL Standard:
$url = new \Uri\WhatWg\Url('https://example.com/blog/post?utm=foo');
$url = $url->withQuery('utm=bar');
echo $url; // https://example.com/blog/post?utm=bar
Two reasons it matters: subtle encoding bugs disappear (%20 vs +, slashes inside parameters, IDN), and we stop reaching for league/uri for the millionth time.
What we're NOT using
First-class callables in constant expressions
const PROCESSORS = [
'json' => json_decode(...),
'xml' => simplexml_load_string(...),
];
Pretty on paper. In practice our dispatch tables sit on classes or configs, not global constants, so nothing changed for us. I suspect this is a framework-targeted feature (Symfony will love it) rather than an application-code one.
Extended asymmetric visibility
Lets you declare public(set: private) and other combinations. Fine, but a new joiner takes longer to read a class with asymmetric visibility than one with a private setter. We're parking it until readability earns its place back.
What to break before migrating
These are the breaking changes we've seen explode in actual projects:
| Change | Typical symptom | How to fix |
|---|---|---|
MYSQLI_REFRESH_* constants deprecated | Warning on boot | Drop them, nobody uses them |
mb_substr_count validates encoding more strictly | Exception where there used to be a warning | Always pass $encoding |
date() with 'P' and a timezone string deprecated | Warnings in logs | Use DateTime::format('P') with an object, not a string |
var_dump float formatting changed | Snapshot tests fail | Regenerate snapshots or move to var_export |
None of them is bad. All of them together in an old project (Laravel 8, Symfony 5) can add up to 50-100 warnings on the first boot. Usual plan: one day of cleanup before touching production.
Framework compatibility
As of today:
- Laravel 11 and 12: officially supported from day one.
- Symfony 7.2 onwards: officially supported.
- WordPress 6.7+: tested and green. Key plugins (WooCommerce, Yoast, ACF) fine.
- Drupal 11: supported.
- PrestaShop 8.x: requires a patch on
vendor/composer/installersbefore migrating. We've seen it. - Magento 2.4.7+: announced, but with caveats around third-party extensions. Do not migrate to production without thorough staging.
How we migrate a server
If the client is already on 8.3 or 8.4, the jump to 8.5 is barely a maintenance event. Steps:
- Staging first, always. The staging VPS mirrors production exactly.
- Automated smoke test: scripts that hit the critical pages (login, checkout, dashboard) and diff HTTP responses and timings.
- Migrate PHP-FPM in the quiet hours (4-6 AM for Spanish B2C projects).
- Keep
php8.4-fpminstalled alongside for 48 hours. If something blows up, swap in Nginx in 30 seconds. - Watch the PHP logs for the first 24 hours with
tail -Fand prepared greps (PHP Warning,PHP Deprecated).
This isn't 8.5-specific. It's our standard PHP-jump procedure. Worth repeating because every time a client does it solo, there's always some warning that breaks that one old logging plugin nobody's touched in three years.
Should we migrate your project?
PHP 8.5 carries official active support until December 2027 and security support until December 2029. If your project is on 8.1 or earlier, you're running on a version that no longer receives security patches. That's the main reason to move — not the pipe operator.
If you want to know what you'd have to retouch in your code before the jump, let's talk. Initial audit is half a day; the migration itself, with staging and rollback ready, usually closes in a day or two.
References
Want to talk about your case?
Tell us what you need and we will get back to you within 24 hours with a clear proposal.
Get a quote