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;
use Time\Duration;
 
// 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(new Duration::fromSeconds(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 Duration class

There's a new \Time\Duration class added to represent a duration of time:

use Time\Duration;

sleep(Duration::fromMilliseconds(500));

The class comes with a bunch of methods to do mathematical operations with durations:

$baseDelay = Duration::fromMilliseconds(100);

$baseDelay->add(Duration::fromSeconds(2));

$attempt = 5;
$delay = $baseDelay->multiplyBy(2 ** $attempt);

You can also compare two durations directly:

if ($durationA < $duractionB) {
    /* … */
}

And you'll be able to use it in places like with the new polling API:

$watchers = $context->wait(new Duration::fromSeconds(1));

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.usestrictmode 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!

Things I wish I knew when I started programming

Things I wish I knew when I started programming cover image

This is my newest book aimed at programmers of any skill level. This book isn't about patterns, principles, or best practices; there's actually barely any code in it. It's about the many things I've learned along the way being a professional programmer, and about the many, many mistakes I made along that way as well. It's what I wish someone would have told me years ago, and I hope it might inspire you.

Read more

Comments

Loading…
No comments yet, be the first!
Noticed a tpyo? You can submit a PR to fix it.
Home RSS Newsletter Discord © 2026 stitcher.io Login