Get "PHP 8 in a Nutshell" (Now with PHP 8.5)
Amit Merchant
Amit Merchant

A blog on PHP, JavaScript, and more

What's new in PHP 8.6

PHP 8.5 has brought us features such as the pipe operator, array_first(), array_last(), the URI extension, and the #[\NoDiscard] attribute.

PHP 8.6 is shaping up to be another interesting release. It contains a mix of language improvements, new standard-library functions, better networking APIs, security improvements, and a few useful internal optimizations. It’s general availability is currently scheduled for November 19, 2026.

So, it’s a good time to look at everything that has made it into PHP 8.6 so far.

This article is based on the features currently listed in PHP’s implemented PHP 8.6 RFCs. Some details may still change before the final release but it should largely remain the same.

Direction-aware property resolution

The Direction-Aware Property Resolution RFC is an engine-level improvement for asymmetric properties introduced in PHP 8.4.

Asymmetric visibility lets you make a property publicly readable but restrict who can modify it:

class User
{
    public private(set) string $name;
}

The property can be read from outside the class, but it can only be changed inside the class.

Previously, PHP had to check the write visibility every time the property was modified. That made writes to private(set) and protected(set) properties more expensive than writes to ordinary public properties.

PHP 8.6 moves this check to the runtime-cache population phase. Once PHP knows that a particular call site has write access, subsequent writes can use the cached result.

The syntax does not change. The improvement is mostly invisible to application code, but it makes asymmetric properties cheaper to use, especially in loops, object hydration, serializers, and builders.

class Counter
{
    public private(set) int $value = 0;

    public function increment(): void
    {
        $this->value++;
    }
}

This is a performance improvement rather than a new language feature. Existing behavior remains the same.

Explicit endianness for pack() and unpack()

PHP 8.6 extends the format-string syntax used by pack() and unpack().

Signed integers

Until now, PHP supported machine-endian signed integers, but not signed integers with an explicitly chosen byte order.

PHP 8.6 adds < and > modifiers:

$bytes = pack('s<l<q<', -258, -16909060, -72340172838076673);

The modifiers mean:

  • s< — signed 16-bit little-endian integer
  • s> — signed 16-bit big-endian integer
  • l< — signed 32-bit little-endian integer
  • l> — signed 32-bit big-endian integer
  • q< — signed 64-bit little-endian integer
  • q> — signed 64-bit big-endian integer

They also work with unsigned machine-endian formats:

$bytes = pack('S<L>Q<', 258, 16909060, 72340172838076673);

This is particularly useful when working with binary protocols and file formats.

Existing format codes such as v, n, V, and N continue to work as before. Applying an endianness modifier to a format that already has an inherent byte order throws a ValueError.

Floating-point values

The same syntax is also being added for floating-point values through the floating-point endianness RFC:

$bytes = pack('f<d>', 3.14159, 2.71828);

The supported formats are:

  • f< — little-endian single-precision float
  • f> — big-endian single-precision float
  • d< — little-endian double-precision float
  • d> — big-endian double-precision float

For example:

$bytes = pack('f>d>', 3.14159, 2.71828);

[$float, $double] = array_values(
    unpack('f<a/d<b', $bytes)
);

The new syntax is equivalent to the existing format letters:

pack('f<', 3.14) === pack('g', 3.14);
pack('f>', 3.14) === pack('G', 3.14);

pack('d<', 3.14) === pack('e', 3.14);
pack('d>', 3.14) === pack('E', 3.14);

Better stream error handling

PHP stream operations have historically reported errors in several different ways. Depending on the stream wrapper and operation, you might get a warning, a notice, a failed return value, or no particularly useful structured information at all.

The Stream Error Handling Improvements RFC introduces a more consistent model.

A stream context can now define how errors should be handled:

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

The available modes are:

  • StreamErrorMode::Error — use normal PHP warnings and notices
  • StreamErrorMode::Exception — throw exceptions for terminating errors
  • StreamErrorMode::Silent — suppress error output

For example:

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

$content = file_get_contents('/path/to/missing-file', context: $context);

In exception mode, errors that prevent an operation from completing can throw a StreamException.

The RFC also adds structured error storage, error codes, and support for custom error callbacks. This should make stream-heavy applications easier to debug and easier to integrate with normal exception-based error handling.

The new behavior must be enabled through an explicit context. It cannot be applied globally through the default stream context, which helps preserve compatibility with existing applications.

Writing to object properties referenced by constants

PHP constants cannot contain arrays that are modified in place, but an object stored in a constant is still a mutable object.

PHP 8.6 allows property operations on objects referenced by constants, as described in the Const Object Property Write RFC.

const CONFIG = new stdClass();

CONFIG->environment = 'production';
CONFIG->debug = false;

The constant binding itself remains immutable. You cannot make CONFIG refer to a different object, but you can modify the object it points to.

The change also covers related operations:

CONFIG->counter++;

isset(CONFIG->debug);

unset(CONFIG->debug);

CONFIG->name .= ' application';

This is intentionally limited to objects. Array constants remain immutable.

A limit for stream filter chains

Stream filters can be chained together:

$stream = fopen('php://memory', 'w+');

stream_filter_append($stream, 'string.toupper');
stream_filter_append($stream, 'string.rot13');

Because filters can be created dynamically, an application could previously build an unnecessarily large filter chain. PHP 8.6 adds a maximum number of filters that can be attached to a stream through the Limit Maximum Number of Filter Chains RFC.

This gives PHP a defensive upper bound and helps reduce the risk of excessive memory usage or denial-of-service scenarios caused by unbounded filter creation.

Normal applications should not need to change anything. The limit mainly protects code that accepts filter configuration from untrusted input.

#[\Override] for class constants

PHP 8.3 introduced the #[\Override] attribute for methods. PHP 8.6 extends it to class constants through the Override for Class Constants RFC.

class Base
{
    protected const TYPE = 'base';
}

class Child extends Base
{
    #[\Override]
    public const TYPE = 'child';
}

If the parent class or interface does not contain a matching constant, PHP reports an error:

class Example
{
    #[\Override]
    public const VALUE = 123;
}

This is useful when a class intentionally overrides a constant from a parent class or interface. It allows PHP to catch typos and accidental API changes early.

The attribute also works with enum cases, since enum cases are internally represented as class constants.

Deprecating return values from constructors and destructors

Constructors and destructors do not return values. Nevertheless, PHP has historically allowed code like this:

class Example
{
    public function __construct()
    {
        return 123;
    }
}

The value is ignored, which can be confusing. PHP 8.6 deprecates returning a value from __construct() and __destruct() through the Deprecate Return Value from Construct RFC.

class Example
{
    public function __construct()
    {
        return 123; // Deprecated
    }

    public function __destruct()
    {
        return 456; // Deprecated
    }
}

A bare return; remains valid because it can be used to exit a constructor or destructor early:

public function __construct()
{
    if (!$this->shouldInitialize()) {
        return;
    }

    $this->initialize();
}

The deprecation is intended to prepare code for a future version of PHP where returning a value from these methods will become an error.

New locale display helpers

The Locale display keyword RFC adds two methods to the Locale class:

Locale::getDisplayKeyword(string $keyword, ?string $displayLocale = null): string|false
Locale::getDisplayKeywordValue(
    string $keyword,
    string $value,
    ?string $displayLocale = null
): string|false

They can be used to display locale keywords and values in a human-readable language.

echo Locale::getDisplayKeyword('collation', 'en');
// Collation

echo Locale::getDisplayKeywordValue('collation', 'phonebook', 'en');
// Phonebook

Procedural aliases are also provided:

locale_get_display_keyword();
locale_get_display_keyword_value();

These methods are useful when building locale-aware settings screens or translating ICU locale metadata.

A new polling API

PHP has had stream_select() for monitoring streams, but it has limitations and does not expose the richer polling facilities available on modern operating systems.

PHP 8.6 introduces the Io\Poll API through the Polling API RFC.

A basic example looks like this:

use Io\Poll\{Context, Event};

$poll = new Context();

$watcher = $poll->add(
    $socket,
    [Event::Readable],
);

while (true) {
    foreach ($poll->wait() as $event) {
        $stream = $event->getHandle()->getStream();

        $data = fread($stream, 8192);

        // Process the incoming data...
    }
}

The API is designed around:

  • polling contexts
  • watchers
  • readable and writable events
  • platform-specific backends
  • callback-based event handling
  • edge-triggered polling where supported

It can use different operating-system backends, such as epoll, kqueue, or poll, depending on the platform.

This gives PHP applications a better foundation for event loops, network servers, proxies, and other long-running I/O workloads.

Debuggable enums

Enums currently have limited output when passed to debugging functions. PHP 8.6 allows enums to define __debugInfo() through the Debuggable Enums RFC.

enum Status: string
{
    case Draft = 'draft';
    case Published = 'published';

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

var_dump(Status::Draft);

This lets an enum control the information shown by functions such as var_dump().

It can be useful for domain-heavy applications where the enum’s name and value are more helpful than PHP’s default representation.

More secure session defaults

PHP 8.6 changes some session configuration defaults through the Secure Session Configuration Defaults RFC.

The most notable change is:

session.use_strict_mode = 1

With strict mode enabled, PHP rejects session IDs that were not created by the server.

This helps protect against session fixation attacks. Applications that deliberately pass session IDs between systems or subdomains may need to review their session flow and explicitly create and close sessions before handing them off.

The broader goal is to make secure session behavior the default rather than something every application has to configure manually.

Deprecating mbregex

The mbregex functionality in the mbstring extension is based on Oniguruma. Oniguruma maintenance ended in 2025, so PHP is beginning the process of removing this dependency.

The Oniguruma maintenance end RFC deprecates the mbregex functions in PHP 8.6 and plans to remove them in PHP 9.0.

The affected functions include:

mb_ereg()
mb_ereg_match()
mb_ereg_replace()
mb_ereg_replace_callback()
mb_ereg_search()
mb_ereg_search_getpos()
mb_ereg_search_getregs()
mb_ereg_search_init()
mb_ereg_search_pos()
mb_ereg_search_regs()
mb_ereg_search_setpos()
mb_eregi()
mb_eregi_replace()
mb_regex_encoding()
mb_regex_set_options()
mb_split()

If your application uses these functions, PHP 8.6 is a good time to start migrating to PCRE or another maintained solution.

An external mb_onig package is also being developed for applications that need to retain the old behavior.

The SortDirection enum

PHP 8.6 adds a SortDirection enum through the SortDirection enum RFC.

SortDirection::Ascending;
SortDirection::Descending;

This is useful when an API accepts a sorting direction and you want to avoid passing arbitrary strings such as 'asc' or 'desc'.

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

The enum can also make APIs more discoverable and easier for static-analysis tools to understand.

DocComments for function parameters

PHP already supports DocComments for functions and properties, but not individual parameters.

PHP 8.6 adds parameter-level DocComments through the Parameter DocComments RFC.

/**
 * Search for entries matching a query.
 */
function search(
    string $query /** Terms to search for in the database */,
    int $limit = 10 /** Maximum number of entries returned */
) {
    // ...
}

Reflection can access the comment:

$reflection = new ReflectionFunction('search');

foreach ($reflection->getParameters() as $parameter) {
    var_dump($parameter->getDocComment());
}

This can be useful for tools that generate documentation, command-line interfaces, schemas, or validation rules from function signatures.

Form Feed support in trim functions

The default character mask used by trim(), ltrim(), and rtrim() includes several whitespace characters.

PHP 8.6 adds the Form Feed character, \f or 0x0C, through the Trim Form Feed RFC.

$value = "\fHello World\f";

trim($value);
// "Hello World"

This brings PHP’s default trimming behavior closer to other programming languages, where Form Feed is commonly treated as whitespace.

grapheme_strrev()

Reversing a UTF-8 string with strrev() can split multibyte characters and emoji sequences.

PHP 8.6 adds grapheme_strrev() through the Grapheme String Reverse RFC.

echo grapheme_strrev(
    'αあいうえお👨‍👨‍👧‍👦'
);

// 👨‍👨‍👧‍👦おえういあα

The function reverses grapheme clusters rather than individual bytes or code points. This means that compound emoji, combining marks, and characters from different scripts remain intact.

grapheme_strrev('Café');
// éfaC

This function is part of the intl extension.

The clamp() function

PHP 8.6 introduces a built-in clamp() function for restricting a value to a minimum and maximum range.

clamp(2, 1, 3); // 2
clamp(0, 1, 3); // 1
clamp(6, 1, 3); // 3

It also supports named arguments:

clamp(2, min: 1, max: 3);

The function works with numbers, strings, and objects that can be compared:

clamp(2.5, 1, 3); // 2.5

clamp('a', 'c', 'g'); // "c"
clamp('d', 'c', 'g'); // "d"

clamp(
    new DateTimeImmutable('2025-08-01'),
    new DateTimeImmutable('2025-08-15'),
    new DateTimeImmutable('2025-09-15')
);

If the minimum is greater than the maximum, PHP throws a ValueError:

clamp(4, 8, 6);
// ValueError

NAN is also rejected as a boundary value.

I’ve written more about the clamp() function in this article.

is_readable() and is_writable() reflection methods

The Reflection readable/writable RFC adds methods that allow reflection code to inspect whether a property can be read or written in a particular context.

$property = new ReflectionProperty(User::class, 'name');

$property->isReadable();
$property->isWritable();

These methods are useful for serializers, hydrators, form generators, dependency-injection containers, and other tools that need to reason about property access.

They provide a clearer alternative to manually inspecting visibility and property modifiers.

mysqli_quote_string()

The mysqli extension gains a standalone mysqli_quote_string() function through the mysqli quote string RFC.

$escaped = mysqli_quote_string($connection, $value);

The object-oriented form is also available:

$escaped = $connection->quote_string($value);

This provides a direct way to escape a string using a MySQL connection’s character set.

Prepared statements should still be preferred for SQL values. The new function is mainly useful when building SQL fragments where parameter binding is not available or when matching the equivalent functionality already exposed by other database APIs.

TLS session resumption for streams

TLS handshakes can be expensive because the client and server need to establish a new cryptographic session.

PHP 8.6 adds TLS session resumption support to streams through the TLS Session Resumption API RFC.

A client can enable session caching:

$context = stream_context_create([
    'ssl' => [
        'session_cache' => true,
    ],
]);

$socket = stream_socket_client(
    'tls://example.com:443',
    context: $context
);

A server can also configure whether sessions should be cached:

$context = stream_context_create([
    'ssl' => [
        'local_cert' => '/path/to/cert.pem',
        'session_cache' => false,
    ],
]);

$server = stream_socket_server(
    'tls://0.0.0.0:8443',
    context: $context
);

This gives applications more control over the performance and security trade-off involved in TLS session reuse.

Displaying function arguments in errors

PHP’s internal error messages do not always show the arguments that caused an error.

PHP 8.6 improves this through the Display Function Arguments in Errors RFC.

For example, instead of receiving a generic message for an invalid call, an error can include the relevant arguments:

TypeError: array_map(): Argument #1 ($callback) must be a valid callback

The goal is to make runtime errors more immediately useful while respecting cases where arguments cannot safely or reliably be rendered.

This should make debugging incorrect internal-function calls a little less frustrating.

Partial function application

PHP 8.6 adds partial function application through Partial Function Application v2.

Partial application lets you provide some arguments now and leave the remaining arguments to be supplied later.

Given this function:

function multiply(int $a, int $b, int $c): int
{
    return $a * $b * $c;
}

You can create a new callable with some arguments already filled in:

$multiplyByTwo = multiply(2, ..., ...);

$result = $multiplyByTwo(3, 4);
// 24

The ... placeholder represents an argument that will be provided later.

This is roughly equivalent to writing:

$multiplyByTwo = fn(int $b, int $c) => multiply(2, $b, $c);

But partial application avoids repeating the function signature manually.

It also works with methods and named arguments:

$formatUser = formatUser(
    user: ...,
    includeEmail: true
);

PHP 8.6 also supports partial application involving optional parameters through the optional placeholder RFC.

You can read more about this feature in my previous article.

The Duration class

PHP 8.6 introduces a Duration class for representing an exact span of time through the Duration class RFC.

Unlike a calendar interval, a duration represents a precise amount of elapsed time.

use DateTime\Duration;

$duration = Duration::fromSeconds(90);

echo $duration->seconds();
// 90

Durations can also include nanoseconds:

$duration = Duration::fromSeconds(
    seconds: 2,
    nanoseconds: 500_000_000
);

They can be added, subtracted, negated, compared, and converted into other representations:

$first = Duration::fromSeconds(30);
$second = Duration::fromSeconds(45);

$total = $first->add($second);

echo $total->toSeconds();
// 75

This is useful when measuring elapsed time, representing timeouts, working with protocol durations, or performing arithmetic where calendar concepts such as months and daylight-saving transitions should not be involved.

Closure optimizations

PHP closures are objects and can capture variables from their surrounding scope:

$tax = 0.2;

$calculate = function (float $price) use ($tax): float {
    return $price * (1 + $tax);
};

The Closure Optimizations RFC improves the engine’s handling of closures that do not actually need to capture variables.

This allows PHP to optimize certain closures as if they were static closures, even when the code does not explicitly use the static keyword.

For example:

$values = array_map(
    function (int $value): int {
        return $value * 2;
    },
    $values
);

The behavior does not change. The improvement is intended to reduce overhead in code that creates or invokes many closures.

Explicitly marking closures as static remains useful when you want to communicate that the closure must not access $this.

Readonly property defaults

Readonly properties usually need to be initialized in a constructor:

class User
{
    public readonly string $role;

    public function __construct()
    {
        $this->role = 'user';
    }
}

PHP 8.6 allows readonly properties to have declared default values through the Readonly Property Defaults RFC.

class User
{
    public readonly string $role = 'user';
}

The property is initialized with the default value and remains readonly afterward.

This is especially useful for value objects and data-transfer objects where a readonly property has a natural default:

class Pagination
{
    public readonly int $page = 1;
    public readonly int $perPage = 20;
}

Reflection reports these properties as both readonly and having a default value.

SNMP improvements

PHP 8.6 also contains several improvements to the SNMP extension through the SNMP Improvements RFC.

The extension gains better support for controlling how SNMP data is represented, including options for:

  • numeric OID output
  • ASCII string output
  • extended indexes
  • suppressing printed units

For example:

$snmp = new SNMP(
    SNMP::VERSION_2C,
    'udp://127.0.0.1',
    'public'
);

$snmp->extended_index = true;
$snmp->dont_print_units = true;

$snmp->setOidOutputFormat(Snmp\OidOutput::Numeric);
$snmp->setStringOutputFormat(Snmp\StringOutput::Ascii);

These options make it easier to consume SNMP data consistently in monitoring tools and automation scripts.

In Closing

PHP 8.6 is not centered around one giant headline feature. Instead, it continues the direction PHP has taken in recent releases: small language improvements, better APIs, stronger defaults, and more useful primitives for application and library authors.

The features I’m most interested in are:

  • clamp(), because it replaces a common bit of userland code
  • grapheme-aware string reversal
  • partial function application
  • the new polling API
  • explicit endianness modifiers for binary data
  • more secure session defaults
  • the Duration class

As always, the final list may change before the stable release. For the latest status, keep an eye on the PHP RFC index and the PHP 8.6 development branch.

Now Available PHP 8.5
PHP 8 in a Nutshell book cover
Chapter: Introduction
Chapter: Match Expressions
Chapter: Pipe operator
Learn the fundamentals of PHP 8 (including 8.1, 8.2, 8.3, 8.4, and 8.5), the latest version of PHP, and how to use it today with my book PHP 8 in a Nutshell. It's a no-fluff and easy-to-digest guide to the latest features and nitty-gritty details of PHP 8. So, if you're looking for a quick and easy way to PHP 8, this is the book for you.

👋 Hi there! This is Amit, again. I write articles about all things web development. If you enjoy my work (the articles, the open-source projects, my general demeanour... anything really), consider leaving a tip & supporting the site. Your support is incredibly appreciated!

Comments?