DEV Community

Cover image for PHP 8.6 Release Date, New Features and Deprecations
devTalk
devTalk

Posted on Originally published at dev-talk.com

PHP 8.6 Release Date, New Features and Deprecations

PHP 8.6 is scheduled for general availability on 19 November 2026. The feature freeze has passed, release candidate 4 is out, and the headline features are known: partial function application, a clamp() function, a proper Duration class, and a batch of deprecations that may light up your logs. I built PHP 8.6.0 RC4 from the official source and ran every example below, so you can see what the code does today instead of what a blog post promises. One warning first: it is a release candidate, and it is not for production.

Dates

From the schedule on wiki.php.net: RC4 shipped on 21 October, RC5 is planned for 5 November, and GA is planned for 19 November 2026. The hard feature freeze was 22 September, so new features should not appear now, but dates and small details can still move. PHP 8.2 reaches end of life on 31 December 2026, which makes this a good moment to plan your upgrade path anyway.

1. Partial function application: the ? placeholder

You can now create a function from another function by leaving some arguments open with ?:

$slug = str_replace(' ', '-', ?);

$slug('Hello World');            // "Hello-World"  ($slug is a Closure)

array_map(str_repeat(?, 2), ['ab', 'c']);   // ["abab", "cc"]
Enter fullscreen mode Exit fullscreen mode

It works on methods too ($greeter->greet('Hello', ?) gave "Hello, Ada!"), and with several placeholders (array_slice(?, ?, 2) called with [1,2,3,4,5] and 1 returned [2,3]). Use ... to leave all remaining arguments open:

$pad = str_pad('7', ...);
$pad(3, '0', STR_PAD_LEFT);      // "007"
Enter fullscreen mode Exit fullscreen mode

Two things I hit while testing:

  • ...must come last. Writing str_pad(..., pad_type: STR_PAD_LEFT) is a fatal error: "Variadic placeholder must be last".
  • Every?becomes a required parameter, even if the original parameter was optional. Calling a closure made with str_replace(' ', '-', ?, ?) with one argument threw ArgumentCountError.

Named arguments work alongside placeholders: str_pad(?, 3, pad_type: STR_PAD_LEFT, pad_string: '0') then called with '7' gave 007. According to the upgrade notes, array_map() with a partial application or first-class callable is compiled into a plain loop, which avoids creating extra closures. I did not benchmark that claim.

2. clamp()

clamp(101, min: 0, max: 100);   // 100
clamp(-5, 0, 100);              // 0
clamp(42, 0, 100);              // 42
clamp(5, 10, 1);                // ValueError
Enter fullscreen mode Exit fullscreen mode

The error message was: "clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)". No more hand-written max($min, min($max, $value)).

3. Time\Duration

An immutable class for lengths of time. It has factory methods (fromSeconds, fromMilliseconds, fromMinutes, fromHours, and fromIso8601DurationString), arithmetic (add, sub, multiplyBy, divideBy, negate, absolute) and a static compare(). The readable properties are seconds, nanoseconds and negative:

use Time\Duration;

// exponential backoff: 100ms, 200ms, 400ms, 800ms
foreach ([0, 1, 2, 3] as $attempt) {
    $delay = Duration::fromMilliseconds(100)->multiplyBy(2 ** $attempt);
}

Duration::fromMinutes(90)->seconds;                        // 5400
Duration::compare(
    Duration::fromMinutes(90),
    Duration::fromIso8601DurationString('PT1H30M')
);                                                          // 0 (equal)
Enter fullscreen mode Exit fullscreen mode

The backoff loop produced 100,000,000, 200,000,000, 400,000,000 and 800,000,000 nanoseconds. Trying to assign $duration->seconds = 9 threw "Cannot modify readonly property". Note that the class has no toMilliseconds() method in RC4: you read seconds and nanoseconds yourself.

4. Smaller additions that worked in my tests

  • Readonly properties can have defaults: public readonly array $tags = ['a']; is now valid, and changing it still throws.
  • Enums can define__debugInfo(): var_dump(Level::Low) printed my custom label and value.
  • #[\Override]works on class constants. On a constant that does not exist in a parent or interface, PHP stops with: "Lone::X has #[\Override] attribute, but no matching parent constant exists".
  • Objects stored in constants can be mutated: after const BOX = new Box;, BOX->n = 5; worked.
  • JSON errors say where: json_last_error_msg() returned "Syntax error near location 1:15", and JsonException carries the same detail.
  • pack()andunpack()accept<and> on signed integers: pack('l>', -2) gave fffffffe and pack('l<', -2) gave feffffff.
  • New in core: a SortDirection enum (cases Ascending and Descending) and a polling API (Io\Poll). The upgrade notes list both; I confirmed the enum exists and did not exercise the polling API.

Deprecations: run your test suite with E_ALL

I triggered each of these on RC4. They are deprecations, not removals, but they will add noise to logs:

Code What PHP 8.6 says
return inside finally "Returning from a finally block is deprecated"
return 5; in __construct() "Returning a value from a constructor is deprecated"
define('X', 1, true) Third argument deprecated (it also emitted a warning that this will be an error in PHP 9.0)
A class named let or is "Using "let" as a class name is deprecated since 8.6"
const _ = 1; "Calling a constant "_" is deprecated since 8.6"
Class constant named namespace "Declaring class constant called 'namespace' is deprecated"
A function named readonly "Calling a function "readonly" is deprecated since 8.6"
is_a($str, $cls, false) with a string Deprecated when $allow_string is false
__debugInfo(): ?array Deprecated: declare a non-nullable array return type

The upgrade notes list more, including Mbregex (the Oniguruma library behind it is no longer maintained) and several mysqli and Reflection calls. Two that older codebases may hit are return inside finally and a third argument to define().

Changes that affect behavior

  • Safer session defaults. session.use_strict_mode now defaults to 1, session.cookie_httponly to 1, and session.cookie_samesite to "Lax". I confirmed the new defaults in the source. If your app reads the session cookie from JavaScript, or depends on cross-site POSTs carrying it, set the old values explicitly. Laravel manages its own session cookie settings, so most Laravel apps are unaffected; apps on native PHP sessions should check.
  • By-referenceforeach can now visit elements it previously skipped after array compaction, and ?? or empty() on a magic property no longer calls __get() in one specific case. Both are in the upgrade notes under backward-incompatible changes.
  • preg_grep()now returnsfalse instead of a partial array when a PCRE error occurs, like the other preg_* functions.

Is Laravel ready?

At the 13.38.0 tag, Laravel's composer.json requires ^8.3, which allows PHP 8.6, but its test workflow covers PHP 8.3, 8.4 and 8.5, with no 8.6 in the list. So the framework is not yet tested against 8.6 in that release, and your packages probably are not either. A sensible plan: add an optional CI job on the 8.6 release candidate that is allowed to fail, run it with error_reporting set to E_ALL, and fix the deprecations that appear. Wait for your dependencies before moving production.

References


Originally published on DEV Talk.

Top comments (0)