August 29, 2026

PHP 8.6: What Actually Breaks When You Upgrade

Photo of Marco Orta Marco Orta | 11 mins read
Compartir
3D illustration of the PHP elephant beside a cracked shield, representing the security defaults that change in PHP 8.6
Table of Contents

    PHP 8.6 ships on November 19, 2026, and the change most likely to break your application in production is not a removed function — it is three session settings whose defaults flip. session.use_strict_mode, session.cookie_httponly and session.cookie_samesite all change, and the failure mode is silent: nothing throws, users just stop staying logged in on cross-site requests.

    Everything else is mostly deprecations, which means warnings rather than fatals. But there are four genuine behavior changes hiding among them, and two of those are the kind that change results without saying anything.

    This is the breakage list. If you want the new features — Partial Function Application, clamp(), the SortDirection enum — those are in everything new in PHP 8.6.

    How settled is this list?

    Worth stating up front, because “PHP 8.6 breaking changes” articles were being published before there was anything to report.

    The release timetable is public: alpha 1 landed on July 2, beta 1 and the soft feature freeze on August 13, the hard feature freeze is September 22, RC1 arrives September 24 and RC4 on November 5, and GA is November 19, 2026.

    What that means for this article:

    • The deprecations are settled. They come from RFCs that have already been voted, and voted RFCs do not get un-voted.
    • The list can still grow. Between now and the hard freeze, RM-approved changes can still land, and bug fixes after that occasionally add upgrade notes.
    • There is no official migration page yet. php.net/manual/en/migration86.php does not exist at the time of writing — the source of truth today is the UPGRADING file on php-src’s master branch, which is where everything below comes from.

    I will update this article after the hard freeze and RC1 on September 24, when the list is effectively final.

    The one that will actually break production

    Session security defaults flip

    Three php.ini defaults change:

    SettingBeforePHP 8.6
    session.use_strict_mode01
    session.cookie_httponly01
    session.cookie_samesite"""Lax"

    Each one is the right default. Together they are the most disruptive change in the release, because none of them throws an error. Your application keeps running and some users stop being logged in.

    cookie_samesite = "Lax" is the one that bites. A Lax cookie is not sent on cross-site POST requests. If anything posts to your application from another origin — a payment gateway returning the user via POST, an identity provider’s callback, an embedded form, a webhook that relies on a session — that request now arrives without a session. The user lands on a login screen for no visible reason.

    cookie_httponly = 1 means JavaScript can no longer read the session cookie via document.cookie. That is correct, and it breaks any front-end code that was reading the session ID directly — some older analytics snippets and hand-rolled AJAX auth do this.

    use_strict_mode = 1 makes PHP reject session IDs it did not generate itself, which is the actual fix for session fixation. It breaks flows that pass a session ID in from outside.

    If you need the old behavior temporarily, set it explicitly rather than relying on the previous default:

    ; only as a stopgap while you fix the real cause
    session.cookie_samesite = "None"
    session.cookie_secure = 1   ; required whenever SameSite=None
    

    Note that SameSite=None without Secure is rejected by browsers, so this is not a one-line revert.

    The honest advice: do not revert. Set these three explicitly in your php.ini today, on PHP 8.5, and find out what breaks while you still control the timing. That converts a release-day surprise into a Tuesday afternoon.

    The behavior changes that stay quiet

    trim() now strips form feed

    trim(), ltrim() and rtrim() add \f (form feed, 0x0C) to their default character list. It was the one ASCII whitespace character they did not strip.

    trim("hello\f");
    // PHP 8.5 → "hello\f"
    // PHP 8.6 → "hello"
    

    This is almost always what you wanted. It is listed here because it changes output silently, and if you have tests asserting on exact strings from parsing fixed-width files, legacy print streams or anything that carries form feeds, they will start failing without an obvious cause.

    NUL bytes now throw instead of being tolerated

    A large set of functions now throw ValueError when passed a string containing a NUL byte, rather than truncating or behaving unpredictably. The list includes getenv(), putenv(), parse_str(), setlocale(), dl(), openlog(), proc_open() (the $cwd argument), and roughly twenty filesystem functions — file_exists(), is_file(), filesize(), stat() and their relatives.

    This is a security hardening measure, and NUL bytes in these arguments have historically been a path-traversal vector. The practical impact is that code passing unsanitized user input to filesystem checks now gets a loud exception instead of quiet nonsense:

    // PHP 8.5: returns false, no signal
    // PHP 8.6: throws ValueError
    file_exists($_GET['path']);   // when $_GET['path'] contains "\0"
    

    If your application does this, the exception is telling you about a bug that was already there.

    unpack() reinterprets < and >

    unpack() now treats < and > after a format code as endianness modifiers rather than as part of the name. This changes how existing format strings parse:

    unpack("s<value", $data);
    // PHP 8.5 → key "<value"
    // PHP 8.6 → little-endian short, key "value"
    
    unpack("C>name", $data);
    // PHP 8.6 → throws (C has no endianness)
    

    Narrow, but if you parse binary formats it is a hard break rather than a deprecation. Grep for unpack( and check whether any format string has a < or > in a name.

    SessionHandlerInterface wants two more methods

    Custom session handlers that do not implement create_sid() and validateId() now emit deprecation notices. If you wrote a database- or Redis-backed session handler by hand, it probably implements the six original methods and not these two — and validateId() is what makes use_strict_mode actually work, so this connects back to the change above.

    The deprecations

    None of these are fatal in 8.6. They emit E_DEPRECATED, and they are the removal list for PHP 9.

    return inside finally. Returning from a finally block discards any return value or exception from the try, which is almost always a bug being hidden. Now deprecated.

    function f() {
        try { return 1; }
        finally { return 2; }   // deprecated — silently wins, returns 2
    }
    

    Returning a value from a constructor. Returning a value from __construct() or __destruct(), or turning either into a generator, now deprecates at compile time — you will see it even if the code never runs. It passed 39 to 0, and becomes an error in the first major after 8.6.

    class Foo {
        public function __construct() {
            return 123;   // deprecated at compile time
        }
    }
    
    class Bar {
        public function __construct() {
            if (random_int(0, 1)) {
                return;   // still perfectly legal — a bare return is fine
            }
        }
    }
    

    array_filter() throws on an invalid $mode. It used to ignore a bad mode silently; now it raises ValueError. Strictly an improvement, but it can take down code that had been passing a wrong constant unnoticed for years.

    php://filter caps chained filters. A limit on how many filters you can chain, as hardening against a known filter-chain exploitation technique. Only relevant if you build filter chains deliberately.

    All of mbregex. Every mb_ereg* function is deprecated, because the Oniguruma library behind them is unmaintained. This is the largest deprecation in the release by surface area. The migration is to PCRE — preg_match() and friends, with the u modifier for Unicode:

    mb_ereg('^[0-9]+$', $s);      // deprecated
    preg_match('/^[0-9]+$/u', $s); // replacement
    

    The patterns are not always a direct translation, so this one deserves real test coverage rather than a find-and-replace.

    Type-check and conversion aliases. is_double(), is_long(), is_integer() and doubleval() are deprecated in favor of is_float(), is_int() and floatval(). Also deprecated: metaphone(), strcoll(), spl_object_hash(), spl_classes(), and the SORT_LOCALE_STRING sort flag.

    spl_object_hash() is the one worth flagging — replace it with spl_object_id(), which is what you almost certainly meant.

    SPL CSV and ArrayIterator methods. SplFileObject::fgetcsv(), fputcsv(), setCsvControl() and getCsvControl() are deprecated, as are ten ArrayIterator methods: getFlags(), setFlags(), asort(), ksort(), uasort(), uksort(), natsort(), natcasesort(), serialize() and unserialize().

    Object arguments where they never made sense. array_walk(), mb_convert_variables(), and the zlib and bz2 stream filters now deprecate object arguments.

    mysqli odds and ends. mysqli_get_charset() and mysqli_stmt_init() are deprecated. SOAP’s classmap option now rejects integer keys.

    What to actually do, in order

    1. Set the three session settings explicitly on PHP 8.5, now. This is the whole risk of the release concentrated in one change, and it is the only one you can test before 8.6 exists. Check every cross-site POST that hits your application.
    2. Turn on deprecation reporting and run your test suite. error_reporting(E_ALL) with a log you actually read. Most of this release surfaces there.
    3. Grep for the specific names. mb_ereg, is_double, is_long, is_integer, doubleval, spl_object_hash, metaphone, strcoll, unpack(. That is a twenty-minute pass and it covers most of the deprecation list.
    4. Check custom session handlers for create_sid() and validateId().
    5. Leave trim() alone unless a test fails. If one does, you have learned something about your input data.

    What does not break

    Worth saying, because upgrade anxiety fills in blanks that are not there. PHP 8.6 removes nothing that was deprecated in 8.x — the removals land in PHP 9. Your typed properties, enums, readonly classes, fibers and attributes are untouched. If your application runs clean on 8.5 with deprecations silenced, the realistic worst case for 8.6 is a noisy log plus the session change.

    The session change is not a small caveat, though. It is the reason this article exists.

    Frequently asked questions

    When is PHP 8.6 released? November 19, 2026. The hard feature freeze is September 22 and the release candidates run from September 24 to November 5.

    What is the most dangerous change in PHP 8.6? The session defaults: session.use_strict_mode and session.cookie_httponly become 1, and session.cookie_samesite becomes "Lax". They fail silently — users stop staying logged in on cross-site POST requests instead of getting an error.

    Does PHP 8.6 remove anything? No. It deprecates a great deal — all of mbregex, several type-check aliases, SPL CSV methods — but removals are scheduled for PHP 9. Deprecations emit warnings, not fatal errors.

    Is mb_ereg really going away? It is deprecated in 8.6 because Oniguruma, the library behind it, is unmaintained. Migrate to PCRE (preg_match and friends with the u modifier). Patterns do not always translate one to one, so test rather than find-and-replace.

    Compartir

    Search

    Tags

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