PHP 8.6 ships in eight days — November 19, 2026. RC6 dropped this week with four targeted bug fixes and nothing else, which means the feature set is frozen and stable enough to test against right now. Three features are worth upgrading for: partial function application, a proper Duration class, and an Io\Poll API that finally gives PHP a scalable I/O primitive. One security default change will break production without tripping a single test. Here is what matters, in the order it matters.
Fix This Before You Upgrade: Session Defaults Changed
The most dangerous change in 8.6 is one you will not find in the feature highlights. Three session INI defaults flipped:
session.cookie_samesite: empty string →"Lax"session.use_strict_mode:0→1session.cookie_httponly:0→1
The SameSite change is the one to watch. If your application passes session tokens across domains — OAuth callbacks, payment redirects, embedded iframes — those flows will silently break in production while every unit test stays green. The fix is explicit: add session.cookie_samesite = None (and session.cookie_secure = 1) wherever you need cross-site behavior. Test in a browser, not just a test runner.
Partial Function Application: Stop Writing Throwaway Closures
Partial function application is the marquee feature and it genuinely changes how everyday PHP reads. Use ? as a placeholder argument. Instead of calling the function, PHP returns a Closure waiting for the missing piece.
// Before PHP 8.6
$slugify = fn(string $s): string => strtolower(str_replace(' ', '-', $s));
// PHP 8.6 — hash every file in an array
$md5Files = array_map(hash_file('md5', ?), $filePaths);
// Named arguments work too
$replaceAlice = str_replace(search: ?, replace: 'Alice', subject: ?);
The ? placeholder fills one argument position. The ... spread captures everything remaining. One gotcha: expressions in a partial run at creation time, not at call time. getenv('APP_ENV') inside a partial captures the value immediately. The RFC passed unanimously and was merged in alpha3.
Io\Poll: PHP Async Finally Has a Foundation
The Io\Poll API is the most technically consequential addition in 8.6, and it is almost completely absent from the broader coverage. The problem it solves: stream_select() — the function powering every PHP async runtime today — is capped at roughly 1,024 file descriptors on Linux and scans every descriptor on every tick. Event loop frameworks like ReactPHP and Revolt have been papering over this with extensions or OS-specific workarounds for years.
Io\Poll replaces all of that. It selects the best available OS mechanism automatically: epoll on Linux, kqueue on BSD and macOS, WSAPoll on Windows.
use Io\Poll\{Context, Event, StreamPollHandle};
$poll = new Context(); // picks epoll/kqueue/WSAPoll automatically
$server = stream_socket_server('tcp://0.0.0.0:8080');
stream_set_blocking($server, false);
$poll->add(new StreamPollHandle($server), [Event::Read]);
while (true) {
foreach ($poll->wait(Time\Duration::fromSeconds(1)) as $event) {
if ($event->handle->hasTriggered(Event::Read)) {
$conn = stream_socket_accept($server);
}
}
}
Benchmarks show 100,000+ concurrent connections at around 3ms latency with Io\Poll versus roughly 500 connections at 48ms with stream_select(). The Revolt event loop already has the driver merged. Symfony shipped polyfill-io-poll for PHP 8.1+ — though the polyfill falls back to stream_select() internally, running 10–35% slower than the native implementation on 8.6.
This is not full async. PHP is still synchronous by default. But Io\Poll is the missing primitive that makes real, zero-extension async runtimes possible in pure PHP.
Time\Duration: DateInterval Done Right
DateInterval works for calendar arithmetic — adding months, computing age. It is awkward for fixed elapsed time, because months have variable lengths and DateInterval cannot compare two durations reliably. Time\Duration fixes this.
use Time\Duration;
$timeout = Duration::fromSeconds(30);
$halfSec = Duration::fromMilliseconds(500);
$parsed = Duration::fromIso8601DurationString('PT1M30S'); // 90 seconds
$total = $timeout->add($halfSec); // immutable — returns new instance
if ($elapsed > Duration::fromSeconds(5)) { /* rate limit, timeout, etc. */ }
It is a final readonly class, nanosecond precision, immutable, and comparable with standard operators. Important: instanceof DateInterval returns false. Update any type checks that test for DateInterval generically before upgrading.
Everything Else Worth Knowing
clamp(): clamp(150, 0, 100) returns 100. Throws ValueError if min exceeds max. Works with any comparable type.
Readonly property defaults: Properties declared readonly can now have default values in the declaration — this was a compile error before 8.6.
Deprecations to fix now: Sixteen mb_ereg*() functions are deprecated (migrate to preg_*()). Old type aliases like is_double(), is_long(), and is_integer() are deprecated. return inside finally blocks is deprecated. These are warnings now; removals land in PHP 9.
preg_grep() behavior change: Now returns false on PCRE error instead of a partial array — consistent with other preg_*() functions, but worth auditing if you relied on partial results.
Test Against RC6 Today
RC6 is feature-complete and stable enough for pre-production testing. Pull the Docker image and run your test suite:
docker pull php:8.6-rc-cli
docker run --rm -v $(pwd):/app -w /app php:8.6-rc-cli php artisan test
Focus your testing on session behavior in a browser (not just unit tests), any mb_ereg*() usage, type checks against DateInterval, and preg_grep() error paths. The official PHP 8.6 migration guide covers the full deprecation and incompatibilities list. Stable drops November 19.

