August 4, 2026

PHP 8.6: Every New Feature and the Release Date

Photo of Marco Orta Marco Orta | 14 mins read
Compartir
3D illustration of the PHP elephant next to a question mark floating as a placeholder inside a function call
Table of Contents

    PHP 8.6 ships on November 19, 2026, and the soft feature freeze is already behind us: it landed on August 13 with beta 1, so anything not already in barely makes it now. The list below is, in practice, the final one — what remains between here and November is betas, the September 22 hard freeze and the release candidates.

    The headline is Partial Function Application, a syntax that spent years in limbo and finally passed unanimously. But what will actually cost you time isn’t the new functions — it’s three quiet changes: sessions become secure by default, mb_ereg and friends get deprecated, and return inside a constructor starts warning.

    Let’s go through it.

    Timeline: when PHP 8.6 arrives

    flowchart TB
        A["🧪 Alpha 1 · Jul 2, 2026"] --> B["🧪 Alpha 2 · Jul 16"]
        B --> C["🧪 Alpha 3 · Jul 30"]
        C --> D["🧊 Beta 1 + soft feature freeze<br/>Aug 13, 2026"]
        D --> E["🧊 Beta 2 · Aug 27"]
        E --> F["🧊 Beta 3 · Sep 10"]
        F --> G["🔒 Hard feature freeze<br/>Sep 22, 2026"]
        G --> H["📦 RC 1 to RC 4<br/>Sep 24 – Nov 5"]
        H --> I["🚀 GA · Nov 19, 2026"]

    In plain terms: from August 13 a new feature needs justification to get in, and from September 22 it needs explicit approval from the release managers. From September 24 onward it’s bug fixes only.

    This release is managed by Daniel Scherzer (veteran), Matteo Beccati, and Joe Ferguson (rookies), elected by single transferable vote in April.

    If you want to try it today without touching your machine, the cleanest route is a container running php:8.6-rc-cli once the RCs are out — the complete Docker guide covers the setup.

    Partial Function Application: the big one

    This is the feature that changes how you write code. The Partial Function Application v2 RFC passed 33 votes to 0, and it has been merged since alpha 3.

    The idea: you can call a function leaving gaps, and instead of executing it, PHP hands you back a closure with those gaps as parameters.

    function add(int $a, int $b, int $c): int {
        return $a + $b + $c;
    }
    
    $add10AndX = add(10, ?, 5);  // Closure: fn(int $b): int
    echo $add10AndX(3);          // 18
    

    The ? is a placeholder. It isn’t a value, it’s a gap.

    Before and after

    The classic case is passing a native function to array_map when the argument that varies isn’t the first one. Today you have to wrap it:

    // PHP 8.5 and earlier
    $strings = ['hello world', 'hello php'];
    $result = array_map(fn ($s) => str_replace('hello', 'hi', $s), $strings);
    

    In 8.6 the wrapper is gone:

    // PHP 8.6
    $replaceHello = str_replace('hello', 'hi', ?);
    $result = array_map($replaceHello, $strings);
    

    Or inline, with no intermediate variable:

    $result = array_map(str_replace('hello', 'hi', ?), $strings);
    

    The two placeholders: ? and ...

    • ? consumes exactly one argument at that position.
    • ... consumes zero or more remaining arguments.
    $replaceWhitespace = str_replace(' ', ?, ?);   // 2 parameters
    $replaceWhitespace('=', $input);
    
    $replaceWhitespace = str_replace(' ', ...);    // whatever's left, as-is
    $replaceWhitespace('=', $input);
    

    If you were already using PHP 8.1’s first-class callable syntax (strlen(...)), good news: it’s exactly the degenerate case of PFA. foo(...) still means the same thing — it’s just part of a bigger family now.

    With named arguments

    Named placeholders adopt the order you write them in, not the order of the original signature:

    function stuff(int $i1, string $s2, float $f3, Point $p4) {}
    
    $c = stuff(s2: ?, i1: ?, p4: ?);
    // Resulting signature: fn(string $s2, int $i1, Point $p4)
    

    With one hard rule: you cannot put a positional placeholder after a named one. That’s a fatal error.

    Combined with the pipe operator

    This is where it really shines. The |> operator arrived in PHP 8.5 (November 2025) and until now forced you to wrap any function taking more than one argument. With PFA:

    $slug = $input
        |> trim(...)
        |> str_replace(' ', '-', ?)
        |> str_replace(['.', '/', '…'], '', ?)
        |> strtolower(...);
    

    And it isn’t just cosmetic: the RFC includes a compile-time optimization that removes the intermediate closure for the simple cases (foo(?), foo(1, ?), foo(1, a: ?), foo(1, ...)) when used with the pipe. You don’t pay for the abstraction.

    The detail that will bite you

    The arguments you do fill in are evaluated when you create the partial, not when you call it. This is the opposite of an arrow function:

    $partial = speak(?, getArg());        // getArg() runs HERE
    $arrow   = fn ($who) => speak($who, getArg());  // getArg() runs on call
    

    If getArg() reads the clock, hits the database, or depends on state, the result is frozen at the moment the partial is created. It’s consistent behavior (you’re applying arguments, not deferring them), but it’s the easiest mistake to make in the first few weeks.

    What you cannot do

    • Constructors: new Foo(?) doesn’t work. Error: Cannot create Closure for new expression.
    • compact(), extract(), func_get_arg(), get_defined_vars(): disallowed, because they depend on the calling scope.
    • __get and __set: don’t apply, since they aren’t invoked as methods. __call and __callStatic do work.
    • Static methods produce static closures (not rebindable); instance methods produce rebindable ones, but only to the same type.

    The optional-parameter patch

    A second RFC (25 for, 0 against) fixed an important detail before this reached production. The final rule:

    • A parameter generated by ? is always required, even if the original function gave it a default value.
    • Parameters pulled in by ... keep their optionality.
    function example(mixed $a, string $b = 'default', string $c = 'also optional') {}
    
    example(?, ?);        // fn(mixed $a, string $b)         ← $b is now required
    example('foo', ...);  // fn(string $b = 'default', string $c = 'also optional')
    

    Without this fix, a child class changing a default value could alter the signature of a partial created on the parent. Now ? always means the same thing.

    The small, useful additions

    clamp()

    Constrains a value between a minimum and a maximum. We’ve all written max($min, min($max, $v)) at some point, and we’ve all gotten it wrong at some point.

    clamp(2, min: 1, max: 3)   // 2
    clamp(0, min: 1, max: 3)   // 1
    clamp(6, min: 1, max: 3)   // 3
    clamp("a", "c", "g")       // "c"
    

    The signature is clamp(mixed $value, mixed $min, mixed $max): mixed and it follows PHP’s normal comparison rules, so it works with dates too:

    clamp(
        new \DateTimeImmutable('2025-08-01'),
        new \DateTimeImmutable('2025-08-15'),
        new \DateTimeImmutable('2025-09-15')
    )->format('Y-m-d');  // 2025-08-15
    

    Two edge cases worth knowing: if $min > $max it throws ValueError, and so does passing NAN as a bound. NAN as the value is returned as-is. Passed 23 to 3.

    enum SortDirection

    An enum in the global namespace with two cases, Ascending and Descending:

    enum SortDirection {
        case Ascending;
        case Descending;
    }
    
    $query->orderBy('created_at', SortDirection::Descending);
    

    Let’s be honest about what this is and isn’t: no core function consumes it yet. What it provides is a shared vocabulary so frameworks, ORMs, and extensions stop inventing their own 'ASC' / 'DESC' / SORT_ASC constants. The RFC mentions scandir() as a future candidate. Passed 22 to 2.

    #[\Override] on class constants

    PHP 8.3’s #[\Override] attribute now works on constants too. If you declare that you’re overriding something and you aren’t, PHP tells you:

    class Demo {
        #[\Override]
        public const C = 'C';  // Error: nothing to override
    }
    
    class Base {
        protected const C = 'C';
    }
    
    class Child extends Base {
        #[\Override]
        public const C = 'Changed';  // Fine
    }
    

    Public and protected constants from parent classes and interfaces satisfy it; private ones don’t. Works on interfaces, enums, and anonymous classes. Passed 15 to 0.

    Debuggable enums

    Until now __debugInfo() was forbidden on enums. In 8.6 the restriction is lifted:

    enum Foo: string {
        case Bar = "Baz";
    
        public function __debugInfo() {
            return [__CLASS__ . '::' . $this->name . ' = ' . $this->value];
        }
    }
    
    var_dump(Foo::Bar);
    

    Small change, no breakage, and handy if you have enums carrying logic you want to inspect. Passed 16 to 2.

    Errors that show their arguments

    This is one of the bigger time-savers when debugging. Today, a warning from a native function tells you what failed but not what with:

    Warning: chmod(): Operation not permitted in /app/chmod.php on line 5
    

    With the new error_include_args directive enabled:

    Warning: chmod('/', 511): Operation not permitted in /app/chmod.php on line 5
    

    One critical detail: it’s off by default (error_include_args=0). The feature passed 24 to 2, but the secondary vote on the default value went to 0 by 18 to 7. If you want it, you turn it on.

    It respects the protections that already existed: it honors the #[\SensitiveParameter] attribute to mask passwords, respects zend.exception_string_param_max_len so logs don’t bloat, and only applies to PHP’s internal functions, not to errors you trigger yourself.

    And also

    • grapheme_strrev() — reverse a string respecting grapheme clusters. In other words, strrev() that doesn’t destroy emoji or composed characters.
    • Polling API (Io\Poll) — an I/O multiplexing API with Context, Watcher, and StreamPollHandle classes, backed by epoll on Linux and kqueue on BSD/macOS. It fixes the two historical limits of stream_select(): the file-descriptor ceiling and its O(n) complexity. If you write async servers in PHP, this is your feature. Passed 33 to 1.
    • mysqli_quote_string() — string escaping without needing an open connection.
    • Locale::getDisplayKeyword() and getDisplayKeywordValue() — human-readable names for locale keywords.
    • Reflection isReadable / isWriteable methods — check whether a property can be read or written without triggering the access.
    • TLS session resumption for streams — fewer full handshakes on repeated connections.
    • DocComments on function parameters — doc blocks can now attach to individual parameters and be read via Reflection.
    • json_decode() reports the error position — the message tells you where the malformed JSON is, not just that it is.

    What breaks: the short version

    PHP 8.6 removes nothing — the removals land in PHP 9 — so the upgrade risk is concentrated in one place: three session directives whose defaults flip.

    DirectiveBeforeIn 8.6
    session.use_strict_mode01
    session.cookie_httponly01
    session.cookie_samesite(empty)Lax

    All three votes were clean — 27-0, 26-0 and 26-0 — and all three are the values every audit has asked for for years. The problem is that nothing throws: cross-site POSTs stop carrying the session cookie, so payment gateway callbacks and older SSO integrations drop the user at a login screen with no error to trace.

    Everything else in the release is deprecations, which emit warnings rather than fatals: all 14 mbregex functions (mb_ereg and its family, because Oniguruma is unmaintained), returning a value from a constructor, the type-check aliases is_double() / is_long() / is_integer() / doubleval(), and a set of SPL and ArrayIterator methods.

    If you’re touching sessions anyway, audit the site’s security headers while you’re at it:

    The full breakage list — including the NUL-byte hardening that turns file_exists() into a ValueError, the unpack() endianness reinterpretation, and the trim() change that alters output silently — is in what actually breaks when you upgrade to PHP 8.6.

    Migration checklist

    A sensible order so nothing surprises you:

    1. Spin up 8.6 in a container as soon as RC1 lands (September 24). Leave your main environment alone.
    2. Run your test suite with error_reporting(E_ALL) and capture the deprecations. The constructor ones surface at compile time on their own.
    3. grep for mb_ereg and mb_split. Decide between PCRE and the mb_onig extension and plan it: you have until PHP 9.0, but the sooner it leaves your backlog, the better.
    4. Review your sessions. If your php.ini already set all three directives explicitly, nothing changes for you. If it relied on defaults, test login, logout, SSO, and any external POST callback.
    5. Update your static analysis tools (PHPStan, Psalm, Rector) before writing new syntax. PFA is new syntax; until your analyzer understands it, you’ll get false positives.
    6. Save PFA for new code. There’s no reason to rewrite closures that already work.

    Should you upgrade?

    If you’re on PHP 8.2 or older, yes, and soon: 8.2 leaves security support on December 31, 2026, and 8.1 already has. If you’re on 8.3, security fixes run until December 31, 2027 — room to plan, not room to forget. Either way, jump straight to 8.6 as soon as your stack supports it.

    If you’re on 8.4 or 8.5, this is one of the easy jumps: no mandatory syntax changes, no removals, just deprecations and three safer defaults. The question isn’t whether to upgrade but whether your dependencies support it yet — and that’s usually the real bottleneck. Laravel, Symfony, and the rest typically take anywhere from a few weeks to a couple of months to declare formal compatibility.

    My practical recommendation: test on September’s RC1, migrate mb_ereg in the meantime, and ship to production when 8.6.1 or 8.6.2 lands. There’s never a prize for being first on a .0.

    Conclusion

    PHP 8.6 isn’t a revolutionary release, and that’s fine. It’s a release that closes gaps: PFA completes what first-class callables started in 8.1 and the pipe operator continued in 8.5; clamp() and SortDirection remove boilerplate that was scattered everywhere; and the session defaults have been everyone’s recommendation for a decade — everyone’s except PHP’s own.

    The only item that demands real work is mbregex, and you have until PHP 9.0 to do it.

    Keep reading:

    Frequently asked questions

    When is PHP 8.6 released?

    PHP 8.6 reaches general availability on November 19, 2026. The schedule leading up to it includes beta 1 with the soft feature freeze on August 13, 2026, the hard feature freeze on September 22, and four release candidates between September 24 and November 5.

    What is Partial Function Application in PHP 8.6?

    It is a syntax that lets you call a function leaving gaps marked with ? or ..., so that instead of executing it returns a closure with those gaps as parameters. For example, str_replace('hello', 'hi', ?) returns a function that only expects the text. It is the general case of which PHP 8.1's first-class callable syntax was a specific instance, and it passed unanimously with 33 votes in favor.

    Does PHP 8.6 break backward compatibility?

    It removes nothing, but it changes three session defaults that may affect you: session.use_strict_mode becomes 1, session.cookie_httponly becomes 1, and session.cookie_samesite becomes Lax. It also deprecates 14 mbregex functions (mb_ereg and family), returning a value from a constructor, and trim now strips the form feed by default. Deprecations do not break anything yet, they only warn.

    Why is mb_ereg deprecated in PHP 8.6?

    Because Oniguruma, the multibyte regular expression library they rely on, is no longer maintained. The 14 mbregex functions are deprecated in PHP 8.6 and removed in PHP 9.0. The recommended migration is PCRE with the u modifier (preg_match with /u), or installing the mb_onig PECL extension if you need to keep Oniguruma-specific syntax.

    What is clamp() for in PHP 8.6?

    clamp(mixed $value, mixed $min, mixed $max) constrains a value within a range: it returns the minimum if the value falls below it, the maximum if it exceeds it, and the value itself if it fits. It replaces the classic max($min, min($max, $v)). It follows PHP's normal comparison rules, so it works with numbers, strings, and objects such as DateTimeImmutable. It throws ValueError if the minimum is greater than the maximum.

    Can I use PHP 8.6 with Laravel?

    You will be able to, but not on launch day. Major frameworks usually take anywhere from a few weeks to a couple of months to declare formal compatibility with a new minor PHP version. The sensible approach is to test on September's RC in an isolated container and ship to production once 8.6.1 or 8.6.2 is out and your dependencies declare support.

    Compartir

    Search

    Tags

    Migration PHP AI Tutorial JavaScript Laravel Web Development Upgrade Best Practices Security SEO Backend Claude Laravel 13 TypeScript