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

3D shield with a green tick above a device, for patching to 8.2.35 now

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.

3D network of connected machines, for testing on 8.3

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.

3D planner with a clock and calendar, for switching before 31 December

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.

3D maze with a route out, for planning the next move in 2027

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() and get_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() and str_decrement() are the replacements)
  • assert_options(), the ASSERT_* constants and non-default assert.* ini settings (use zend.assertions)
  • ReflectionProperty::setValue() with one argument (pass null first 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 length
  • dba_fetch() with the handle as its third argument
  • calling FFI::cast(), FFI::new() and FFI::type() statically
  • the MT_RAND_PHP variant of mt_rand()
  • NumberFormatter::TYPE_CURRENCY and the misspelt U_MULTIPLE_DECIMAL_SEPERATORS constant
  • SQLite3::enableExceptions(false), and ZipArchive::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() and utf8_decode(). Move to mb_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 = null is deprecated in 8.4. Writing ?Type $x = null today removes the notice in advance.
  • Session tuning. Changing session.sid_length, session.sid_bits_per_character or session.use_trans_sid is deprecated, as is the SID constant. 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 the xml_set_*_handler() functions; pass callables instead.
  • exit() with odd arguments. From 8.4 an invalid type throws TypeError, so keep exit values to strings and integers.

php.net: deprecated in PHP 8.4

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 testVersion set to 8.2-8.3 reports 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-run shows them first.
  • PHPStan with phpVersion: 80300 analyses the code as 8.3 code.
  • Composer's why-not php 8.3 lists 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

  1. 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.
  2. The same extensions. Compare php -m and a web-served phpinfo() for both versions; on systems that package extensions per PHP version, each one has to be installed again for 8.3.
  3. Composer told about 8.3. Set config.platform.php to 8.3.36 in a branch, run composer update, and on the staging site run composer check-platform-reqs, which checks the real platform rather than the configured one.
  4. Notices on, display off. error_reporting = E_ALL and log_errors = On with display_errors = Off. The production template that ships with PHP excludes deprecations, so set the level explicitly on staging.
  5. 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 --ini shows it).
  6. 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.

Reading backported versions on Linux

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.

The PHP 8.2 to 8.4 guide

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.

PHP-FPM pools and restarts

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.

3D rocket illustration above the call to find every exposed PHP server

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.