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.
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:
new cannot be partially applied, but static factory methods can.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.
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().
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.
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;
}
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 */
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.
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().error_mode stream context option and stream_last_errors() give structured access to stream errors instead of parsing warnings.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.The default session configuration is stricter in PHP 8.6:
| Setting | PHP 8.5 | PHP 8.6 |
|---|---|---|
session.use_strict_mode | 0 | 1 |
session.cookie_httponly | 0 | 1 |
session.cookie_samesite | not set | Lax |
If your application relies on reading the session cookie from JavaScript, or on accepting session IDs supplied by the client, set these values explicitly.
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.PHP 8.6 deprecates a long list of features. These are the ones most likely to show up in application code:
finally blockis_double(), is_integer(), is_long() and doubleval(): use is_float(), is_int() and floatval()$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 flagSplFileObject: fgetcsv(), fputcsv(), setCsvControl() and getCsvControl()ArrayIteratormysqli::stmt_init(), mysqli_get_charset() and mysqli::get_charset()let or is as an identifier, _ as a constant name, and readonly as a function nameDeprecations 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.
clamp() function.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.
Join 4,000+ others and never miss out on new tips, tutorials, and more.