Upgrade guide: PHP 8.2 to 8.3
PHP 8.2 to 8.3: one release, before 31 December 2026
PHP 8.2 reaches end of life on 31 December 2026. Its PHP2Shell fix, 8.2.35, is a normal point update, so install that first. Moving on to 8.3 is a single release step with a short list of breaking changes, and it keeps the PHP project's security fixes coming until 31 December 2027.
Last reviewed against php.net's PHP 8.3 migration guide and the PHP2Shell advisories.
Fix first, then migrate
On 8.2, the security fix is a patch, not a project
Unlike 8.1 and older, 8.2 is still supported, and it received a PHP2Shell release on 21 October 2026: 8.2.35. A point update within the branch is not a migration, so do it now, separately from the move to 8.3.
- Restart PHP after updating so no worker keeps running the old build, then confirm the version over the web, not only on the command line.
- On a distribution package, the fix may arrive under the distribution's own version string; its security notice is the authority.
- If even the point update is delayed, the PHP project's interim settings reduce the risk while you wait.
Both branches side by side
| Branch | Stage | Support ends | PHP2Shell fix |
|---|---|---|---|
| PHP 8.2 | Security fixes only since the end of 2024 | 31 Dec 2026 | 8.2.35 |
| PHP 8.3 | Security fixes only since the end of 2025 | 31 Dec 2027 | 8.3.36 |
If the server is not on a supported branch at all, the risk calculator is the place to start, and the upgrade hub lists every route.
The plan in four moves
From 8.2.35 today to 8.3 before the deadline
Now
Install 8.2.35
The PHP2Shell fix for your current branch. A point update inside 8.2, so it needs a normal patch check, not a migration.
Next
Test the code on 8.3.36
A staging copy on the fixed 8.3 build, logging everything, with the changes below checked one by one.
Before 31 December 2026
Switch production
Move the live site while 8.2.35 is still a supported fallback, then confirm the version a web request reports.
During 2027
Plan the next move
8.3 is supported until 31 December 2027. Clear 8.4's deprecations on 8.3 so the next step is small.
The 8.3 changes
PHP 8.3, change by change, for code that runs on 8.2
With only one release to absorb, this route can afford to look at the whole 8.3 list rather than the headlines. It falls into three groups: changes that can stop a request, changes that alter results quietly, and deprecations.
Changes that can stop a request
Deep recursion can now throw
What changed: 8.3 adds two settings, zend.max_allowed_stack_size and zend.reserved_stack_size. A program whose stack use goes beyond the first minus the second now gets an Error instead of crashing the process.
What to look for: Recursive menu builders, tree serialisers and template includes that call themselves.
Dates fail with exceptions
What changed: The date extension gained its own DateError and DateException class families. DateTime::modify() with an unparseable string throws DateMalformedStringException instead of returning false.
What to look for: Free-text date input and imported timestamps. These are new global class names too, so a project that defined its own global DateException class now collides with PHP's.
range() rejects bad input
What changed: Passing an array, object or resource as a limit raises TypeError, and ValueError now covers zero steps, negative steps on ascending ranges and infinite or NAN arguments.
What to look for: range() fed from query strings or configuration.
Stricter argument checks in several extensions
What changed: proc_open() given an array command needs at least one non-empty element, or it throws ValueError. mysqli_fetch_object() and pg_fetch_object() throw ValueError when constructor arguments are given for a class with no constructor, and pg_insert() throws where it used to warn. IntlDateFormatter throws for an invalid locale.
What to look for: Database helpers that hydrate objects, and command runners that build argument arrays dynamically.
New global function names
What changed: 8.3 adds json_validate(), str_increment(), str_decrement(), mb_str_pad() and stream_context_set_options() to the global namespace.
What to look for: Polyfills or helpers declaring those names without a function_exists() guard.
A removed opcache setting
What changed: opcache.consistency_checks no longer exists.
What to look for: Old php.ini files that still set it.
Changes that alter results without an error
<?php
$list = [];
$list[-5] = 'first';
$list[] = 'second';
var_dump(array_keys($list));
// PHP 8.2: [-5, 0]
// PHP 8.3: [-5, -4] Trait static properties
What changed: When a class uses a trait with a static property, it now gets separate storage for that property rather than sharing the one inherited from its parent.
What to look for: Shared counters or caches kept in trait statics across parent and child classes.
Negative keys on an empty array
What changed: The next automatic key after $list[-5] = 'x' on an empty array is now -4, not 0.
What to look for: See the example above; tests that assert exact array keys show it straight away.
Numbers, sums and strings
What changed: number_format() with negative decimals rounds before the decimal point. array_sum() and array_product() warn about values they cannot convert, and now convert numeric objects such as GMP. str_getcsv() and fgetcsv() return an empty string for a last field holding only an unterminated enclosure.
What to look for: Reports, totals and CSV imports with unusual data.
Text encoding details
What changed: mb_strtolower() and mb_convert_case() apply the Greek final sigma rule, and mb_decode_mimeheader() turns underscores in QPrint-encoded words into spaces.
What to look for: Email parsing and any Greek-language content.
Processes, handlers and output
What changed: Repeated proc_get_status() calls now return the right result on POSIX systems, user exception handlers catch exceptions thrown during shutdown, and highlight_string() produces different HTML.
What to look for: Process supervisors that worked around the old status bug, and shutdown code that relied on exceptions going uncaught.
Serialised data
What changed: Problems found by unserialize() are reported as warnings rather than notices, and leftover bytes at the end of the input now trigger a warning of their own.
What to look for: Cache and session stores; the warnings point to corrupted or truncated entries.
Deprecated in 8.3
These still work on 8.3 but log a notice. Search for each:
get_class()andget_parent_class()called without an argument- incrementing a string that is empty or not alphanumeric, or decrementing one that is empty or not numeric (
str_increment()andstr_decrement()are the replacements) assert_options(), theASSERT_*constants and non-defaultassert.*ini settings (usezend.assertions)ReflectionProperty::setValue()with one argument (passnullfirst for a static property)ldap_connect()with a separate host and port- a negative width in
mb_strimwidth() Phar::setStub()with a resource and a lengthdba_fetch()with the handle as its third argument- calling
FFI::cast(),FFI::new()andFFI::type()statically - the
MT_RAND_PHPvariant ofmt_rand() NumberFormatter::TYPE_CURRENCYand the misspeltU_MULTIPLE_DECIMAL_SEPERATORSconstantSQLite3::enableExceptions(false), andZipArchive::FL_RECOMPRESS
On Windows, note that 8.3 needs Windows 8 or Windows Server 2012 as a minimum.
The full guide on php.net: backward incompatible changes, deprecated features, other changes, new functions.
Already in your log
The 8.2 notices that come along unchanged
Anything 8.2 deprecated is still only a deprecation on 8.3. If those notices were muted on 8.2, the move is a good moment to deal with them, because they will still be there for the step after.
- Undeclared properties. Declare them, or mark the class
#[\AllowDynamicProperties]as a stopgap. "${var}"interpolation. Rewrite as"{$var}".utf8_encode()andutf8_decode(). Move tomb_convert_encoding()or one of the other options php.net lists.- Relative callables. Strings such as
"self::method"become[self::class, 'method']or first-class callable syntax.
Before the next step
8.4 deprecations to get ahead of during 2027
Landing on 8.3 means another move before 31 December 2027. These 8.4 changes can be made in code that still runs on 8.3:
- Parameters made nullable by their default.
Type $x = nullis deprecated in 8.4. Writing?Type $x = nulltoday removes the notice in advance. - Session tuning. Changing
session.sid_length,session.sid_bits_per_characterorsession.use_trans_sidis deprecated, as is theSIDconstant. php.net's advice is to let session storage accept the default 32-character hexadecimal IDs. lcg_value(). Deprecated;Random\Randomizer::getFloat(), available since 8.3, replaces it.xml_set_object(). Deprecated, along with passing method names as plain strings to thexml_set_*_handler()functions; pass callables instead.exit()with odd arguments. From 8.4 an invalid type throwsTypeError, so keep exit values to strings and integers.
Tooling
Light tooling for a one-release move
php.net's 8.3 guide is short enough to read in full, and the lists above follow it. Open source analysers then save time on large code bases by finding every occurrence.
- PHPCompatibility for PHP_CodeSniffer with
testVersionset to8.2-8.3reports code that would break on either version. The rules for recent releases come in its 10.0 pre-release line rather than the 9.3.5 stable release of 2019. - Rector with
withPhpSets(php83: true)proposes rewrites for the 8.3 set and all earlier ones;--dry-runshows them first. - PHPStan with
phpVersion: 80300analyses the code as 8.3 code. - Composer's
why-not php 8.3lists dependencies that would block the switch.
vendor/bin/phpcs -p . --standard=PHPCompatibility --runtime-set testVersion 8.2-8.3
vendor/bin/rector process --dry-run # with ->withPhpSets(php83: true) in rector.php
composer why-not php 8.3 Project documentation: PHPCompatibility, Rector, PHPStan, Composer.
Testing and rollout
Run 8.3 next to 8.2, then switch
- A second PHP version on the same host. Add 8.3.36 or later as its own PHP-FPM pool, or select it for a staging site in your control panel, so production stays on 8.2.35.
- The same extensions. Compare
php -mand a web-servedphpinfo()for both versions; on systems that package extensions per PHP version, each one has to be installed again for 8.3. - Composer told about 8.3. Set
config.platform.phpto8.3.36in a branch, runcomposer update, and on the staging site runcomposer check-platform-reqs, which checks the real platform rather than the configured one. - Notices on, display off.
error_reporting = E_ALLandlog_errors = Onwithdisplay_errors = Off. The production template that ships with PHP excludes deprecations, so set the level explicitly on staging. - A full walk-through. Every template, form, payment and email, plus scheduled jobs and command line scripts, which may use a different
php.ini(php --inishows it). - The switch. Point production at the 8.3 pool or setting in a quiet period, restart PHP, and watch the log.
Rolling back, and the deadline
Of all the routes off an old branch, this one has the easiest fallback, provided you fall back to 8.2.35 or later: that build carries the PHP2Shell fix. An older 8.2 build reopens both CVEs. The fallback also has an expiry date. After 31 December 2026, 8.2 receives nothing further from the PHP project, so a rollback in January buys time only until the next PHP flaw. Keep the 8.2 pool in place until 8.3 has run cleanly, then remove it.
Platform constraints
Most current platforms accept both 8.2 and 8.3
On this route, the platform question is usually whether your release already lists 8.3, and whether 8.3 unlocks a newer release you want anyway.
- Magento 2: 2.4.7 lists both 8.2 and 8.3, so PHP can move on its own. 2.4.6 stops at 8.2 and needs upgrading first.
- Laravel: 11 and 12 run on both. Laravel 13 requires 8.3, so the PHP move opens the way to it.
- Drupal: 10.4 to 10.6 accept both versions; 11.1 and later need 8.3.
- Moodle: 8.3 is usable from 4.4, and 5.2 and 5.3 require it.
- Nextcloud: 33 and 34 already mark 8.2 as deprecated, and 35 supports 8.3 to 8.5 only.
- Joomla: Joomla 6 needs 8.3; Joomla 5 recommends it.
- PrestaShop 9.0 and 9.1, and OpenMage from 20.14.0, support both versions.
- WooCommerce: asks for 8.3 or greater, so a shop on 8.2 is already below its stated requirement.
Server steps: how to update PHP, including cPanel, Plesk, PHP-FPM, Linux, shared hosting and WordPress. Other routes: 8.2 to 8.4, 8.1 to 8.3, 8.1 to 8.4, 7.4 to 8.4, 7.4 to 8.3 and 8.0 to 8.4.
Quick answers
PHP 8.2 to 8.3 questions
Does PHP 8.2.35 fix PHP2Shell?
Yes. 8.2.35, released on 21 October 2026, contains the fixes for CVE-2026-109618 and CVE-2026-109619. Any 8.2 release below it is vulnerable. If your PHP comes from a distribution, its package may show an older-looking number and still include the fix, so check the supplier's security notice.
What happens if my site is still on PHP 8.2 on 1 January 2027?
It keeps running. What stops is the supply of fixes: any PHP flaw found from that date gets no 8.2 release from the PHP project. A server left on 8.2 would then be in the position 8.1 servers were in when PHP2Shell was disclosed.
Should I go to 8.3 or straight to 8.4 from 8.2?
8.3 is the smaller change and the safer bet if time before 31 December is short or your platform tops out at 8.3. 8.4 adds a year of support, to 31 December 2028, in exchange for 8.4's own changes, including four extensions leaving the core.
Can PHP 8.2 and 8.3 run side by side while I test?
Usually, yes. Control panels such as cPanel and Plesk let you choose the PHP version per site, and on your own servers you can run a separate PHP-FPM pool for each version. That lets a staging site run 8.3.36 next to a production site on 8.2.35.
Will my supplier keep patching PHP 8.2 after 31 December 2026?
That depends entirely on the supplier. Linux distributions and long-term support vendors set their own support periods for the PHP packages they ship, which can differ from php.net's. Read their published policy rather than assuming either way, and plan the move regardless.
Know your exposure
Find every PHP server you run
Patching the server you know about is the easy part. A vulnerability scan finds the forgotten staging site, the old microsite and the appliance with PHP inside, then tells you which ones are still exposed.