PHP 8.1+ log flood: Passing null to parameter of type string is deprecated
After moving a PHP 7.4 app to PHP 8.3, our logs grew to 9 GB a day of null-to-string deprecation notices and filled a disk. We found the real call sites, fixed them by intent instead of blind casts, and blocked new ones in CI.
Five days after we moved our booking platform from PHP 7.4 to PHP 8.3, the on-call engineer was
paged at 03:10 for a full root volume on one of the web nodes. The application was still serving
traffic, but log shipping had stalled and /var/log held 31 GB, almost all of it PHP-FPM output.
The lines all looked like variations of this:
[16-Dec-2024 02:58:41 UTC] PHP Deprecated: strlen(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/app/src/Search/QueryParser.php on line 88
[16-Dec-2024 02:58:41 UTC] PHP Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/app/templates/partials/guest.php on line 14
[16-Dec-2024 02:58:41 UTC] PHP Deprecated: str_replace(): Passing null to parameter #3 ($subject) of type array|string is deprecated in /var/www/app/src/Util/Slug.php on line 22
Each node wrote roughly 9 GB of these per day. Our staging environment had display_errors off and
error_reporting set to exclude deprecations, which is why the upgrade had looked clean there.
Reproducing it
Any request that touched a nullable value reproduced it. The simplest was a search with no query string:
curl -s -o /dev/null "https://staging.example.com/search"
tail -n 1 /var/log/php8.3-fpm/error.log
$_GET['q'] ?? null produced null, which went straight into strlen() and trim().
What we ruled out
- A broken dependency. We first assumed a vendor package was not PHP 8 ready. Grouping by file
showed 97% of the lines came from our own
src/andtemplates/directories. - A real functional bug.
strlen(null)still returns0andhtmlspecialchars(null)still returns""in PHP 8.x. The deprecation is a warning about future behaviour, not a change in today's result, so no page rendered differently. - Just raising the log level. Hiding
E_DEPRECATEDwould have stopped the disk filling, but the RFC that introduced this deprecation states that these calls become aTypeErrorin the next major version. Hiding them would have turned a log problem into a future outage.
Root cause
PHP 8.1 deprecated passing null to non-nullable scalar parameters of internal functions. In PHP 7.x,
internal functions silently coerced null to "", 0 or false. Our code relied on that
everywhere: nullable database columns rendered in templates, optional query parameters, and helper
functions that returned null for "not found". The behaviour still worked, but every call now
emitted an E_DEPRECATED notice, and our production error_reporting was E_ALL, with
log_errors = On and no rate limiting.
What is happening under the hood
User-defined functions have always been strict about null: if you declare function f(string $s)
and pass null, PHP throws a TypeError even in coercive typing mode. Internal functions, written
in C, historically accepted null for scalar parameters and coerced it. PHP 8.1 aligned the two
behaviours in steps. For now, internal functions keep coercing but emit a deprecation; the plan is
to throw TypeError like user functions do.
Parameters that are explicitly nullable in the signature (for example the $length parameter of
substr()) are unaffected. The message tells you exactly which parameter is involved, its position,
name and declared type, which is what makes the fix mechanical once you know where to look.
Environment
| Component | Version / value |
|---|---|
| OS | Debian 12 (bookworm) |
| PHP | 8.3.14 with PHP-FPM, upgraded from 7.4.33 |
| Framework | Slim 4.14 with plain PHP templates |
| Logging | FPM catch_workers_output = yes, error_log to file |
error_reporting (prod) |
E_ALL |
error_reporting (staging) |
E_ALL & ~E_DEPRECATED & ~E_STRICT |
Minimal reproduction
<?php
// null_to_internal.php
$query = $_GET['q'] ?? null; // null when the parameter is missing
if (strlen($query) > 2) { // Deprecated in 8.1+, TypeError planned for 9.0
echo 'searching';
}
function shout(string $s): string { return strtoupper($s); }
echo shout(null); // TypeError already, in every PHP 8 version
php -d error_reporting=E_ALL null_to_internal.php
Who is affected
Any codebase upgraded from PHP 7.x or 8.0 to 8.1, 8.2, 8.3 or 8.4 that passes nullable values to
string functions (strlen, trim, str_replace, htmlspecialchars, explode, strpos),
numeric functions or date functions. Typical sources are optional request input, nullable database
columns, and functions that return null on failure such as preg_replace() on a regex error.
The fix
First we stopped the bleeding: we added a log rotation policy capped by size and temporarily routed deprecations to a separate sampled channel instead of the main error log. Then we found the real call sites by collapsing 14 million lines into unique locations:
grep -h 'Passing null to parameter' /var/log/php8.3-fpm/error.log* \
| sed -E 's/^\[[^]]+\] //' \
| awk -F' in ' '{print $NF}' \
| sort | uniq -c | sort -rn | head -n 40
4108823 /var/www/app/src/Search/QueryParser.php on line 88
2911402 /var/www/app/templates/partials/guest.php on line 14
1730355 /var/www/app/src/Util/Slug.php on line 22
...
Only 63 unique locations produced all 14 million lines, and the top ten produced 91% of them. We
fixed each one according to what null meant there, not with a reflexive cast:
// Optional input: null means "no query", which is a real state.
-$query = $_GET['q'] ?? null;
-if (strlen($query) > 2) {
+$query = trim((string) ($_GET['q'] ?? ''));
+if ($query !== '' && strlen($query) > 2) {
// Nullable column: render nothing for a missing middle name.
-<?= htmlspecialchars($guest['middle_name']) ?>
+<?= htmlspecialchars($guest['middle_name'] ?? '') ?>
// Helper that should never receive null: make the contract explicit.
-function slugify($title)
+function slugify(string $title): string
{
return strtolower(trim(str_replace(' ', '-', $title)));
}
The last one is the most valuable kind of change. Adding the string type moved the failure to the
caller, where we found a controller passing $event->subtitle that was null for 40% of events.
That was a real bug: those events were getting an empty slug segment.
Alternatives and trade-offs
-
Rector's
NullToStrictStringFuncCallArgRector. It is part of thephp81set and rewrites calls such asstrlen($x)tostrlen((string) $x)when it cannot prove$xis a string. It is fast and safe in the sense that behaviour does not change, but it also hides places wherenullindicates a bug, like the slug case above. We ran it only on thetemplates/directory, where empty-string-for-null is almost always the intent.// rector.php use Rector\Config\RectorConfig; use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector; return RectorConfig::configure() ->withPaths([__DIR__ . '/templates']) ->withRules([NullToStrictStringFuncCallArgRector::class]); -
Suppressing
E_DEPRECATEDin production. Reasonable as a short-term disk-saving measure, provided deprecations are still collected somewhere, such as CI or a sampled log. Do not make it permanent. -
A global error handler that converts deprecations to exceptions. Great in tests, too aggressive in production on day one.
How to test the fix
In the test suite we turned deprecations into failures. With PHPUnit 10 or 11 this is a configuration switch:
<!-- phpunit.xml (excerpt) -->
<phpunit failOnDeprecation="true" displayDetailsOnTestsThatTriggerDeprecations="true">
<php>
<ini name="error_reporting" value="-1"/>
</php>
</phpunit>
vendor/bin/phpunit --testsuite feature
We also wrote a smoke test that hits the 30 busiest routes with and without their optional parameters and asserts the error log did not grow.
Confirming in production
After two deploys over three days, deprecation lines per node dropped from about 9 GB per day to
under 2 MB, almost entirely from a single vendor package that we reported upstream. The log volume
alert we added at 1 GB per day per node has not fired since. Disk usage on /var stayed flat at 18%.
Regression guard
- PHPStan at level 8 reports nullable values passed to non-nullable parameters (output below). We added it to CI with a baseline for the remaining legacy code, so new violations fail the build while old ones are burned down.
failOnDeprecationin PHPUnit keeps new deprecations out of tested code paths.- Staging now runs with exactly the same
error_reportingas production.
vendor/bin/phpstan analyse --level=8 src templates
------ -------------------------------------------------------------------------------
Line src/Search/QueryParser.php
------ -------------------------------------------------------------------------------
88 Parameter #1 $string of function strlen expects string, string|null given.
------ -------------------------------------------------------------------------------
Security implications
The deprecations themselves are harmless, but the incident had two security angles. A full disk
can stop audit logging and session writes, which is a denial-of-service risk; size-based rotation
and a volume alert now cover that. Second, the htmlspecialchars() call sites are output escaping.
When fixing them we checked that every one still escapes, because a careless "fix" that drops the
call to avoid the warning would open an XSS hole.
Prevention checklist
- Run staging with the same
error_reportingand logging settings as production. - Aggregate deprecations by file and line before fixing; the long tail is usually short.
- Decide what
nullmeans at each call site: missing input, empty value or bug. - Add scalar type declarations to your own functions so null fails loudly at the caller.
- Keep PHPStan level 8 and PHPUnit
failOnDeprecationin CI before the next major PHP version.
References
- PHP RFC: Deprecate passing null to non-nullable arguments of internal functions — https://wiki.php.net/rfc/deprecate_null_to_scalar_internal_arg
- PHP 8.1 migration guide, Deprecated features — https://www.php.net/manual/en/migration81.deprecated.php
- Rector rule NullToStrictStringFuncCallArgRector — https://getrector.com/rule-detail/null-to-strict-string-func-call-arg-rector
- PHPStan rule levels — https://phpstan.org/user-guide/rule-levels
Add a label
Embed this post
<iframe src="https://hibug.me/php-81-passing-null-to-string-deprecated" width="700" height="420" frameborder="0" loading="lazy"></iframe>
I have spent most of my career on the side of software that pages people at night: payment integrations, web platforms with real traffic, CI/CD pipelines, Kubernetes clusters and the databases underneath them. Somewhere along the way I started keeping a notebook of every bug that cost more than an hour — the exact error, what we ruled out, the real root cause and how we proved the fix. Hibug is where those notes live. Every post follows the same path: Investigate, Explain, Fix, Verify — with the environment spelled out, the fix tested, and the security angle checked.
Related Stories
Laravel queue workers grow in memory for hours, slow down, then get OOM-killed
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.