Discover the main new features in PHP 8.6

Marcos Marcolin • August 17, 2026 • 7 min read

PHP PHP 8.6 PHP RFC

What's new in PHP 8.6 🐘

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.

If you follow this blog or my LinkedIn, you've probably already seen some of these new features, since I usually cover the most important RFCs and changes throughout the year.

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. I'll keep updating this post as new RFCs get voted on and implemented.

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.

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

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

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"
// }

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: