stitcher.io
What's new in PHP 8.6
Written on 2026-07-29
PHP 8.6 will be released on November 19, 2026. It includes partial function application, a new polling API, function parameter doc comments, a bunch of deprecations, and more.
By the way! If you haven't yet participated in the State of PHP survey, maybe this is a very good time to spend 15-20 minutes on it. This survey is a joint effort between the PHP Foundation and JetBrains; our goal is to get the best picture possible of "the PHP community". Over 8000 developers have already participated, but we'd like to see more.
---
Partial function application
Partial function application — PFA for short — allows you to create a reference to a closure with some of its parameters prefilled. A simple example is a function to replace all spaces in a string with dashes:
$makeSlug = str_replace(' ', '-', ?);
Once a closure is created, you can call it like so:
$makeSlug('Hello World');
// Hello-World
PFA is especially useful when combined with the pipe operator, because the pipe operator always requires a callable with exactly one parameter.
$output = 'Hello World' |> str_replace(' ', '-', ?) |> strtolower(...);
// hello-world
You can read all about partial function application in this post.
---
Readonly property defaults
With the addition of property hooks in PHP 8.4, you can define property hooks on interfaces:
interface MigratesUp { public string $name { get; }
public function up(): QueryStatement; }
Because of this change though, readonly properties with default values would make sense in many cases:
final class CreateBooksTable implements MigratesUp { public readonly string $name = '2026-01-01_create_books_table'; public function up(): QueryStatement { / … / } }
However, prior to PHP 8.6, you could not assign default values to readonly properties. This was a deliberate design choice when readonly properties were added because a readonly property with a default value is essentially a constant. Of course that was before property hooks could be defined on interfaces, because now a default, unchangeable value does make sense if it's part of a bigger contract.
And that's why default values for readonly properties is now allowed!
final class CreateBooksTable implements MigratesUp { // ✅ public readonly string $name = '2026-01-01_create_books_table'; // … }
---
Polling API
The new Polling API was created first and foremost to facilitate easier internal development. Features like PHP-FPM and singal-handling in ZTS (Zend Thread Safety) mode will benefit from a unified platform to build upon. However, the new polling API is also exposed to userland, which could lead to lower-level frameworks like ReactPHP or Amp to make use of them:
use Io\Poll\Context; use Io\Poll\Event; use Io\Poll\StreamPollHandle; // Create a poll context with automatic backend selection $context = new Context();
// Create a non-blocking socket, just like before $stream = stream_socket_server('tcp://0.0.0.0:8080', $errno, $errstr); stream_set_blocking($stream, false);
// Wrap that stream in a new `StreamPollHandle` so that it can make use of the new API $handle = new StreamPollHandle($stream); // Add the handle to the context $context->add($handle, [Event::Read], ['type' => 'server']);
while (true) { // Wait for one second, polling for new events $watchers = $context->wait(1); // … }
It's imortant to note that this new polling API won't introduce any new async features to PHP. It's another (and easier) way to interact with async features — previously PHP only had `stream_select()` as an option for I/O multiplexing. Because the new API also works with several backends when available like epoll or WSAPoll, performance will be better compared to `stream_select()` at a certain scale.
The new polling API also doesn't come with a built-in event loop, so wrapping it into a higher-level abstraction is still up to userland libraries.
---
A new clamp function
`clamp()` is a pretty common function in many frameworks already, which will now ship built-in with PHP 8.6. This function ensures a given value (numeric or other) is within a given bound. If not, the nearest edge value is returned.
clamp(10, min: 0, max: 100); // Will return `10` clamp(101, min: 0, max: 100); // Will return `100` clamp(-1, min: 0, max: 100); // Will return `0`
`clamp()` works on more than integers. For example strings:
clamp("y", "x", "z") // Will return "y" clamp("a", "x", "z") // Will return "x"
Or `DateTime` objects:
clamp( value: new DateTimeImmutable('2025-01-01'), min: new DateTimeImmutable('2026-01-01'), max: new DateTimeImmutable('2026-12-31'), ); // Will return `DateTimeImmutable('2026-01-01')`
---
New `isReadable` and `isWriteable` reflection functions
There are two new functions to indicate whether a reflection property is readonly and/or writeable. With the addition of property hooks in PHP 8.4, it makes sense to have these methods in PHP's reflection API:
final class Book { private(set) string $title; }
$property = new ReflectionProperty(Book::class, 'title');
$property->isReadable(scope: Book::class); $property->isWriteable(scope: null);
Most important is that `$scope` variable, as it determines from which scope the property can be read or written. For example: a private property can be read from within the class itself, but not from the outside:
$property->isWriteable(scope: Book::class); // true $property->isWriteable(scope: null); // false
Optionally, you can also pass in a second `$object` parameter. If you do, the reflection API can determine whether a readonly property has already been set or not, which will also determine whether it's writeable or not.
final class Book { public readonly string $title; }
$book = new Book();
$property = new ReflectionProperty(Book::class, 'title');
$property->isWriteable(scope: null, object: $book); // true
$book->title = 'Timeline Taxi';
$property->isWriteable(scope: null, object: $book); // false
---
Function parameter doc comments
PHP's reflection API has been extended with a `getDocComment()` method on `ReflectionParameter`. In practice that means that you can rewrite this:
/* @param Book[] $books / function store(array $books): void { / … / }
As this:
function store( /* @param Book[] / array $books, ): void { / … / }
Of course, you can also write a shorter one-line version by adding the doc block on either side of the parameter:
function store(/* @param Book[] / array $books): void { / … / } function store(array $books /* @param Book[] /): void { / … / }
Note that if the doc comment goes behind the parameter, it should go before the comma separating the parameter from the next one:
function store( array $books /* valid placement to link to $books /, array $other, /* this one won't be detected / ): void { / … / }
While this feature will likely be most useful for static analyzers, you can of course use reflection to read these parameter doc comments:
$reflection = / … a method or function reflector /
$parameters = $reflection->getParameters;
$parameters[0]->getDocComment();
---
A new SortDirection enum
PHP 8.6 comes with a new built-in enum to represent sort directions:
enum SortDirection { case Ascending; case Descending; }
Frameworks and libraries are free to opt-in to support this enum in anything that needs sorting, and then map the `SortDirection` to whatever is needed in that context. Note that built-in PHP functions like `array_multisort()` or `scandir()` don't yet support this enum, but the plan is to do that in the future.
---
Improved security for session defaults
PHP changes a couple of default ini setting values to make sessions more secure out of the box: both `session.use_strict_mode` and `session.cookie_httponly` will default to `1` instead of `0`; and `session.cookie_samesite` will also be set to `Lax` instead of not being set at all. The RFC provides an overview of how your projects may be affected by these changes, I'll list them here as well:
#### `session_use_strict_mode`
Applications that deliberately supply an externally controlled session ID — for example, a shared-secret hand-off between subdomains using the files save handler — will have the ID rejected because no matching session file exists at the time of the first request. The correct approach is to write and close the session on the originating side with `session_write_close()` before presenting the ID to the receiving side.
Applications using a custom session handler whose `validateId()` method returns true unconditionally are unaffected. This includes common backends such as Redis and Memcached when using the default php-memcached or phpredis session handlers, which do not implement `validateId()` and therefore fall back to returning true.
#### `session.cookie_httponly`
The `HttpOnly` flag is enforced by the browser. It prevents the session cookie value from being read via `document.cookie;` it has no effect on whether the browser sends the cookie with requests.
Applications that read the session cookie value from `document.cookie` in JavaScript (for example, to embed the ID in a custom request header) will no longer be able to do so. The session ID is a server-side credential and should not be consumed by client-side code. Applications that need a client-accessible token for request correlation should use a separate, explicitly non-HttpOnly CSRF token rather than the session cookie itself.
#### `session.cookie_samesite`
With `SameSite=Lax`, the browser sends the session cookie on same-site requests and on top-level cross-site navigations using safe HTTP methods (`GET` and `HEAD`). It does not send the cookie on cross-site sub-resource requests or cross-site `POST` requests.
Applications that rely on cross-site `POST` carrying the session cookie — for example, SP-initiated SAML SSO flows or legacy cross-origin form submissions — must either set `SameSite=None; Secure` explicitly for those endpoints or migrate to a token-based flow.
As noted above, Chrome and Firefox already apply `Lax` as the implicit default for cookies sent without a `SameSite` attribute. Applications running on those browsers are already subject to this behaviour; the change makes it explicit and consistent across all browsers and PHP versions.
---
Debugable enums
Enums can now implement the `__debugInfo()` magic method which means you can provide your own debug implementation for enums:
enum Status: int { case OK = 200; case NOT_FOUND = 404; case FOUND = 302; case INTERNAL_SERVER_ERROR = 500; public function __debugInfo() { return [_CLASS_ . '::' . $this->name . ' = ' . $this->value]; } } var_dump(Status::OK);
// enum(Status::OK) (1) { // [0]=> // string(16) "Status::OK = 200" // }
---
Deprecations
As usual, any minor PHP release comes with a bunch of deprecations: warnings that something will change or be removed in a future major version of PHP. It's good to fix deprecations already today, before they become an actual problem in the future.
#### Returning from a `finally` block
Returning from a `finally` has always been very confusing and a cause of subtle bugs. This is deprecated now:
function getConfig(): array { try { return loadConfig(); } finally { return []; } }
#### Deprecate using `let` as an identifier
The motivation for this deprecation is to be able to use `let` as a possible future keyword:
class let { / … / }
function let() {}
#### Deprecate using `is` as an identifier
The motivation for this deprecation is to be able to use `is` in the future in combination with the `match` operator:
class is { / … / }
function is() {}
#### Deprecate using `namespace` as a class constant name
The motivation is to reserve `namespace` for future syntax, for example, it would allow a future `::namespace` pseudo-constant, analogous to `::class`
class Foo { const NAMESPACE = ''; }
#### Deprecate the possibility to name a function `readonly`
`readonly` was initially allowed as a function name because WordPress had a function with the same name. This was achieved with a hack in PHP's lexer, which is now being deprecated:
function readonly { / … / }
#### Deprecate using `_` as a constant and compile time alias
This follows a PHP 8.4 deprecation where a single underscore was already deprecated for class names.
const _ = 1;
#### Passing objects which are interpreted as arrays
Some functions like `array_walk()` or `inflate_init()` accept objects alongside arrays. The internal way this is implemented can cause subtle bugs and lead to corrupted memory. That's why the behviour is deprecated in PHP 8.6:
$objectInsteadOfArray = new / … /;
array_walk($objectInsteadOfArray, fn () => / … /)
#### Deprecate `is_double`
Instead of `is_double()`, `is_float()` should be used:
if (is_double($value)) { / … / }
#### Deprecate `is_integer`
Instead of `is_integer()`, `is_int()` should be used:
if (is_integer($value)) { / … / }
#### Deprecate `is_long`
Instead of `is_long()`, `is_int()` should be used:
if (is_long($value)) { / … / }
#### Deprecate `doubleval`
Instead of `doubleval()`, `floatval()` should be used:
if (doubleval($value)) { / … / }
#### Deprecate `$case_insensitive` for `define` function
Support for case insensitive constants has been long removed.
define('MY_CONSTANT', 'value', case_insensitive: true);
#### Deprecate `is_subclass_of` and `is_a` when `$allow_string` is `false`
`is_subclass_of()` is used to check if an object or class name is a subclass of some other named class/interface. The first parameter can be either an object to check, or the name of a class. The third parameter, `$allow_string`, controls whether the first parameter can be a string. The reason this is useful is that when set to false, strings will not trigger autoloading. However, if a string is given when `$allow_string` is `false`, the function will always return `false`, even if the class does not need to be autoloaded.
If a developer has specified that strings should not be allowed (the default is that they are allowed), then passing strings indicates a bug, which is why it's been deprecated.
is_subclass_of(Foo::class, Bar::class, allow_string: false); is_a(Foo::class, Bar::class, allow_string: false);
#### Deprecate `strcoll`
This function is now deprecated:
strcoll($a, $b);
#### Deprecate `SORT_LOCALE_STRING` flag for `sort` functions
sort($array, flags: SORT_LOCALE_STRING);
#### Deprecate the `metaphone` function
This function could be used to determine similar sounding words. However, it's based on a very old algorithm and only supports English. Instead, we're encouraged to rely on more modern userland implementations like noodlesnz/double-metaphone.
#### Deprecate setting `ReflectionProperty` values with wrong types
The ability to use a reflection property of one class to set a property on another class has been deprecated:
class Original { private $property; } $property = new ReflectionProperty(Original::class, 'property');
Note that `$property` refers to `Original::$property`. However, you could use that same relfection property to set values on other objects:
class Other {}
$other = new Other();
$property->setValue($other, true);
#### Deprecate static invoking via reflection on actual objects
Both `ReflectionMethod::invoke()` and `ReflectionMethod::invokeArgs()` take an optional object when calling instance methods. If you're calling a static method, `null` should be passed instead:
class Book { public static function create() {} }
$method = new ReflectionMethod(Book::class, 'create');
$method->invoke(null);
However, you're still able to pass in an object when calling a static method, even though it doesn't do anything. That behaviour has been deprecated:
$book = new Book();
$method = new ReflectionMethod(Book::class, 'create');
$method->invoke($book); // Only if the method is static
#### Deprecated `ArrayIterator` methods
From the RFC:
Various \`ArrayIterator\` methods only exist because for a long time it shared a common implementation with \`ArrayObject\`. Most of these methods do not make much sense, and prevent optimizing the implementation of \`ArrayIterator\`.
These methods have been deprecated:
ArrayIterator::getFlags() ArrayIterator::setFlags() ArrayIterator::asort() ArrayIterator::ksort() ArrayIterator::uasort() ArrayIterator::uksort() ArrayIterator::natsort() ArrayIterator::natcasesort() ArrayIterator::unserialize() ArrayIterator::serialize()
#### Deprecate `spl_classes`
The `spl_classes()` has long had a better alternative with `ReflectionExtension::getClassNames()`. So the function is now deprecated:
spl_classes();
// Use this one instead: new ReflectionExtension('spl')->getClassNames();
#### Deprecate `spl_object_hash`
Instead of `spl_object_hash()`, you should usen `spl_object_id()`.
spl_object_hash($object);
If you still need an actual hash instead of the object's ID, you can generate it like so:
$hash = $obj |> spl_object_id(...) |> dechex(...) |> str_pad(?, 16, '0', STR_PAD_LEFT) |> (fn ($x) => $x . '0000000000000000');
#### Deprecate csv-specific methods in `SplFileObject`
These methods are deprecated:
SplFileObject::fgetcsv() SplFileObject::fputcsv() SplFileObject::setCsvControl() SplFileObject::getCsvControl()
The RFC lists the following reason:
These APIs have become increasingly difficult to maintain due to historical design issues and inconsistencies. The introduction of named arguments has further exposed problems in the API design and behavior.
CSV processing functionality does not naturally belong in SplFileObject, and future work would be better served by a dedicated CSV extension providing a cleaner and more maintainable API.
#### Deprecate `mysqli::stmt_init`
$statement = $mysqli->stmt_init(); $statement->prepare($sql);
// Instead do this: $statement = $mysqli->prepare($sql);
#### Deprecate `mysqli_get_charset`
From the RFC:
This function can be used to retrieve internal implementation details of the currently selected character set. It was able to provide more meaningful values when mysqli was compiled against libmysql, but since PHP 8.2 that's no longer the case.
The proposal is to deprecate and remove this function, and its OO-style alias \`mysqli::get\_charset()\`, and later remove it without a substitute. It's pretty unlikely that any PHP project still uses this function, and if they do, they probably don't know that it returns falsified information.
#### Deprecate passing invalid session handlers to `session_set_save_handler`
From the RFC:
User-defined session handlers aren't forced to implement methods that would ensure that the session extension is well behaved in regards to the session.use_strict_mode INI setting. This is important as the default value for it has been changed in the "Secure Session Configuration Defaults" RFC, and should never be disabled.
The methods which are required for the behaviour to be well-defined are \`create\_sid()\` and \`validateId()\`.
#### Deprecate returning from constructors and destructors
While this used to be technically allowed, a return statement from a constructor or destructor never made any sense, since both functions can only be called in contexts where you couldn't store that return value anyway. That's why the behaviour is now deprecated:
class Book { public function __construct() { return 1; } public function __destruct() { return 0; } }
---
That's it for now! This list may still update as PHP 8.6 is still being prepared for feature freeze. Subscribe to my newsletter to stay up to date!
Also a final reminder that you can still participate in the State of PHP survey. This survey is a joint effort between the PHP Foundation and JetBrains, and our goal is to get the best picture as possible of "the PHP community". Over 8000 developers have already participated, and I hope you can pitch in as well!

