PHP 8 migration means moving a production codebase from PHP 7.x or an early 8.x release to a supported 8.x version, today PHP 8.4, without breaking what works. The deadline is close: PHP 8.2 loses security support on December 31, 2026. And on October 21, 2026, the PHP2Shell vulnerabilities showed what running an unsupported version actually costs.
This PHP migration guide is for CTOs, engineering directors and tech leads with production on PHP 7.4-8.2. It covers which versions are still supported, what PHP2Shell changes, the breaking changes you’ll hit, the 8-step process our team uses on Docker with Rector and PHPStan, where AI helps, and when an upgrade turns into a modernization project.
Key takeaways
- Target version: PHP 8.4. It has security support until December 31, 2028, and wider library support than 8.5.
- Deadline: PHP 8.2 reaches end of life on December 31, 2026. PHP 8.1 and older are already unsupported.
- Risk: after PHP2Shell (CVE-2026-109618, CVE-2026-109619), PHP 7.4-8.1 received only source-level backports, and PHP 5.x-7.3 received no fixes from the PHP project at all.
- Effort: most of the mechanical work is automated by Rector; testing and dependency upgrades take most of the time.
- Duration: from a few weeks for a small, well-tested app to several months for a large legacy codebase. See the timeline section below.
Which PHP Versions Are Supported in 2026?
As of October 23, 2026, the PHP project supports four branches: 8.5 and 8.4 get full support, 8.3 and 8.2 get security fixes only. PHP 8.2 reaches end of life on December 31, 2026. Every version from 8.1 down, including PHP 7.4, no longer receives official releases.
|
Version |
Released |
Active support until |
Security support until |
Status (Oct 26, 2026) |
|
PHP 8.5 |
Nov 20, 2025 |
Dec 31, 2027 |
Dec 31, 2029 |
Full support |
|
PHP 8.4 |
Nov 21, 2024 |
Dec 31, 2026 |
Dec 31, 2028 |
Full support |
|
PHP 8.3 |
Nov 23, 2023 |
Dec 31, 2025 |
Dec 31, 2027 |
Security fixes only |
|
PHP 8.2 |
Dec 8, 2022 |
Dec 31, 2024 |
Dec 31, 2026 |
Security fixes only |
|
PHP 8.1 |
NA |
NA |
Ended Dec 31, 2025 |
Unsupported |
|
PHP 8.0 |
NA |
NA |
Ended Nov 26, 2023 |
Unsupported |
|
PHP 7.4 |
NA |
NA |
Ended Nov 28, 2022 |
Unsupported |
|
PHP 5.6 |
NA |
NA |
Ended Dec 31, 2018 |
Unsupported |
Sources: php.net Supported Versions, php.net Unsupported Branches.
The most important takeaways:
- PHP 8.2 end of life means any team that upgraded to 8.2 after its December 2022 release now has a second migration due by December 31, 2026.
- PHP 8.4 leaves active support on the same day, so after January 2027 it gets security fixes only, which is fine for production but worth knowing when you pick a target.
- Is PHP 7.4 end of life? Yes, since November 28, 2022, so when PHP2Shell hit, 7.4 got no official fix, only source-level backports.
Is Patching Enough After PHP2Shell, or Do You Need to Upgrade?
Patching is enough only if you run PHP 8.3, 8.4 or 8.5, which received official fixed releases. PHP 8.2 can patch now but must migrate before December 31, 2026. On PHP 7.4-8.1 you get only source-level backports or a paid LTS vendor’s build, and PHP 5.x-7.3 got no fixes at all, so migration is the only lasting answer.
On October 21, 2026, the PHP project released PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. They close two remote code execution flaws: CVE-2026-109618 (memory corruption on out-of-memory) and CVE-2026-109619 (stale global state) (NCSC-NL). Each carries a CVSS v4.0 score of 9.2. The vulnerability is in the PHP core, and it doesn’t depend on your application software. The attack needs no authentication and fires before your application code runs, so login screens and app-level checks don’t block it (Datadog Security Labs).
Any installation that serves web requests is exposed: PHP-FPM, Apache mod_php (the highest risk), php-cgi, FrankenPHP and LiteSpeed. Amazon’s MadPot honeypots recorded the first exploitation attempt on October 17, four days before disclosure. (Help Net Security)
|
Your version |
What’s available after PHP2Shell |
What to do |
|
8.3-8.5 |
Official release |
Patch and restart PHP workers |
|
8.2 |
Official release |
Patch now, migrate before Dec 31, 2026 |
|
7.4-8.1 |
Source-level backports or LTS vendor patches only |
Temporary protection plus a migration plan |
|
5.x-7.3 |
No fixes from the PHP project |
Migrate or modernize; meanwhile a paid LTS vendor or taking the system offline |
PHP2Shell is not a one-off: the PHP project ships fixes for PHP vulnerabilities only in supported branches, so any version past the end of life keeps collecting holes that will never be patched officially.
Workarounds such as disabling session.upload_progress reduce exposure but don’t close the holes. Datadog puts it bluntly: “Only patching fixes these vulnerabilities.” In the US, CISA published an alert Critical Vulnerabilities in PHP Require Immediate Patching, and FBI and NSA guidance for systems that can’t be updated right away is to “take the system offline until it can be fully mitigated”.
What does it mean for a business running on PHP? A single unsupported runtime can turn a security bulletin into severe downtime, and upgrading PHP is how you pay down the technical debt that made the bulletin dangerous.
What Changed in PHP 8.x (8.0 to 8.5)
The difference between PHP 7 and PHP 8 is mostly about stricter typing, errors that throw instead of warning, and a faster engine. PHP 8.0 added JIT, named arguments, union types and match. Each later release added language features and deprecated old behavior, so the jump from 7.4 to 8.4 covers five sets of changes.
PHP 8.0 (2020) brought the Zend Engine 4 and JIT compilation. JIT helps with CPU-bound code such as image processing or calculations, but typical I/O-bound web apps gain little from it, so measure before you count on it. Attributes replaced PHPDoc annotations for structured metadata, which is why Symfony and Doctrine moved routing and mapping to #[Route] and #[ORM\Column].
|
Version |
What’s new |
What it deprecates or breaks |
|
8.0 |
JIT, attributes, named arguments, union types, match, nullsafe operator ?-> |
String-to-number comparison rules, many warnings become TypeError/ValueError |
|
8.1 |
Enums, readonly properties, fibers, never type, first-class callable syntax |
Passing null to non-nullable internal function parameters |
|
8.2 |
Readonly classes, DNF types, true/false/null as standalone types |
Dynamic properties, ${var} string interpolation, utf8_encode()/utf8_decode() |
|
8.3 |
Typed class constants, #[\Override], json_validate() |
Calling get_class()/get_parent_class() without arguments |
|
8.4 |
Property hooks, asymmetric visibility, lazy objects, new without extra parentheses |
Implicitly nullable parameter types (Foo $x = null); IMAP, OCI8 and Pspell moved to PECL |
|
8.5 |
Pipe operator |>, #[\NoDiscard], built-in URI extension, array_first()/array_last() |
Backtick shell operator, non-canonical casts such as (integer) and (boolean) |
PHP 8 Breaking Changes to Check Before You Upgrade
Now let’s talk about PHP 8 breaking changes, which php.net calls backward incompatible changes, and which mostly show up as fatal errors on old syntax, TypeError exceptions where PHP 7 only warned, and deprecation floods from 8.1 onward.
Automated refactoring tools like Rector, which rewrites old PHP syntax for a newer version, fix most syntax-level issues; behavior changes, such as how strings compare to numbers, need tests and human review.
The official issue list is long: start with Migrating from PHP 7.4 to 8.0 and PHP 8.4 backward incompatible changes. These are the ones we see break real projects most often:
|
Change |
Version |
Symptom |
How to fix |
Rector? |
|
0 == “abc” is now false |
8.0 |
Silent logic changes in validation and lookups |
Find loose comparisons, add tests, switch to === |
No |
|
Curly-brace string offsets $str{0} removed |
8.0 |
Parse error |
$str[0] |
Yes |
|
each(), create_function() removed |
8.0 |
Fatal error on old libraries |
Rewrite with foreach and closures |
Partly |
|
Arithmetic on arrays/objects, invalid internal arguments throw |
8.0 |
TypeError instead of a warning |
Fix types at the source; add strict types gradually |
No |
|
@ no longer hides fatal errors; E_ALL by default |
8.0 |
Errors appear that were always there |
Fix them; don’t restore the old level |
No |
|
Resources became objects (curl_init() returns CurlHandle) |
8.0 |
is_resource() checks fail |
Check !== false or instanceof |
Yes |
|
null to non-nullable internal parameters |
8.1 |
Deprecation log floods (strlen(null)) |
Cast or guard values |
Partly |
|
Missing return types on ArrayAccess, Iterator, etc. |
8.1 |
Deprecations in older libraries |
Add return types or #[\ReturnTypeWillChange] |
Yes |
|
Dynamic properties deprecated |
8.2 |
Deprecations on legacy models and DTOs |
Declare properties or use #[\AllowDynamicProperties] |
Partly |
|
Implicitly nullable parameters deprecated |
8.4 |
Deprecations across code written for 7.x |
?Foo $x = null |
Yes |
You should treat these deprecations as future fatal errors: a codebase that logs thousands of 8.1 deprecations will hit a wall at PHP 9.
PHP Migration Paths: 7.4 to 8.4 and 8.1/8.2 to 8.4/8.5
Should you upgrade PHP 7.4 directly to 8.4 or go version by version? Both. You step through 8.0, 8.1, 8.2 and 8.3 locally, fixing and committing at each stop, then deploy straight to 8.4 in production. In this way you get small, reviewable diffs without running four production releases.
Why? Because the local steps keep each change set readable. A single 7.4 to 8.4 diff from Rector can touch thousands of files, and when a test fails you won’t know which version’s change caused it. Only the end state goes to production.
From 8.2, the path will be shorter: patch for PHP2Shell first, then move to 8.4. Choose 8.5 only if your full dependency tree already supports it; composer why-not php 8.5 tells you in seconds.
The framework your system runs on usually decides the migration order. You often have to upgrade the framework and PHP in alternating steps, because each version supports a limited PHP range.
|
Framework version |
Supported PHP |
Notes |
|
Laravel 10 |
8.1-8.3 |
Out of security support |
|
Laravel 11 |
8.2-8.4 |
Security support ended March 2026 |
|
Laravel 12 |
8.2-8.5 |
Security fixes only until Feb 24, 2027; use as a stepping stone, not a final target |
|
Laravel 13 |
8.3+ |
Recommended target for 7.4-era Laravel apps; security fixes until Q1 2028 |
|
Symfony 5.4 LTS |
7.2.5+ officially; up to 8.3 in practice |
No longer developed; Symfony maintainers advise PHP 8.3 at most, though 8.4 may work. We ran 5.4.23 on PHP 8.2.4 with Doctrine, StofDoctrineExtensionsBundle, RedisBundle and EasyAdmin. Upgrade to 6.4 or 7.4 LTS before moving to PHP 8.4 |
|
Symfony 6.4 LTS |
8.1+ |
Common stepping stone from 5.4 |
|
Symfony 7.4 LTS |
8.2+ |
NA |
|
Symfony 8.0 |
8.4+ |
NA |
How Long Does a PHP 8 Migration Take?
From our experience, a small, well-tested app usually moves from PHP 7.4 to 8.4 in a few weeks. A mid-size product with framework upgrades takes a few months, and a large legacy system with low test coverage can take six months or even more. The rule here: test coverage and dependency age drive the estimate more than lines of code.
|
Codebase profile |
Typical signs |
Duration |
|
Small, maintained |
Current framework, 60%+ test coverage, few custom extensions |
3-6 weeks |
|
Mid-size product |
One or two framework majors behind, partial tests, some abandoned packages |
2-4 months |
|
Large legacy |
Custom framework or 5.x-era code, little or no tests, business logic in templates |
6-12+ months |
Top 6 PHP Migration Tools 2026
You can use five tools:
- Rector to rewrite code automatically
- composer why-not to find blocking packages
- PHPStan to catch type errors
- PHP_CodeSniffer to keep style consistent
- PHPCompatibility to find incompatible code
- AI coding assistants
Together they automate most of a PHP 7 to 8 migration but people still own tests and behavior control.
|
Tool |
What it does |
When to run it |
|
Rewrites code with version-specific rule sets; also upgrades Symfony, Laravel, Doctrine and PHPUnit code |
At each local version stop | |
|
composer why-not php 8.4 |
Lists packages whose constraints block the target version |
Before you start and after each framework upgrade |
|
Static analysis that finds type errors and dead code without running it |
After every Rector pass and in CI | |
|
Checks and auto-fixes coding standards (PSR-12 or PER Coding Style) |
After Rector, so diffs stay readable | |
|
Scans code for syntax and functions that break on a target PHP version |
Before you start, to size the work |
Rector is the closest thing to a PHP 7 to 8 migration tool that does the job end to end. PHPStan is especially useful on legacy code with few tests, because it finds the type problems PHP 8 will now throw at runtime. If you still run PHP 5.6, the same tools apply; Rector has sets from PHP 5.3 upward, so the PHP 5.6 to 7 migration tool and the PHP 7 to 8 one are the same tool.
To size the job before you change anything:
composer why-not php 8.4
composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev phpcompatibility/php-compatibility:"^10.0.0@dev"
vendor/bin/phpcs -ps src --standard=PHPCompatibility --runtime-set testVersion 8.4
PHPCompatibility checks for PHP 8.x live in the 10.x line, which is still in alpha, so install it explicitly as shown above and use as an additional tool; the stable 9.3.5 release won’t flag PHP 8 issues.
8 Steps to Migrate to PHP 8.4
Here’s the PHP 8 upgrade flow our team uses: containerize, upgrade the framework and libraries, switch the PHP version, update Composer, let Rector fix the code, align code style, verify with PHPStan and tests, then test manually.
The examples use a Symfony setup and target PHP 8.4.27, the first 8.4 release with the PHP2Shell fixes.
Step 1. Containerize the application
Docker lets you switch PHP versions on one machine and gives every developer and CI the same runtime. If the app isn’t containerized yet, do that first, while it still runs on 7.4, so the container is the only variable that changes later.
Step 2. Check framework and library support
Check your framework’s documentation for PHP 8 support and upgrade the framework if needed (see the compatibility table above). Then check every library. composer why-not php 8.4 and composer outdated –direct show what’s blocking you. If you’re below 7.4, get to 7.4 first: it’s the last 7.x release and the shortest jump to 8.x.
Step 3. Switch the PHP version
Change the image in docker-compose.yml to the first stop on the route, PHP 8.0:
services:
app:
# was: image: php:7.4.33-fpm
image: php:8.0-fpm
volumes:
- ./:/var/www/html
docker compose up -d --build
Expect errors at this point: the runtime is on 8.0, but the code and dependencies aren’t.
Steps 3-7 form one iteration. Once tests pass on 8.0, commit and repeat them for 8.1, 8.2, 8.3 and finally PHP 8.4 (php:8.4.27-fpm, the release with the PHP2Shell fixes). Each iteration gives you a small diff you can review and roll back.
Step 4. Update dependencies with Composer
Set the PHP requirement to the version you just switched to, and pin the platform version so Composer resolves packages for that runtime, not for the PHP on your laptop:
{
"require": {
"php": ">=8.0"
},
"config": {
"platform": {
"php": "8.0.30"
}
}
}
{
"require": {
"php": ">=8.0"
},
"config": {
"platform": {
"php": "8.0.30"
}
}
}
docker compose exec app composer update --with-all-dependencies
In each iteration, raise both values to the PHP version you’re on now. On the final iteration they become >=8.4 and 8.4.27. If the old PHP version blocked framework upgrades in Step 1, run them now.
Step 5. Fix code with Rector
Install Rector and create rector.php in the project root:
docker compose exec app composer require --dev rector/rector
<?php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/src',
__DIR__ . '/tests',
])
// Applies every PHP-level rule up to 8.4 (replaces SetList::PHP_82 from the 2023 version of this guide)
->withPhpSets(php84: true)
->withAttributesSets(symfony: true, doctrine: true);
withPaths() lists the folders Rector scans. For the version-by-version route, start with php80: true, commit, and move up one version at a time. Preview first, then apply:
docker compose exec app vendor/bin/rector process --dry-run
docker compose exec app vendor/bin/rector process
Clear the framework cache afterward (bin/console cache:clear in Symfony) or restart the containers.
Step 6. Align code style (optional)
Rector’s output follows its own formatting. Run PHP_CodeSniffer so the diff your reviewers see is about logic, not whitespace:
docker compose exec app composer require --dev squizlabs/php_codesniffer
docker compose exec app vendor/bin/phpcs --standard=PSR12 src
docker compose exec app vendor/bin/phpcbf --standard=PSR12 src
Step 7. Verify with PHPStan and automated tests
Rector uses PHPStan internally to understand types, but that doesn’t replace running PHPStan yourself:
docker compose exec app composer require --dev phpstan/phpstan
docker compose exec app vendor/bin/phpstan analyse src --level=5
docker compose exec app vendor/bin/phpunit
Aim for “No errors” at a level you can hold in CI, then raise it gradually. Run unit and API tests and confirm everything passes.
Step 8. Test manually
Automated tests miss things: a report that renders slightly differently, a payment callback, a cron job that only runs on the first of the month. Run the critical user journeys by hand, ideally with QA engineers who know the product.
Zero-downtime rollout
For production, run the new PHP version side by side with the old one: a separate PHP-FPM pool or container set behind the load balancer, with traffic shifted gradually (blue-green or rolling). Move queue workers and cron jobs separately, because they often run code paths the web tests never touch. Keep the old pool warm until you’ve seen a full business cycle, and write the rollback steps down before you start.
How AI Speeds Up PHP Migration and Where It Doesn’t
AI helps most where Rector stops: code Rector has no rule for, characterization tests for untested legacy code, and explaining what old modules actually do. AI doesn’t replace Rector, static analysis or human review. We use AI inside a pipeline where every change passes independent checks before a person approves it.
|
Stage |
Tool |
What AI adds |
|
Find what breaks |
PHPCompatibility, composer why-not |
Summarizes findings by module and estimates effort |
|
Fix known patterns |
Rector (withPhpSets, level sets) |
Nothing: deterministic rules beat generated code here |
|
Catch type errors |
PHPStan or Psalm |
Drafts fixes for errors Rector can’t resolve |
|
Cover legacy behavior |
PHPUnit |
Generates characterization tests that pin down today’s behavior before the upgrade |
|
Fix the long tail |
AI coding agent in a sandbox branch |
Rewrites custom extensions, dynamic-property models, abandoned-package replacements |
|
Verify and ship |
CI, code review, staged rollout |
Nothing; engineers decide |
The rules come from our AI-assisted software modernization approach:
- Capture existing behavior in tests before AI agents touch anything
- Give the agent narrow authority: it can read and propose, and execute only in a sandbox
- Never let the same agent write the fix and provide the only proof it works
- Rector, PHPStan and the test suite are the independent checks
PHP in 2026: Upgrade, Extended Support, or Modernize?
For most engineering teams the answer is to upgrade to PHP 8.4, because extended support from an LTS vendor can buy time but it only postpones the decision. Modernization makes sense when the version is only the visible problem and the architecture is what blocks the business.
Read our PHP application modernization case studies to see how this played out on real products.
|
Option |
When it fits |
Cost |
Risk |
Timeline |
|
Upgrade to PHP 8.4 |
Supported framework, reasonable tests, architecture that still serves the roadmap |
One-off project |
Low with tests and staged rollout |
Weeks to months |
|
Extended support (LTS vendor) |
Upgrade can’t finish before a hard deadline; low-change or soon-to-retire systems |
Recurring subscription |
You depend on a vendor’s backports; technical debt keeps growing |
Immediate |
|
Modernize |
Upgrade is blocked by custom frameworks, no tests, or architecture the roadmap can’t build on |
Larger, staged investment |
Managed through bounded stages |
Months, delivered in increments |
Then score your system against these criteria to decide:Then score your system against these criteria to decide:
|
Criterion |
Points to upgrade |
Points to modernize |
|
Compliance |
Auditors only need a supported runtime |
Audit findings go beyond the PHP version |
|
Architecture |
Framework-based, modular |
Custom framework, logic in templates, shared database everyone writes to |
|
Test coverage |
Critical journeys covered |
Few or no tests |
|
Roadmap |
Next features fit the current design |
Next features (AI, integrations, scale) need a different design |
|
Budget |
One project |
Phased budget over several quarters |
There’s a middle option we often recommend: a two-phase approach: runtime upgrade + strangler fig pattern
Phase one upgrades the runtime to 8.4 with minimal code changes, so security stops being urgent. Phase two moves the worst modules behind new services one at a time, with old and new running side by side until the new path earns trust.
How MobiDev Can Help
MobiDev has been building and modernizing custom software since 2009, and our PHP team has migrated production apps across framework and PHP versions, including Symfony. We start with a software audit: dependencies, test coverage, PHP2Shell exposure, and a timeline you can plan around. Then we run the migration in your repository, with your in-house team, and roll out without downtime.
If the upgrade turns out to be the start of a bigger job, our legacy PHP modernization services cover the next stage.
Need a Tech Consultation on PHP Migration? Schedule a call
PHP 8 Migration FAQ
As of October 2026, PHP 8.5 and 8.4 have full support, and PHP 8.3 and 8.2 receive security fixes only. PHP 8.2 support ends on December 31, 2026, PHP 8.3 on December 31, 2027, and PHP 8.4 on December 31, 2028. PHP 8.1, 8.0, 7.4 and all older versions are end of life. The latest version of PHP in 2026 is 8.5.
PHP 8.2 reaches end of life on December 31, 2026. Active support ended on December 31, 2024, and since then it has received security fixes only, including the PHP2Shell patch in PHP 8.2.35. After the end of life date there will be no official releases, so teams on 8.2 should plan the move to PHP 8.4 now.
No. PHP 7.4 reached end of life on November 28, 2022, and it now receives no official security releases. After PHP2Shell, the PHP project published only source-level backports for 7.4, so you either build patched PHP yourself, pay an LTS vendor, or migrate. Is PHP 7.4 outdated? Yes, by four years and six releases.
PHP2Shell is the name for two critical remote code execution flaws, CVE-2026-109618 and CVE-2026-109619, disclosed on October 21, 2026, with a CVSS v4.0 score of 9.2. They affect any PHP installation that serves web requests. Fixed releases exist for PHP 8.2-8.5; PHP 7.4-8.1 got source backports only, and 5.x-7.3 got no fixes.
Patching is enough on PHP 8.3, 8.4 and 8.5. On PHP 8.2, patch now and upgrade before December 31, 2026. On PHP 8.1 or older there’s no official release to install, so patching means backports or a vendor’s build, and only an upgrade fixes the problem for good. Workarounds alone don’t close the vulnerabilities.
PHP 8 adds a JIT compiler, attributes, named arguments, union types, match, enums and readonly properties, and it’s stricter: many PHP 7 warnings now throw TypeError or ValueError, and string-to-number comparisons work differently. PHP 7 is fully end of life, while PHP 8.2-8.5 still receives security fixes.
Go version by version locally and deploy once. Run Rector and your tests at 8.0, 8.1, 8.2 and 8.3, committing each step so diffs stay reviewable, then release straight to PHP 8.4 in production. Framework constraints often force this order anyway, since Laravel and Symfony versions each support a limited PHP range.
A small, well-tested app can move in a few weeks. A mid-size product with framework upgrades usually takes a few months, and a large legacy codebase with little test coverage can take six months or longer. Test coverage, abandoned dependencies and framework upgrades drive the timeline more than code size.
Partly. Rector handles most version-specific rewrites with deterministic rules, and AI is useful for what Rector can’t do: custom code, characterization tests for untested modules, and replacements for abandoned packages. AI output still needs independent verification through PHPStan, the test suite and human review before it reaches production.
Buy extended support only as a bridge: when the migration can’t finish before a compliance or security deadline, or when the system will be retired soon. It keeps an old runtime patched but doesn’t reduce technical debt or unblock framework upgrades. For a product you’ll keep developing, migration to PHP 8.4 costs less over time.