PHP 8.6: What Actually Breaks When You Upgrade
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, and the hard feature freeze passed on September 22. There was no RC1: a packaging mistake (the API version numbers were not bumped when the PHP-8.6 branch was cut) meant the first release candidate shipped as RC2, on September 24. RC3 followed on October 8, RC4 is planned for October 22 and RC5 for November 5. GA is still November 19, 2026.
What that means for this article:
- The list is effectively final. We are past the hard freeze, so from here on only bug fixes land, and a fix only occasionally adds an upgrade note.
- The deprecations are settled. They come from RFCs that have already been voted, and voted RFCs do not get un-voted.
- There is an official migration page now.
php.net/manual/en/migration86.phpwent up after this article was first published; the other source of truth is theUPGRADINGfile on php-src’sPHP-8.6branch, which is where everything below comes from.
Status as of October 15, 2026: checked against RC3 (October 8) and UPGRADING on the PHP-8.6 branch. The session defaults change is still in, unchanged. The list did grow a little after the freeze — mostly deprecations that were voted earlier but only documented on September 28 — and those are now included below.
The one that will actually break production
Session security defaults flip
Three php.ini defaults change:
| Setting | Before | PHP 8.6 |
|---|---|---|
session.use_strict_mode | 0 | 1 |
session.cookie_httponly | 0 | 1 |
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(), http_build_query(), deflate_init() / inflate_init(), and the zlib and bz2 stream filters now deprecate object arguments. http_build_query() is the one with real-world reach: passing it a DTO or a plain object used to work, and now each call warns — convert with get_object_vars() first.
is_a() and is_subclass_of() with a class-name string. Passing a string as the first argument while $allow_string is false is deprecated. It never did what it looked like it did; pass the object, or set $allow_string to true.
Reserved-looking names. Using let or is as the name of a class, interface, trait, enum, function or constant is deprecated, and so is _ as a constant name, extending the PHP 8.4 deprecation of _ as a class name. Passing a third argument to define() is deprecated too.
mysqli odds and ends. mysqli_get_charset() and mysqli_stmt_init() are deprecated. SOAP’s classmap option now rejects integer keys.
Late additions that do break
Two narrow hard breaks entered UPGRADING after this article first went out, and both throw rather than warn:
ZipArchivecan no longer be serialized.serialize()andunserialize()on aZipArchivenow throw anException. If something caches or queues objects that hold one, it fails at that point.ftp_nb_fget(),ftp_nb_fput(),ftp_nb_get()andftp_nb_put()throw anErrorwhen the connection is already transferring, instead of emitting a warning and returningfalse.
And one for local development only: the CLI built-in server no longer reflects the request’s Host header.
What to actually do, in order
- Set the three session settings explicitly on PHP 8.5, now. This is the whole risk of the release concentrated in one change, and you can test it on 8.5 today without waiting for an RC. Check every cross-site POST that hits your application.
- 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. - Grep for the specific names.
mb_ereg,is_double,is_long,is_integer,doubleval,spl_object_hash,metaphone,strcoll,http_build_query,unpack(. That is a twenty-minute pass and it covers most of the deprecation list. - Check custom session handlers for
create_sid()andvalidateId(). - 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 passed on September 22. RC1 was skipped because of a packaging mistake, so the release candidates are RC2 (September 24), RC3 (October 8), RC4 (October 22) and RC5 (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.
Related reading
- Everything new in PHP 8.6 — the features side: Partial Function Application,
clamp(), theSortDirectionenum. - What breaks upgrading to Laravel 13 — Laravel 13 requires PHP 8.3 or later, so these two upgrades often land in the same sprint.
Changes to this article
- — Checked against RC3 (October 8) and the UPGRADING file on the PHP-8.6 branch: the session defaults change stands. RC1 was skipped (the first release candidate was RC2, on September 24) and the RCs now run RC2 to RC5; GA is still November 19. Added the deprecations and breaks documented after the hard freeze: let, is and _ as identifiers, is_a()/is_subclass_of() with a string, http_build_query() with an object, ZipArchive serialization and the ftp_nb_* functions. The official migration page now exists.
Found it useful? Get the next one by email
Once a week: what breaks when you upgrade, AI for developers and what I'm building, with sources. No spam.
By subscribing you accept our privacy policy.