New Features and Changes in PHP 8.6

PHP 8.6 is scheduled for general availability on 19 November 2026. The release candidates are out and the feature list is frozen, so this is a good moment to look at what is new, what changes behaviour, and what you should fix before upgrading.

The headline features are partial function application, a native clamp() function, a Time\Duration class, default values for readonly properties and a new Polling API in core. There is also a long deprecation list and a change to the default session settings.

Key Features in PHP 8.6

1. Partial Function Application

PHP 8.5 and earlier: Fixing some of the arguments of a function meant writing a closure by hand.

$dashes = fn (string $subject) => str_replace(' ', '-', $subject);

echo $dashes('PHP 8.6 released');
// PHP-8.6-released

PHP 8.6: Call the function with placeholders and you get a closure back. ? stands for exactly one argument at that position, and ... stands for zero or more remaining arguments.

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

echo $dashes('PHP 8.6 released');
// PHP-8.6-released

function connect(string $host, int $port, float $timeout, bool $ssl) {}

$local = connect('127.0.0.1', ?, ?, false); // two arguments left
$secure = connect('db.internal', 5432, ...); // the rest is left open

It combines well with the pipe operator from PHP 8.5:

$slug = ' PHP 8.6 Released '
    |> trim(...)
    |> str_replace(' ', '-', ?)
    |> strtolower(...);
// string(16) "php-8.6-released"

A few rules to keep in mind:

  • The arguments you do pass are evaluated when the partial is created, not when it is called.
  • The closure keeps the parameter names, types and by-reference flags of the original function.
  • new cannot be partially applied, but static factory methods can.
  • A positional placeholder cannot follow a named argument.
  • Functions that inspect their caller, such as compact(), extract() and func_get_arg(), are not supported.

The generated closure performs essentially the same as the closure you would have written by hand, and a partial placed directly on the right of |> is optimised away at compile time.

2. clamp()

PHP 8.5 and earlier: Limiting a value to a range needed nested min() and max() calls.

$workers = max(1, min(64, $requested));

PHP 8.6: clamp() returns the value if it is inside the bounds (inclusive), otherwise the nearest bound.

$workers = clamp($requested, min: 1, max: 64);

clamp(101, min: 0, max: 100); // 100
clamp(-5, min: 0, max: 100);  // 0
clamp('a', 'c', 'g');         // "c"

clamp() follows the normal PHP comparison rules, so it works with integers, floats, strings and comparable objects such as DateTimeImmutable. It throws a ValueError when $min is greater than $max, or when either bound is NAN.

Note: clamp is now a reserved function name. If your code base or a dependency declares a global clamp() function, it needs to be renamed or guarded with function_exists().

3. Time\Duration

PHP 8.5 and earlier: A span of time was an integer or a float, and the unit lived in the variable name or the documentation.

$timeout = 1.5;      // seconds? milliseconds?
$retryAfter = 500;   // milliseconds, hopefully

PHP 8.6: Time\Duration is a final, readonly value object with nanosecond precision. It has a named constructor per unit, arithmetic methods, and it supports the comparison operators.

use Time\Duration;

$timeout = Duration::fromMilliseconds(1500);
$retryAfter = Duration::fromSeconds(2);

$backoff = $retryAfter->multiplyBy(3);
$total = $timeout->add($backoff);

var_dump($timeout < $retryAfter);
// bool(true)

This is the kind of type that long-running servers benefit from most. Timeouts, heartbeats and retry intervals are everywhere in an OpenSwoole application, and mixing up seconds and milliseconds is a classic source of bugs.

4. Default Values for Readonly Properties

PHP 8.5 and earlier: A readonly property could not have a default value. It had to be assigned in the constructor.

final class HealthCheck
{
    public readonly string $path;

    public function __construct()
    {
        $this->path = '/health';
    }
}

PHP 8.6: Readonly properties accept a default value. The readonly rules are unchanged, so the property still cannot be reassigned afterwards.

final class HealthCheck
{
    public readonly string $path = '/health';
    public readonly int $port = 9501;
}

5. Doc Comments on Function Parameters

PHP 8.6 adds ReflectionParameter::getDocComment(), so a doc comment can be attached to a single parameter and read back at runtime. This is useful for frameworks that generate API documentation, CLI help or validation rules from signatures.

function search(
    /** Terms to search for in the index */
    string $query,
    /** Maximum number of results to return */
    int $limit = 10,
): array {
    return [];
}

$parameter = new ReflectionParameter('search', 'query');
echo $parameter->getDocComment();
// /** Terms to search for in the index */

6. Polling API

PHP 8.5 and earlier: The only way to wait for I/O readiness in core PHP was stream_select(), which is typically limited to around 1024 file descriptors and gets slower as the number of connections grows.

PHP 8.6: The new Io\Poll namespace gives PHP a portable polling API backed by epoll on Linux, kqueue on BSD and macOS, event ports on Solaris and WSAPoll on Windows, with poll() as the fallback.

use Io\Poll\{Context, Event};

$server = stream_socket_server('tcp://0.0.0.0:8080');
stream_set_blocking($server, false);

$poll = new Context();
$poll->add(new StreamPollHandle($server), [Event::Read]);

foreach ($poll->wait(1) as $watcher) {
    if ($watcher->hasTriggered(Event::Read)) {
        $connection = stream_socket_accept($server);
    }
}

It is a building block rather than an event loop: there are no timers, no coroutines and no scheduler. We look at it in detail, and compare it with the OpenSwoole reactor, in PHP 8.6 Polling API vs the OpenSwoole Reactor.

7. Smaller Additions

  • SortDirection enum: a global enum with Ascending and Descending cases. Nothing in core accepts it yet, but libraries can start using a shared type.
  • __debugInfo() on enums: enums can now control how they appear in var_dump().
  • Stream error handling: a new error_mode stream context option and stream_last_errors() give structured access to stream errors instead of parsing warnings.
  • URI extension follow-ups: a Uri\Rfc3986\UriBuilder and more helpers for the URI extension introduced in PHP 8.5.
  • json_decode() error location: error and exception messages now say where in the input the problem is.
  • grapheme_strrev(): reverses a string by grapheme cluster.

Behaviour Changes to Check

New Session Defaults

The default session configuration is stricter in PHP 8.6:

SettingPHP 8.5PHP 8.6
session.use_strict_mode01
session.cookie_httponly01
session.cookie_samesitenot setLax

If your application relies on reading the session cookie from JavaScript, or on accepting session IDs supplied by the client, set these values explicitly.

Other Changes

  • trim(), ltrim(), rtrim() and chop() now also strip the form feed character (\f) by default.
  • array_filter() throws a ValueError when $mode is not a valid value.
  • clamp can no longer be declared as a userland function name.

Deprecations

PHP 8.6 deprecates a long list of features. These are the ones most likely to show up in application code:

  • Returning from a finally block
  • Returning a value from a constructor or destructor
  • is_double(), is_integer(), is_long() and doubleval(): use is_float(), is_int() and floatval()
  • The $case_insensitive parameter of define()
  • spl_object_hash(): use spl_object_id()
  • spl_classes(): use ReflectionExtension::getClassNames()
  • strcoll(), metaphone() and the SORT_LOCALE_STRING sort flag
  • The CSV methods on SplFileObject: fgetcsv(), fputcsv(), setCsvControl() and getCsvControl()
  • The sorting, flag and serialisation methods on ArrayIterator
  • mysqli::stmt_init(), mysqli_get_charset() and mysqli::get_charset()
  • Using let or is as an identifier, _ as a constant name, and readonly as a function name

Deprecations only raise a notice in PHP 8.6, but they are scheduled to become errors in a later major version. Run your test suite with E_DEPRECATED enabled before you upgrade production.

How to Prepare

  1. Run your tests against a PHP 8.6 release candidate. Release candidates are not for production use.
  2. Search your code and dependencies for a global clamp() function.
  3. Review the session settings if you rely on the old defaults.
  4. Fix deprecation notices now, while they are still notices.

OpenSwoole and PHP 8.6

OpenSwoole 26.2.0 requires PHP 8.3 or later and is fully compatible with PHP 8.5. See the supported versions page and the release announcements for the PHP versions each OpenSwoole release supports.