Two PHP deadlines, six weeks apart
PHP 8.6 has two dates with your name on them. The first is 19 November 2026, when PHP 8.6 goes stable — release candidate 4 lands this Thursday, 22 October, and with the hard feature freeze already behind us there is nothing left to add, only bugs to fix. The second is 31 December 2026, when PHP 8.2 loses security support for good. Six weeks separate the two. If you run Laravel or WordPress, this is the week the work starts: test your code against the release candidate now, and start planning the 8.2 exit before the December holidays turn infrastructure changes into a gamble.
PHP ships a new minor every November, and every new branch pushes an older one out of support. That is the whole story of this post: a release to test and a retirement to plan, arriving back to back. Here is the timeline, what is new, what will break, and the checklist to work through this week.
The PHP 8.6 release timeline: what is left
The schedule below comes from the official PHP 8.6 release page on the PHP wiki, as tracked by the RC announcements:
| Milestone | Date | Status |
|---|---|---|
| Alpha 1–3 | 2–30 July | Done |
| Beta 1 (soft feature freeze) | 13 August | Done |
| Beta 2 / Beta 3 | 27 August / 10 September | Done |
| Hard feature freeze | 22 September | Done |
| RC1 | 24 September | Skipped — packaging mistake |
| RC2 | 24 September | Done |
| RC3 | 8 October | Done |
| RC4 | 22 October | This week |
| RC5 | 5 November | Planned |
| General availability | 19 November | Planned |
Two things matter about this table. First, RC1 never shipped: the RC2 announcement opens with “It is not a typo: a mistake was made while packaging RC1, so RC2 was released instead.” Second, the hard feature freeze landed on 22 September, so the feature set you test this week is the feature set you get in November — everything from here is bug fixes. The release managers are Daniel Scherzer, Matteo Beccati and Joe Ferguson, and the official line on every RC announcement is identical: “Please DO NOT use this version in production, it is an early test version.” Test it on your laptop, in Docker or on staging, and report anything you find on GitHub Issues.
What’s new in PHP 8.6: the parts you’ll actually use
Partial function application
The headline feature, passed 33 votes to 0. You call a function with some arguments and placeholders for the rest, and instead of executing, the call returns a Closure waiting for the missing arguments. ? stands for exactly one argument at this position; ... stands for zero or more remaining arguments (you already know strlen(...) — conceptually that is a partial application with no bound arguments).
// PHP 8.5
$slugs = array_map(
fn (string $title): string => str_replace(' ', '-', $title),
$titles,
);
// PHP 8.6
$slugs = array_map(str_replace(' ', '-', ?), $titles);
It pairs with the pipe operator that arrived in PHP 8.5, whose right-hand side must take exactly one argument — previously that meant a parenthesised arrow function every time:
// PHP 8.6
$slug = $title
|> trim(...)
|> str_replace(' ', '-', ?)
|> strtolower(...);
Two semantics will bite you if you convert arrow functions blindly. First, bound arguments are evaluated once, when the partial is created — an arrow function evaluates its body on every call, so logLine(date('H:i:s'), ?) freezes the timestamp at creation time. Second, every ? becomes a required parameter on the closure, even if the original parameter had a default — a follow-up RFC tightened this before release, so older blog posts showing ? inheriting defaults are wrong. The engine rewards you too: array_map() with a partial application is compiled into a loop with no intermediate closures, and a single-placeholder partial directly after |> becomes a direct call with no closure at all.
clamp()
A global function that replaces the max($min, min($max, $value)) trick everyone gets backwards:
$page = clamp($requestedPage, 1, $lastPage);
clamp(101, min: 0, max: 100); // 100
clamp(-1, min: 0, max: 100); // 0
It works with any comparable type, including strings and DateTimeInterface objects. It throws a ValueError when $min exceeds $max or a bound is NAN — which is a behaviour change worth knowing: the nested version silently returns 1 when $lastPage is 0 on an empty result set, while clamp() throws. Check that edge case before replacing every max(min()) in your codebase.
Readonly properties with defaults
Before 8.6, giving a readonly property a default value was a compile-time error. That restriction is gone:
final class RetryPolicy
{
public readonly int $maxAttempts = 3;
}
The rule to remember: the default counts as the initialising assignment. You cannot “override” it in the constructor — that is an error. If a value should be configurable, use a promoted constructor parameter with a default instead. __clone() and clone-with may reinitialise the property once.
The rest, in brief
Time\Duration— afinal readonlyclass for exact, nanosecond-precise durations, built from named factories likeDuration::fromSeconds(2, 500_000_000). Comparison operators work;+and-do not (useadd()andsub()). No integration withDateTimeorsleep()yet — today its value is typed configuration instead ofint $timeoutof unknown unit.Io\Poll— an object-oriented event-loop primitive with automatic backend selection (epoll, kqueue, poll, event ports, WSAPoll) and await()that takes aTime\Durationtimeout. This is for library authors; application developers benefit indirectly once async runtimes drop theirstream_select()workarounds.SortDirectionglobal enum — a native unit enum replacing arbitrary'ASC'/'DESC'strings in sorting operations.#[\Override]on class constants — PHP now validates that a constant in a child class actually overrides a parent or interface constant, catching broken contracts at compile time.- Accurate
json_decode()error locations — pinpoints the exact byte offset where parsing failed, instead of a bare “syntax error”. error_include_args— a new INI option (off by default) that adds function arguments to error messages. Parameters marked#[\SensitiveParameter]are redacted; everything else is not, so check what ends up in your logs before enabling it.grapheme_strrev()— multibyte and grapheme-safe string reversal for UTF-8 and emoji.- Stream errors as exceptions — opt-in per stream context via
'error_mode' => StreamErrorMode::Exception. The default stays warnings, so nothing changes unless you ask. - Closure caching — the engine reuses stateless closures instead of instantiating them, worth about 3% in a real-world Laravel template test. Edge cases to know:
ReflectionFunction::getClosureThis()returnsnullfor inferred-static closures, and two stateless closures from the same location now compare identical with===.
What PHP 8.6 will break: ranked by migration pain
New syntax cannot break you — it is the tightened edges that cost time, and they sit where nobody looks until something breaks: session configuration, argument validation and old SPL APIs. Ranked by how much pain each one causes:
1. Session defaults — test this first
Three INI defaults change, and this one can break authentication without a single line of your code changing:
session.use_strict_mode: 0 → 1. PHP rejects session IDs it did not create. If you share sessions across subdomains or services by passing an externally chosen ID, that stops working.session.cookie_httponly: 0 → 1. JavaScript can no longer read the session cookie viadocument.cookie.session.cookie_samesite: unset →"Lax". The cookie is no longer sent on cross-site POST requests. Some SSO flows (SAML POST bindings) and payment provider callbacks that post back into a session are affected.
These are settings most applications should have had anyway, and frameworks usually configure cookie options explicitly — Laravel and Symfony set their own session cookie parameters. The risk sits in custom session handling and legacy code. If you depend on an old value, set it explicitly in your INI before upgrading.
2. Stricter argument validation
Many functions now throw ValueError where they used to ignore, truncate or warn about invalid input. From the UPGRADING notes: array_filter() with an invalid $mode, array_change_key_case(), pathinfo() flags and scandir() sorting order; out-of-range numbers like number_format() decimals or sleep() beyond platform limits; and NUL bytes in getenv(), putenv(), parse_str(), setlocale() and the whole filename family (file_exists(), is_file(), filesize(), stat()). That last one deserves attention: code passing user input to file_exists() used to get false for a path containing \0; now it gets an uncaught exception, turning a quiet “not found” into a 500 error. Smaller edges in the same category: trim(), ltrim() and rtrim() now strip form feed (\f) by default, and preg_grep() returns false on an execution error.
3. Deprecations — the mechanical grep list
A deprecation is not a breaking change: your code keeps running and emits E_DEPRECATED. Treat these as a PHP 9 backlog, not upgrade blockers. The mechanical ones are a search-and-replace or a Rector rule:
# Run these against your codebase this week
grep -rn --include='*.php' -E 'is_integer\(|is_long\(|is_double\(|doubleval\(' app/ src/
grep -rn --include='*.php' 'spl_object_hash(' app/ src/
grep -rn --include='*.php' -E 'mb_ereg\w*\(' app/ src/
- Scalar aliases:
is_integer(),is_long(),is_double(),doubleval()→ useis_int(),is_float(),floatval(). (The(integer)and(boolean)casts are not deprecated.) spl_object_hash()→spl_object_id(). The replacement is an integer where the hash was a string — check serialised data and cache keys before swapping.- Third argument of
define(),spl_classes()andmysqli_get_charset(). - Needs review:
returninside afinallyblock (it silently discards exceptions — every occurrence needs a decision); SPL sort/flag/serialisation methods andSplFileObject‘s CSV methods; returning values from__construct()or__destruct(); custom session handlers withoutcreate_sid()andvalidateId().
Worth knowing what did not pass: list() and the _() gettext alias were voted down and stay.
4. The out-of-memory exit code changes
On a hard out-of-memory error, PHP now calls abort() instead of exit(1). The exit code becomes 134 and a core dump may be written. If your Supervisor configs, systemd units or alerting match on exit codes, update them — this one bites in production monitoring, not in the code.
5. mbregex is on the strategic list
mb_ereg(), mb_eregi(), mb_ereg_replace(), mb_split() and friends are deprecated because the underlying Oniguruma library reached end of maintenance, with removal planned for PHP 9.0. Migrating to PCRE with the u modifier is usually straightforward, but the dialects differ — write tests first, then migrate.
Test PHP 8.6 in Docker this week
The whole point of the RC period is that real test suites find the bugs. The official Docker images already track the release candidates, so testing takes one container and zero changes to your machine:
# Confirm the RC runs (check Docker Hub for the exact RC tag)
docker run --rm php:8.6-rc-cli php -v
# Run your app's test suite against it
docker run --rm -v "$PWD":/app -w /app php:8.6-rc-cli \
php -d error_reporting=E_ALL -d display_errors=1 vendor/bin/phpunit
# Or drop into a shell and poke at the new features
docker run --rm -it -v "$PWD":/app -w /app php:8.6-rc-cli bash
On Debian or Ubuntu, Ondřej Surý’s repositories already carry the RC builds alongside your existing PHP versions — RC3 is in the pool as 8.6.0~rc3-1, so you can install php8.6-cli next to your production PHP and run the suite with an explicit binary instead of Docker:
# Ubuntu: the well-known PHP PPA (testing only — not for production)
sudo add-apt-repository ppa:ondrej/php -y
sudo apt update && sudo apt install -y php8.6-cli
php8.6 -v
php8.6 -d error_reporting=E_ALL vendor/bin/phpunit
Whatever route you take, turn error reporting all the way up. Deprecations are the signal the RC period exists to collect — run with E_ALL, read the log, and fix what you find while the release managers can still merge fixes. For WordPress specifically, our debug.log guide shows how to capture and read every warning a version bump produces.
The other deadline: PHP 8.2 loses security support on 31 December
While 8.6 arrives, 8.2 leaves. PHP’s supported-versions page gives every branch two years of active support followed by two years of security fixes only — and 8.2’s four years end on 31 December 2026:
| Branch | Active support | Security fixes until |
|---|---|---|
| PHP 8.2 | Ended Dec 2024 | 31 Dec 2026 |
| PHP 8.3 | Ended Dec 2025 | 31 Dec 2027 |
| PHP 8.4 | Ends Dec 2026 | 31 Dec 2028 |
| PHP 8.5 | Ends Dec 2027 | 31 Dec 2029 |
After 31 December, a CVE published in January 2027 stays open on your 8.2 server permanently unless your OS vendor backports it — and you should not plan around that. If you are on 8.2, the sane target is 8.3 or 8.4: both are stable, boring and fully supported, and both give you years of runway. The framework floors agree — Laravel 13’s composer.json requires PHP ^8.3, and WordPress.org recommends PHP 8.3 or greater. Jumping straight to 8.6 in its first weeks is rarely worth the risk on a production site; let the early adopters find the .0 bugs.
Check where you stand before you plan anything:
php -v # what actually runs your app
composer check-platform-reqs # what your dependencies require
If those two disagree — the CLI says 8.4 but the web server still runs 8.2 — fix the mismatch first. It is the most common “upgrade” bug there is.
When will your host offer PHP 8.6?
On your own schedule, but not on your host’s. Shared hosts add new PHP versions to cPanel’s PHP selector or their own panels on their own timetable — do not expect 8.6 in the dropdown on 19 November. That is fine, and it is actually the correct order of operations: fix the deprecations and session issues against the RC now, and flipping the selector later becomes a two-minute change instead of a debugging session.
If you manage your own VPS, the Surý packages and Docker images above get you 8.6 the day it lands. Either way, schedule the 8.2 → 8.3/8.4 move for November, not December — nobody wants to debug a PHP upgrade between Christmas and New Year, and your future self will thank you for not trying.
Your two-deadline checklist
- This week: pull the 8.6 RC (Docker or Surý) and run your full test suite with
E_ALL. Log every deprecation and failure. - Grep the mechanical deprecations (
is_integer(),spl_object_hash(),mb_ereg*) and fix them — they are a PHP 9 backlog, but they are cheapest to fix while you are looking. - Audit your session configuration. If anything depends on cross-site POST sessions, externally chosen session IDs, or JavaScript-readable session cookies, set the INI values explicitly before upgrading.
- Hand-test the risky flows against the RC: login, SSO, and every payment callback. The session defaults break these first.
- Check your production PHP version today with
php -vandcomposer check-platform-reqs. If it says 8.2, schedule the 8.3/8.4 jump for November. - Do not run an RC in production, and do not upgrade production to 8.6.0 on day one. Test on the RC, cut over after GA — and even then, the .1 release is the calmer choice.
- Update your monitoring if anything matches on PHP’s exit code: out-of-memory now aborts with 134 instead of exiting 1.
Two dates, one week of work. The release candidate is the cheapest testing you will get all year, and the 8.2 deadline is the most predictable emergency you will ever avoid. Start now.
Further Reading & References
- PHP 8.6.0 RC3 is out and ready for testing — phpnews.net, with the remaining RC4/RC5/GA timetable and references to the official RC3 announcement, NEWS and UPGRADING notes.
- PHP 8.6.0 RC2 is out, and there is no RC1 — phpnews.net, the full timetable and the story of the skipped RC1 packaging mistake.
- PHP 8.6.0 RC3 now available in Ondřej Surý’s repositories — linuxcompatible.org, the Debian/Ubuntu testing path.
- PHP 8.6 release date: what’s new and how to test your app — the official-wiki schedule, feature rundown and Docker testing walkthrough.
- PHP end of life for 8.2 arrives Dec 31 — MonsterMegs, the 8.2 retirement date and why the support cycle matters for real sites.
- PHP 8.6: the features that matter and what can break when you upgrade — migration severity analysis checked against the RC3 UPGRADING and NEWS files.
- Laravel 13: what actually breaks when you upgrade (and what doesn’t) — dev.to, including Laravel 13’s PHP 8.3 floor.
- WordPress 7.2, PHP 8.2, MySQL 8.0 upgrade calendar — the PHP support table alongside WordPress’s own release dates.



