Discover the main new features in PHP 8.6

Marcos Marcolin • August 17, 2026 • 8 min read

PHP PHP 8.6 PHP RFC

PHP 8.6 is still under development and testing, with a planned release for the second half of November 2026. The information in this post may change until then, as RFCs get voted on and implemented.

In this post, I bring a summary of the main improvements in 8.6, based on the RFCs already accepted. I picked out what makes the biggest difference day-to-day, since a lot is developed throughout the year to make the language simpler, faster, and more consistent.

Contents

Polling API: a native foundation for async I/O

RFC

PHP 8.6 brings the new Io\Poll namespace, with a native polling API for I/O multiplexing, promising better performance than the traditional stream_select(). The idea is to offer a common foundation that low-level libraries, such as ReactPHP and Amp, can use to manage multiple streams, sockets, and other pollable resources.

PHP 8.5 and earlier:

$read = [$server];
$write = $except = [];

stream_select($read, $write, $except, 1);

foreach ($read as $stream) {
    // handle the read event
}

PHP 8.6:

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], ['type' => 'server']);

while (true) {
    foreach ($poll->wait(timeoutSeconds: 1) as $watcher) {
        if ($watcher->getData()['type'] === 'server' && $watcher->hasTriggered(Event::Read)) {
            $client = stream_socket_accept($server);
            $poll->add(new StreamPollHandle($client), [Event::Read], ['type' => 'client']);
        }
    }
}

For now, PHP 8.6 ships the foundation of the API. More specific handles, such as SocketPollHandle, CurlPollHandle, TimerHandle, and SignalHandle, are left for future versions.

Partial Function Application: filling in arguments ahead of time

main RFC · companion RFC

PHP 8.6 introduces partial function application, allowing you to create closures from a function call by filling in some arguments and leaving others for later. This is done with the ? (single argument) and ... (remaining arguments, variadic-style) placeholders. A companion RFC made the behavior more predictable: every ? placeholder now always generates a required parameter in the resulting closure.

This feature is a natural extension of the first-class callable syntax added in PHP 8.1, and it pairs really well with the pipe operator introduced in PHP 8.5.

PHP 8.5 and earlier:

function convertCurrency(float $amount, string $from, string $to): float { /* ... */ }

$toBRL = fn (float $amount, string $from): float => convertCurrency($amount, $from, 'BRL');

PHP 8.6:

function convertCurrency(float $amount, string $from, string $to): float { /* ... */ }

$toBRL = convertCurrency(?, ?, 'BRL');
$toBRL(100.0, 'USD');
// Equivalent to: fn(float $amount, string $from): float => convertCurrency($amount, $from, 'BRL');

$prices = [19.90, 249.00, 5.50];
$pricesInBRL = array_map(convertCurrency(?, 'USD', 'BRL'), $prices);

Worth noting that constructors aren't supported: you can't create a partial closure from a new expression. The reason, according to the RFC itself, is that constructors are invoked indirectly by the engine, which would make supporting them notably more work, and the use cases for it are rare, especially now that PHP 8.4 has lazy objects.

The clamp() function: restricting values to a range

RFC

PHP 8.6 gets the native clamp() function, which restricts a value to a minimum and maximum range. If the value is outside the range, it returns the nearest bound; otherwise, it returns the value itself. It's such a common case that it always ended up as some combination of min() and max() scattered across the codebase.

PHP 8.5 and earlier:

$zoom = max(50, min(200, $zoom));

PHP 8.6:

$zoom = clamp($zoom, min: 50, max: 200);

clamp(30, min: 50, max: 200); // 50
clamp(120, min: 50, max: 200); // 120
clamp(250, min: 50, max: 200); // 200

More secure session defaults

RFC

Three ext/session directives had security implications, but shipped with default values that left applications unnecessarily exposed. PHP 8.6 changes these defaults so new installs are more secure out of the box, with no code changes needed for the common case.

Stream error handling

RFC

Error handling for streams has always been a bit inconsistent in PHP: each wrapper reports failures its own way (a warning, a notice, or just a plain false with no context), which makes it hard to know exactly what went wrong. PHP 8.6 adds a structured, opt-in system for this, with the new StreamErrorMode enum and the StreamError class.

PHP 8.5 and earlier:

$stream = @fopen('file.txt', 'r');

if ($stream === false) {
    // no structured information about why it failed
}

PHP 8.6:

$context = stream_context_create([
    'stream' => ['error_mode' => StreamErrorMode::Exception],
]);

try {
    $stream = fopen('file.txt', 'r', context: $context);
} catch (StreamException $e) {
    $error = $e->getErrors()[0];
    echo $error->code->name; // e.g., "NotFound"
}

The default behavior stays the same as today (StreamErrorMode::Error), so nothing changes for code that doesn't opt into the new mode. PHP 8.6 also adds the stream_last_errors() and stream_clear_errors() functions, to inspect and clear stored errors outside of a try/catch block.

Additional features and improvements

Besides the main features in PHP 8.6, several smaller improvements were added to make development safer, more consistent, and more predictable.

Default values for readonly properties

RFC

readonly properties can now have a default value, which is especially useful combined with the interface properties introduced in PHP 8.4, letting you declare fixed metadata without needing a constructor:

final readonly class InvoiceExporter implements ExporterBlueprint
{
    public string $name = 'Invoice Exporter';
    public string $format = 'pdf';
}

New Time\Duration class

RFC

Class that represents a fixed, context-free time span (unlike DateInterval), useful for timeouts, delays, and measurements. It ships with methods like Duration::fromSeconds(), add(), and divideBy().

#[\Override] now also for class constants

RFC

The #[\Override] attribute, which already worked on methods and properties, can now also be used on class constants, helping guarantee that a constant is really overriding one from the parent class.

Writing to properties of objects referenced by constants

RFC

It's now possible to change properties of an object held in a class constant, while keeping the constant itself immutable (that is, the reference to the object can't be swapped, only its internal state):

const BACKING = new stdClass();

class C
{
    const O = BACKING;
}

C::O->prop = 42;
var_dump(C::O->prop);
// int(42)

More expressive enums: SortDirection and custom debug output

RFC · RFC

PHP 8.6 brings a native SortDirection enum, with the cases Ascending and Descending, meant for frameworks and libraries to use instead of loose strings when sorting data:

$query->orderBy('created_at', SortDirection::Descending);

Enums can also now implement __debugInfo(), just like regular classes, customizing the output of var_dump():

enum OrderStatus: string
{
    case Paid = 'paid';

    public function __debugInfo(): array
    {
        return [self::class . '::' . $this->name => $this->value];
    }
}

var_dump(OrderStatus::Paid);
// enum(OrderStatus::Paid) (1) {
//   ["OrderStatus::Paid"]=>
//   string(4) "paid"
// }

json_decode() errors with an exact location

Syntax errors in json_decode() now point to the exact line and position (line:column) where the problem occurred, both in the message returned by json_last_error_msg() and in the exception thrown with JSON_THROW_ON_ERROR. Previously, the message only described the type of error, without pointing to where it was in the JSON:

json_decode('[{');
json_last_error_msg();
// PHP 8.5 and earlier: "Syntax error"
// PHP 8.6: "Syntax error near location 1:3"

This makes it a lot easier to debug malformed API payloads, webhooks, or configuration files.

New in Reflection, enums, and functions

Deprecations and backwards-compatibility breaks

Syntax and reserved words

Form feed is now whitespace in trim()

The trim(), ltrim(), and rtrim() functions now also strip the form feed character (\f, ASCII 12) by default, alongside space, tab, newline, carriage return, and NUL. This is a backwards-compatibility break: code that relied on preserving this character at the edges of a string needs to be reviewed.

Objects passed to functions that expect arrays

Passing objects to functions that expect an array, such as array_walk(), array_walk_recursive(), deflate_init(), inflate_init(), the zlib and bzip2 stream filters, mb_convert_variables(), and http_build_query(), is now deprecated. Use get_object_vars() to convert the object before passing it in.

Deprecated functions and methods

Final thoughts

In my view, PHP 8.6 reinforces just how consistently the language keeps evolving. The new features are useful in day-to-day work, solve real problems, and make code clearer and more modern.

This post will keep getting updated as new RFCs get voted on and accepted until the official release. You can follow the schedule and all the RFCs on the official PHP site.

Until next time!

Share: