PHP 8.6: What's New, and What Upgrading From 7.x or Early 8.x Actually Gets You
PHP 8.6 isn't out yet. As of today it's at Beta 3, released September 10, with RC1 planned for September 24 and general availability on the release managers' timetable for November 19, 2026. The feature freeze was Beta 1 on August 13, so the feature set below is what ships; what changes between now and November is bug fixes. The beta announcement says not to run it in production, and we agree.
That still leaves two questions we get asked every time a new PHP lands. What's in it, and what does moving to it actually get a store or an application that's sitting on 7.4, 8.0 or 8.1. The second one gets answered with numbers copied from somebody else's benchmark, so this time we ran our own.
What's new
The UPGRADING file for Beta 3 runs to about a thousand lines. Most of it is extension-level cleanup — a function that now throws a ValueError where it used to return false. These are the changes most application code will notice.
Partial function application
You can leave a hole in a function call with ? and get back a closure that fills it:
// PHP 8.5 and earlier
$result = array_map(static fn(string $s): string => str_replace('hello', 'hi', $s), $items);
// PHP 8.6
$result = array_map(str_replace('hello', 'hi', ?), $items);
... stands for "every remaining argument", so foo(1, ...) is a closure over everything after the first parameter. The closure the engine builds carries the real parameter names, types and defaults from the underlying function, so reflection and static analysis see what you'd expect. This is the follow-on to the first-class callable syntax from 8.1 (strlen(...)), which the RFC describes as the degenerate case of this one. It reads well next to the pipe operator from 8.5.
Default values on readonly properties
public readonly string $driver = 'redis'; was a compile error until now. The original readonly RFC ruled it out because a readonly property with a default is effectively a constant. Interface properties in 8.4 changed the calculus: a class can now satisfy public string $name { get; } with a fixed readonly value and no constructor boilerplate.
Time\Duration
A new always-available class for stop-watch time — timeouts, back-off, "run every 30 seconds" — with nanosecond precision, and none of the calendar ambiguity of DateInterval. Duration::fromMilliseconds(500), fromSeconds(), fromMinutes(), fromHours(), plus ISO 8601 parsing. Expect libraries that take int $timeoutSeconds today to start accepting one of these.
Smaller language changes
#[\Override]now works on class constants and enum cases, so a typo in an overridden constant name is a compile error instead of a second constant.- Enums can define
__debugInfo(). - Objects stored in constants can have their properties written to directly (
Foo::BAR->prop = $val). SortDirectionis a new enum for the sort functions.trim(),ltrim()andrtrim()now strip form feed (\f) by default. Small, and a behavior change.json_last_error_msg()andJsonExceptionmessages now say where in the document the error is.- A new
error_include_argsINI option makes error messages show the arguments that were actually passed, with the same redaction rules as stack traces. Off by default.
For people writing servers and clients
Three of the larger RFCs are infrastructure you'll meet through libraries, not in your own code: a stream errors API (typed StreamError objects instead of warnings, controlled by stream context options), a polling API under Io\Poll for waiting on many handles at once, and TLS session resumption, external PSK and TLS 1.3 early data on stream sockets. There's also a UriBuilder/UrlBuilder pair for the URI extension that arrived in 8.5, and a batch of socket options — keepalive, linger, buffer sizes — that previously needed the sockets extension.
Session defaults that are now secure
This is the change with the most practical reach, and it's in the backward-incompatible section for a reason. Three INI defaults move:
| Setting | Was | Now |
|---|---|---|
session.use_strict_mode |
0 | 1 |
session.cookie_httponly |
0 | 1 |
session.cookie_samesite |
unset | Lax |
Strict mode rejects a session ID the server never issued, which closes session fixation. HttpOnly keeps the cookie away from document.cookie. Lax stops it being sent on cross-site POSTs.
If you're on Laravel or Magento, the framework already sets all three, so nothing changes. Where it bites: a custom session save handler that never implemented validateId() and create_sid() — those are now expected, and passing a handler without them is separately deprecated — and anything that reads the session cookie from JavaScript. A checkout that posts back from a third-party payment page on a different domain and expects the session cookie to arrive needs SameSite=None set explicitly, with cookie_secure on.
What's deprecated
Deprecations are notices. Nothing stops working in 8.6; the point is that it will stop in 9.0. Which ones you'll see in a real codebase, roughly in order of how often:
spl_object_hash() — use spl_object_id(). This is everywhere. In the vendor/ directory of one Laravel 13 application we maintain it appears in 15 files across 12 packages, including laravel/framework, symfony/console, guzzlehttp/guzzle and nesbot/carbon. Every one of those is a notice on 8.6 until the package updates, and every one of them will update.
return inside a finally block — it silently discards the exception or return value from the try, which has been a footgun since PHP 5.5. Now you're told.
is_integer(), is_long(), is_double(), doubleval() — the aliases. Use is_int(), is_float(), floatval(). Old Magento extension code uses is_integer() a fair amount.
The mb_ereg family — the Oniguruma regex library underneath it is no longer maintained, so the whole of mbregex is deprecated. If you have mb_ereg_replace() in a slug or search-normalization routine, move it to preg_replace() with the /u modifier.
Returning a value from __construct() or __destruct(), and making either a generator.
SplFileObject::fgetcsv(), fputcsv(), setCsvControl(), getCsvControl() — the CSV methods on SplFileObject. Import scripts built on it should move to fgetcsv() on a plain stream.
ArrayIterator::asort(), ksort(), getFlags(), setFlags() and the rest of the methods it inherited from ArrayObject.
mysqli_stmt_init(), mysqli_get_charset(), metaphone(), spl_classes(), and passing an object where an array is expected to array_walk(), http_build_query() and mb_convert_variables().
The full list with the reasoning is in the deprecations RFC. Run your test suite on the RC with error_reporting=E_ALL and grep the log; that's the whole audit.
What got faster inside 8.6 itself
The performance section of the UPGRADING file lists compiler-level changes:
printf()calls using only%sand%dare compiled into string interpolation, so there's no function call and no format-string parsing.array_map()with a first-class callable or a partial application is compiled into the equivalentforeach, which removes the closure allocation and the userland-from-internal call overhead.- Closures that provably don't use
$thisare made static automatically, which breaks the object-closure reference cycle that otherwise sits around waiting for the garbage collector. Stateless closures are cached instead of re-created per call. The RFC tested the inference on the Symfony demo and it caught 68 of 87 closures that had been explicitly marked static. - JSON encoding of arrays and objects is faster, and pretty-printing indentation in particular.
- The tail-call VM got faster, the ZTS build got faster, and the JIT now works on ZTS builds on Apple Silicon — which matters for Macs running the threaded build.
A cluster of standard-library functions — array_sum(), array_product(), array_intersect(), array_unshift(), array_walk(), str_split() — got individual speedups too. None of these come with a number, and in our measurement below the difference between 8.5 and 8.6 is inside the noise. They're real, and they're small.
What an upgrade actually gets you
Here's the part that gets copied around without a source. php.net's own numbers stop at 8.1: the 8.0 release page says the tracing JIT shows "about 3 times better performance on synthetic benchmarks and 1.5–2 times improvement on some specific long-running applications", and that "typical application performance is on par with PHP 7.4". The 8.1 page reports the Symfony demo application 23.0% faster and WordPress 3.5% faster than 8.0, mostly from the inheritance cache, which stopped classes being re-linked on every request. From 8.2 on the release pages say "performance improvements" and give no figures.
So we measured it. Same machine, same code, eight PHP versions.
Setup. An Intel Core i9 laptop, Docker under Colima with 8 CPUs, the official php:<version>-cli images: 7.4.33, 8.0.30, 8.1.34, 8.2.33, 8.3.33, 8.4.25, 8.5.10 and 8.6.0beta3. Opcache on for every run. Each version measured twice: JIT off, then the tracing JIT with a 64 MB buffer. Every number is the median of repeated runs, and the versions were interleaved so a warming laptop didn't penalize whichever one ran last.
Benchmark one: Zend/bench.php. PHP's own micro-benchmark — tight loops, recursion, string building. Total seconds, lower is better, median of seven:
| PHP | JIT off | JIT on |
|---|---|---|
| 7.4.33 | 0.242 | — |
| 8.0.30 | 0.247 | 0.106 |
| 8.1.34 | 0.265 | 0.104 |
| 8.2.33 | 0.258 | 0.103 |
| 8.3.33 | 0.258 | 0.101 |
| 8.4.25 | 0.257 | 0.100 |
| 8.5.10 | 0.264 | 0.099 |
| 8.6.0beta3 | 0.268 | 0.100 |
That's php.net's "3 times on synthetic benchmarks" — we got 2.4× from 7.4 to 8.6 with the JIT. It's also the whole story of that number: with the JIT off, this benchmark is flat from 7.4 to 8.6. The interpreter itself did not get faster at tight loops, and if anything the 8.x line is a few milliseconds slower here. We didn't dig into why.
Benchmark two: a page that looks like an application. Nobody serves bench.php. So we wrote a small autoloaded page — a five-deep class hierarchy of about 55 classes, a container, an event dispatcher, a catalog of 3,000 product objects filtered and sorted into a listing, a cart priced with integer money arithmetic and a customer-group discount, a payload encoded to and decoded from JSON five times, preg_replace slugs, DateTimeImmutable formatting, and an .phtml template rendered through output buffering with escaping. About 10 ms of PHP per request on 7.4, and byte-identical output on every version. Served by PHP's built-in server, hit sequentially by ab. Milliseconds per request, lower is better, median of three runs of 600 requests:
| PHP | JIT off | JIT on |
|---|---|---|
| 7.4.33 | 9.94 | — |
| 8.0.30 | 9.65 | 8.49 |
| 8.1.34 | 9.42 | 8.23 |
| 8.2.33 | 9.34 | 7.83 |
| 8.3.33 | 9.33 | 7.93 |
| 8.4.25 | 9.51 | 8.33 |
| 8.5.10 | 9.54 | 7.74 |
| 8.6.0beta3 | 9.54 | 7.73 |
Reading it:
- 7.4 to 8.6, JIT off: about 4% less time per request. That's "on par", as php.net said in 2020.
- 7.4 to 8.6 with the JIT on: about 22% less time, or roughly 1.3× the throughput. More than php.net's "on par" for typical applications, a long way short of the 2.4× the synthetic benchmark shows.
- 8.0 to 8.6, both with the JIT: about 9%. Early 8.x to current is a modest gain, spread over six releases.
- Adjacent 8.x releases are within noise. Run-to-run variation on a laptop is 5–10%. The 8.4 dip and the 8.2 bump in the JIT column aren't real; don't build a case on them.
Two honest caveats. Our page has 55 classes; a Magento or Symfony request links hundreds, which is exactly where 8.1's inheritance cache paid off and where php.net's 23% came from. If your application is framework-heavy, the 8.0-to-8.1 step is probably bigger for you than this table shows. And the built-in server plus loopback overhead is a fixed cost inside every number here, which compresses the ratios slightly; under PHP-FPM the percentages would be a little larger, not smaller.
The JIT is still off by default, in 8.6 as in every release since 8.0. For 8.0 through 8.3 the buffer size defaulted to zero; since 8.4 opcache.jit defaults to disable. Turning it on is two lines of php.ini:
opcache.jit=tracing
opcache.jit_buffer_size=64M
It's reversible, so the right approach is to measure your own application on staging with it on and off. Ours gained a fifth. Yours might gain less, and a heavily database-bound page will gain close to nothing because the time isn't in PHP.
The actual reason to upgrade
It isn't speed. From php.net's supported versions and end-of-life pages:
| Branch | Security fixes end |
|---|---|
| 7.4 | November 28, 2022 |
| 8.0 | November 26, 2023 |
| 8.1 | December 31, 2025 |
| 8.2 | December 31, 2026 |
| 8.3 | December 31, 2027 |
| 8.4 | December 31, 2028 |
| 8.5 | December 31, 2029 |
| 8.6 | December 31, 2030, under the same four-year policy |
A server on 7.4 has had no security fixes for nearly four years. 8.0 and 8.1 are in the same state. 8.2 has fifteen weeks left. That is the case for moving, and a 4% or 22% faster page is what you get on the side.
Where you land depends on what's on top of PHP. Magento 2.4.9 supports PHP 8.4 and 8.5, and Adobe has added each new PHP version in the following feature release — 8.3 with 2.4.7, 8.4 with 2.4.8, 8.5 with 2.4.9 — so 8.6 on Magento is a 2.4.10 question and not a November one. Mage-OS follows the same line. Laravel publishes a support table per release; check it before you move a server, because the framework's range is the constraint, not PHP's. If you're still on Magento 1, OpenMage LTS runs on 8.1 through 8.5. And whatever the target, the PHP upgrade is its own step, done and verified before the application upgrade, so a failure has one possible cause instead of two.
What we'd do with the beta today: run your test suite against php:8.6.0beta3-cli or the RC, with error_reporting=E_ALL, and count the deprecations by package. That's an afternoon, it tells you which of your vendors you're waiting on, and it's the only part of the 8.6 upgrade that's worth starting before November.
Emyrix runs PHP and platform upgrades for Magento, Adobe Commerce, Mage-OS and Laravel applications. If you'd like the deprecation audit done on your codebase or a PHP upgrade planned, get in touch. For a Magento store, the free health check will tell you what version you're on and what it's costing you.
Feature lists, deprecations and the release timetable are from the PHP 8.6.0 Beta 3 UPGRADING file, the RFCs linked above and the release managers' page on wiki.php.net, all read on September 17, 2026. The 8.0 and 8.1 performance figures are php.net's own. The two benchmark tables are our measurements on one Intel laptop and are meant to show direction, not to predict your application; the method is described in full above so you can repeat it.
Frequently asked questions
Has PHP 8.6 been released yet?
Not as of September 17, 2026. The third beta shipped on September 10, the first release candidate is planned for September 24, and general availability is scheduled for November 19, 2026. The feature freeze was at Beta 1 on August 13, so the feature list is settled; what changes between now and November is bug fixes.
How much faster is PHP 8 than PHP 7.4?
Less than most articles say. php.net's own statement for PHP 8.0 was that typical application performance is on par with 7.4, with the JIT giving 3× on synthetic benchmarks. Our own measurement on an app-shaped page agrees: 7.4 to 8.6 is about 4% faster without the JIT and about 22% with it. PHP 8.1's inheritance cache gave frameworks with large class graphs a bigger step — php.net measured 23% on the Symfony demo application.
Is the PHP JIT enabled by default in PHP 8.6?
No. It has been off by default in every release since 8.0, and 8.6 doesn't change that. Enabling it is two php.ini lines — opcache.jit=tracing and opcache.jit_buffer_size=64M — and it's reversible, so the sensible approach is to measure your own application on staging with it on and off.
Will PHP 8.6 break my Magento or Laravel application?
The runtime changes are mostly deprecation notices, which don't break anything but do fill logs. What decides whether you can run it is your framework's support matrix, not PHP itself. Magento 2.4.9 lists PHP 8.4 and 8.5; Adobe has added each new PHP version in the following feature release, so wait for their word. Check Laravel's support table for the same reason before moving a server.
What are the PHP 8.6 session changes?
Three INI defaults change: session.use_strict_mode becomes 1, session.cookie_httponly becomes 1, and session.cookie_samesite becomes Lax. Most frameworks already set these themselves, so most applications will see no difference. A custom session handler that relied on accepting externally supplied session IDs, or JavaScript that reads the session cookie, is what breaks.
Which PHP versions are still supported in 2026?
PHP 8.2, 8.3, 8.4 and 8.5. PHP 8.2 gets security fixes only until December 31, 2026, so it's the next one to leave. 8.1 ended on December 31, 2025, 8.0 in November 2023, and 7.4 in November 2022.