Migrating to PHP 8
Why Migrate? In simple terms: Every PHP version eventually stops receiving security fixes. Running an unsupported version means known vulnerabilities stay open forever. Newer versions are also signifi
Why Migrate?
In simple terms: Every PHP version eventually stops receiving security fixes. Running an unsupported version means known vulnerabilities stay open forever. Newer versions are also significantly faster and bring features that make code shorter and safer — union types, enums, readonly properties, match, named arguments, and more. Migrating an older application (for example, from PHP 7.4) to a current PHP 8 release is one of the most valuable maintenance tasks a team can do.
Technically: Each release documents its backward-incompatible changes, deprecations, and new features in the migration appendices of the PHP manual. PHP 8.0 was a major release with several breaking changes; the 8.x minor releases add features and mostly turn old behavior into deprecation notices first, before removing it in the next major version. A safe migration combines automated tools (static analysis, Rector), tests, deprecation logging, and a staged rollout.
Supported Versions
Each PHP version receives two years of active support followed by two years of security fixes. Check https://www.php.net/supported-versions.php for the current status and plan upgrades before a version reaches its end of life.
The Most Important Breaking Changes in PHP 8.0
Saner String-to-Number Comparisons
<?php
var_dump(0 == 'a');
var_dump('abc' == 0);
var_dump('1' == '01');
var_dump('10' == '1e1');
var_dump(100 == '1e2');
Output (PHP 8)
bool(false)
bool(false)
bool(true)
bool(true)
bool(true)
| Comparison | PHP 7.4 | PHP 8 |
|---|---|---|
0 == 'a' | true | false |
'abc' == 0 | true | false |
'1' == '01' | true | true |
'10' == '1e1' | true | true |
PHP 8 compares a number with a non-numeric string as strings instead of converting the string to 0. Code that relied on the old behavior — often unknowingly, for example in in_array() without the strict flag or in switch statements — may now take different branches. Using === and strict in_array(..., true) avoids the issue entirely (see Comparison Operators).
Other Notable 8.0 Changes
| Change | Effect |
|---|---|
Internal functions throw TypeError / ValueError for invalid arguments | strlen([]) throws instead of warning and returning null |
| Removed functions | each(), create_function(), money_format(), get_magic_quotes_gpc(), and others |
@ no longer silences fatal errors | Hidden failures become visible |
Default error_reporting is E_ALL | More notices and deprecations appear |
| Stricter checks for arithmetic on arrays, objects, and null | [] + 1 throws TypeError |
| Methods with the class name are no longer constructors | Old PHP 4–style constructors must be renamed to __construct() |
| Many resources became objects | curl_init(), xml_parser_create(), imagecreate() return objects; is_resource() checks fail |
Deprecations in the PHP 8.x Series
Deprecations do not break code yet, but they will become errors in the next major version. Log them and fix them early.
| Version | Key Deprecations and Changes |
|---|---|
| 8.1 | Passing null to non-nullable parameters of internal functions (strlen(null)); implicit lossy float-to-int conversions; return types added to internal methods (#[\ReturnTypeWillChange] silences notices when extending them) |
| 8.2 | Dynamic properties (see PHP Properties); ${var} string interpolation; utf8_encode()/utf8_decode() |
| 8.3 | Mostly additions (typed class constants, #[\Override], json_validate()); few incompatibilities |
| 8.4 | Implicitly nullable parameters (string $x = null, see Nullable Types); E_STRICT constant; trigger_error() with E_USER_ERROR; new defaults such as bcrypt cost 12 |
| 8.5 | Non-canonical casts (integer), (double), (boolean), (binary); curl_close() and other no-op functions |
Examples of Deprecation Notices
<?php
echo strlen(null); // PHP 8.1+
class Product {}
$product = new Product();
$product->colour = 'blue'; // PHP 8.2+
Deprecated: strlen(): Passing null to parameter #1 ($string) of type string is deprecated in ... on line 2
Deprecated: Creation of dynamic property Product::$colour is deprecated in ... on line 6
A Step-by-Step Migration Plan
1. Take Inventory
- Which PHP version runs today, and which is the target?
- Which extensions does the application use? Are they available for the target version (see Installing Extensions (PECL / PIE))?
- Which Composer packages block the upgrade?
composer why-not php 8.4
This lists installed packages whose requirements do not allow PHP 8.4, showing what must be updated first.
2. Improve the Safety Net
- Run the existing test suite; add tests for critical flows (checkout, login, payments) if they are missing.
- Add static analysis (PHPStan or Psalm) configured for the target version.
3. Find Incompatibilities Automatically
# Static check for version-specific problems (PHP_CodeSniffer + PHPCompatibility)
vendor/bin/phpcs --standard=PHPCompatibility --runtime-set testVersion 8.4 src/
# Static analysis targeting the new version (phpstan.neon: parameters: phpVersion: 80400)
vendor/bin/phpstan analyse src
4. Upgrade Code Automatically With Rector
Rector rewrites code automatically: it fixes many incompatibilities and can modernize syntax (constructor promotion, match, typed properties, explicit nullable types).
<?php
// rector.php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
->withPhpSets(php84: true);
composer require --dev rector/rector
vendor/bin/rector process --dry-run # preview the changes
vendor/bin/rector process # apply them
Review Rector's changes like any other code change, in small commits.
5. Update Dependencies
Upgrade frameworks and libraries to versions supporting the target PHP version — often the largest part of the work for older applications. Upgrade one major framework version at a time, following each framework's upgrade guide.
6. Run the Application With Deprecations Visible
In development and CI, run with error_reporting = E_ALL and log deprecations. In production, log them too (without displaying them) after the upgrade, to catch code paths the tests missed.
7. Test in CI Across Versions
# Excerpt of a CI matrix (GitHub Actions)
strategy:
matrix:
php: ['8.3', '8.4', '8.5']
Running tests on both the current and target versions allows the code to stay compatible with both during the transition.
8. Roll Out Gradually
Deploy to a staging environment that mirrors production, then to production — ideally to a subset of servers first — while monitoring error logs, performance, and business metrics. Keep the old version available for a quick rollback.
Common Mistakes
Upgrading Several Major Versions at Once Without Tests
Effect: Many failures at once, hard to diagnose.
Fix: Build a test safety net first; upgrade step by step if the gap is large.
Ignoring Deprecation Notices
Effect: The next upgrade breaks the application.
Fix: Log and fix deprecations continuously.
Forgetting Extensions and Server Configuration
Effect: The code is ready, but php.ini settings or extensions differ on the new version.
Fix: Recreate the configuration for the new version and verify with php -m and a configuration check.
Upgrading PHP but Not Dependencies
Effect: Old libraries fail or emit many deprecations.
Fix: Update Composer packages as part of the migration.
Best Practices
- Stay on supported PHP versions and plan upgrades before end-of-life dates.
- Read the official migration guides for every version between the current and target versions.
- Use tests, static analysis, PHPCompatibility, and Rector to find and fix problems automatically.
- Treat deprecations as tasks, and run CI on both current and target versions.
- Roll out gradually with monitoring and a rollback plan.
Official reference: https://www.php.net/manual/en/appendices.php