A while back I lost an afternoon to a retry setting of 300. The developer who wrote it meant milliseconds. The worker that read it assumed seconds, so every failed job parked itself for five minutes and the queue backed up until someone paged me. Nothing in the type system could have caught it, because both sides were just passing an int around. PHP 8.6, due on November 19, 2026, finally attacks that class of bug head on, and that, more than any new syntax, is why I'm looking forward to this release.
<?php
use Time\Duration;
// backoff math stays in duration space, no unit juggling
$base = Duration::fromMilliseconds(100);
$attempt = 5;
$delay = $base->multiplyBy(2 ** $attempt);
$timeout = Duration::fromSeconds(2)
->add(Duration::fromMilliseconds(500));
if ($delay < $timeout) {
usleep($delay->seconds * 1000000);
}The feature everyone will demo at meetups is partial function application. You write number_format(?, 2, '.', ' ') and get back a closure waiting for the missing argument, with ... available to leave whole tails of a signature open. Paired with the pipe operator it means a transformation chain no longer needs a throwaway arrow function at every step. Add readonly properties with default values, #[\Override] extended to class constants, and enums that can implement __debugInfo(), and you have a solid grab bag. Good stuff. But my vote for the headline goes to Time\Duration.
Duration is a proper value type for spans of time. You build one with fromSeconds() or fromMilliseconds(), and internally it holds three parts: seconds, nanoseconds and a negative flag, so 1500 milliseconds becomes one second plus 500 million nanoseconds without you doing the arithmetic. Instances are immutable, add() and multiplyBy() return fresh objects, and two durations compare directly with < and friends. Exponential backoff becomes multiplication on a value that knows what it is. We stopped storing money as bare floats years ago because the unit kept getting lost in transit; a timeout stored as a naked integer is the same trap, and 8.6 finally closes it.
Around that centerpiece sit smaller standardizers that I'd call quality-of-life legislation. clamp($volume, 0, 100) replaces the max() wrapped around min() contortion every codebase reinvented, and it accepts anything comparable, DateTimeImmutable included. SortDirection ships as a core enum with Ascending and Descending cases, so the eternal 'asc' versus 'ASC' versus true string-typing in repository methods gets a real type. And JsonException messages now carry coordinates, reporting a syntax error near location 1:3 instead of shrugging at you from somewhere inside a 40 KB payload. Each of these is tiny. Each one also retires a helper function that existed in five incompatible flavors across your vendor directory.
Then there is the piece server-side folks should study: the new Polling API. Io\Poll\Context lets a process register stream handles with events like Event::Read and then wait() on all of them at once, with PHP picking the best OS mechanism underneath, epoll on Linux, kqueue on macOS and BSD, WSAPoll on Windows, event ports or plain poll elsewhere. Until now the built-in answer was stream_select(), which inherits the scaling ceilings of the ancient select() syscall. To be clear about scope: this is a waiting primitive, no scheduler, no event loop, no async keyword. But projects like Amp and ReactPHP, which today either lean on stream_select() or ask your ops team to install an extra extension, get a native foundation in every default PHP build. And yes, wait() takes a Duration for its timeout, zero for a non-blocking check, null to block until something happens. The new pieces already interlock.
Now the honest counterpoint. Syntax is what changes your diffs on day one, and partial application will reshape everyday code far more visibly than any Duration object. I concede that, and I'd add the caveats worth knowing: arguments you supply when building a partial are evaluated right there, at creation time, so a config lookup baked into a partial is frozen while the same lookup inside an arrow function reruns on every call. And a partial is a brand-new anonymous closure; ask reflection who it is and it won't tell you about the function it wraps, whereas strlen(...) keeps its identity, attributes and all. Why do I still rank the library work higher? Because you could always fake currying with fn() and a little discipline. No composer package can make Duration the unit type that PDO drivers, HTTP clients and your own code all agree on, and none can put epoll into the default runtime. Only core can mint shared vocabulary.
Before you upgrade, budget for the housekeeping. Session defaults get stricter out of the box: session.use_strict_mode, cookie_httponly and a SameSite value of Lax are now the baseline, which is the right call but will surprise cross-site login flows and any JavaScript that reads the session cookie. Returning from a finally block is deprecated, as is returning values from __construct() and __destruct(). spl_object_hash() is on the way out; spl_object_id() replaces it but hands back an int instead of a 32-character string, so grep for code that stores or compares those hashes. Legacy aliases like is_double(), plus strcoll(), metaphone() and object-to-array coercion join the deprecation list, the identifiers let, is and readonly become reserved for class and function names, and the trim() family now strips form-feed characters by default. PHPCompatibility, Rector and a PHPStan baseline turn all of this into a checklist rather than a crisis.
Here is where I genuinely want pushback. Every one of these additions overlaps with something userland already built: Carbon-style time objects, framework sort enums, hand-rolled clamp helpers, ext-uv. One camp says core absorbing this territory finally gives us one convention instead of six. The other says it just adds a seventh, and that interop pain between Time\Duration and DateInterval, or between SortDirection and whatever your ORM invented in 2019, will outlast the benefit. Where do you land, and what should core standardize next, a money type, a real URI object, or nothing at all for a while?




Comments
No comments yet — be the first.
Open the discussion
No account or password needed — just enter your e-mail and we’ll send you a one-time sign-in link. First time here? You’re set up automatically.
Your rating will be applied automatically after you sign in.
Check your inbox
We’ve sent a sign-in link to …. Open it on this device — this tab will sign you in automatically.
Nothing arrived? Check your spam folder — and mark the mail as "Not spam" so it lands in your inbox next time.