PHP 7 to 8: where it breaks and how to know in advance
Most PHP upgrade failures do not come from the framework. They come from custom code and old dependencies.
The jump from PHP 7 to 8 is not a single update but several. Between PHP 7.4 and 8.3 lie four minor versions, each with changes to types, error behavior and extensions. Most applications do not break at startup. They break in places that worked quietly for years.
This article covers the typical breakage points and how to find them before the update.
Types get stricter
PHP 8 checks types more closely, including for internal functions. Passing
null to a parameter that does not allow null produces a deprecation warning
from PHP 8.1 onward. Examples include strpos(), strlen(), trim() and
htmlspecialchars() called with null as an argument. In PHP 7, null was
silently converted to an empty string.
Internal functions also throw TypeError more often instead of returning null
or false. Code that checks the return value falls through. The same change
affects union types and return types in your own signatures. What PHP 7
tolerated is rejected at runtime.
A related issue concerns default values. Signatures such as
function f(string $x = null) were common for years. If you move directly to
PHP 8.4 or later, they are marked as deprecated. Check those places when you
touch the signatures anyway.
Warnings that become errors
A missing array key produced a notice in PHP 7. In PHP 8.0 it became a warning:
"Undefined array key". Reading a property from a null value produces a similar
warning.
A warning on its own is not fatal. Many frameworks and error handlers convert warnings into exceptions, such as the Symfony ErrorHandler in debug mode. A warning that was ignored for years then stops a request. In production the visible effect is often log noise that buries real failures.
Review how your application treats E_WARNING and E_DEPRECATED. If an error
handler turns them into exceptions, that behavior belongs before the update, not
after.
Strings and numbers
PHP 8 changed comparisons between strings and numbers. 0 == "foo" returned
true in PHP 7, because the non-numeric string was converted to 0. In PHP 8
the number is converted to a string instead, so the result is false. 0 == ""
is no longer true either. Comparing "0" == "foo" was already false in PHP 7
and stays false, because two strings are compared as strings in both versions.
Comparisons like these are common in grown code: status checks, discount logic,
voucher codes, filters from forms. Look for loose comparisons with == in paths
where money or permissions are involved, and replace them with ===. No special
tool is required to start. A targeted search is enough.
PHP 8 also provides str_contains(), str_starts_with() and str_ends_with().
They replace many strpos() comparisons and are easier to read. When reworking
critical paths, both changes can be combined.
Errors and exceptions
PHP 8 throws ValueError and TypeError more often for invalid arguments. A
negative value in str_repeat(), an invalid mode in fopen() or a malformed
DateTime call now raises an exception instead of failing silently.
Inheritance matters here. TypeError and ValueError extend Error, not
Exception. A catch (Exception $e) block does not catch them. Check whether
your central error handling catches Throwable. Otherwise an invalid input
becomes a blank page.
Removed functions and extensions
PHP 8.0 removed functions that still appear in older code: each(),
create_function(), money_format(), get_magic_quotes_gpc() and the
FILTER_SANITIZE_MAGIC_QUOTES constant. The XML-RPC extension moved out of the
core into PECL.
Older jumps still matter. mcrypt disappeared in PHP 7.2 and the mysql_*
functions in PHP 7.0. If your application started on PHP 7.0 and those calls
were never replaced, it breaks before PHP 8 is even involved.
More deprecations arrive with each minor version: strftime() and the mhash
family from 8.1, utf8_encode() and utf8_decode() from 8.2, dynamic
properties from 8.2. A search through the code, combined with the migration
notes of the target version, produces the list of hits.
Dependencies set the lower bound
Before discussing code, settle the versions. Composer packages and frameworks determine which PHP version is possible at all.
Laravel requires PHP 8.0 in version 9, PHP 8.1 in version 10 and PHP 8.2 in
version 11. Symfony 6.4 requires PHP 8.1. A package that needs ext-mcrypt or
ext-xmlrpc blocks the update regardless of your own code.
Useful commands are composer check-platform-reqs against the target
environment, composer why-not php 8.2 for blocking packages and
composer audit for known vulnerabilities. With config.platform.php in
composer.json you can test dependency resolution against the target version
before installing it. Also check the CI configuration: a pipeline that still
installs PHP 7.4 does not test the target version.
Finding risk in advance
Static analysis is the cheapest way to find breakage. PHPStan and Psalm check
code against a configured target version. In phpstan.neon, set phpVersion to
the target and run the analysis over the critical directories. A baseline
records the initial findings so new problems stay visible.
Rector can also show which automatic changes an upgrade set would make. A run
with --dry-run produces a diff without touching files. It is no substitute for
tests, but a useful overview.
If a test suite exists, run it on the old version before the update. That is the only way to know whether a failure after the update comes from the code or from the environment.
Test on a staging environment with production-like data. Same PHP version, same extensions, same configuration. An anonymized database dump surfaces cases that synthetic test data never creates.
Define an error budget for the first days after the update. For example: no new fatal errors and no more warnings than before.
Invoice rounding, tax rates, discounts, payment callbacks and refunds are the places where a type error causes real damage. Those paths need tests before the version changes.
Conclusion
The jump to PHP 8 rarely breaks the home page. It breaks special cases nobody remembers anymore. Checking the target version, the dependencies and the critical paths beforehand finds most problems while they are still cheap to fix.
The right sequence for a grown application is described in PHP modernization. Why postponement makes things worse is covered in signs of technical debt. And if a rewrite is already on the table, the comparison modernize or rewrite helps.
