Passing null to non-nullable internal function parameters – Updating Existing Code Base to php 8.1

To answer the bit about the “easiest most time efficient way to handle upgrading this code”.

In short, you can’t.


First, some background…

Roughly 15% of developers use strict_types=1, so you’re in the majority of developers that don’t.

You could ignore this problem (deprecation) for now, but PHP 9.0 will cause a lot of problems by making this a Fatal Type Error.

That said, you can still concatenate a string with NULL:

$name = NULL;
$a="Hi " . $name;

And you can still compare NULL to an empty string:

if ('' == NULL) {
}

And you can still do calculations with NULL (it’s still treated as 0):

var_dump(3 + '5' + NULL); // Fine, int(8)
var_dump(NULL / 6); // Fine, int(0)

And you can still print/echo NULL:

print(NULL);
echo NULL;

And you can still pass NULL into sprintf() and have that coerced to an empty string with %s, e.g.

sprintf('%s', NULL);

And you can still coerce other values (following the rules), e.g.

strlen(15);
htmlspecialchars(1.2);
setcookie('c', false);

NULL coercion has worked like this since, I assume the beginning, and is also documented:

  • To String: “null is always converted to an empty string.”
  • To Integer: “null is always converted to zero (0).”
  • To Float: “For values of other types, the conversion is performed by converting the value to int first and then to float”
  • To Boolean: “When converting to bool, the following values are considered false […] the special type NULL”

Anyway, to fix… the first part it trying to find the code you will need to update.

This happens any time where NULL could be passed to one of these function parameters.

There are at least 335 parameters affected by this.

There is an additional 104 which are a bit questionable; and 558 where NULL is problematic, where you should fix those, e.g. define(NULL, 'value').

Psalm is the only tool I can find that’s able to help with this.

And Psalm needs to be at a very high checking level (1, 2, or 3).

And you cannot use a baseline to ignore problems (a technique used by developers who have introduced static analysis to an existing project, so it only checks new/edited code).

If you haven’t used static analysis tools before (don’t worry, it’s suggested only 33% of developers do); then expect to spend a lot of time modifying your code (start at level 8, the most lenient, and slowly work up).

I could not get PHPStan, Rector, PHP CodeSniffer, PHP CS Fixer, or PHPCompatibility to find these problems (results); and Juliette has confirmed that getting PHPCompatibility to solve this problem will be “pretty darn hard to do” because it’s “not reliably sniffable” (source).


Once you have found every single problem, the second part is editing.

The least likely place to cause problems is by changing the sinks, e.g.

example_function(strval($name));
example_function((string) $name);
example_function($name ?? '');

Or, you could try tracking back to the source of the variable, and try to stop it being set to NULL in the first place.

These are some very common sources of NULL:

$search = (isset($_GET['q']) ? $_GET['q'] : NULL);
 
$search = ($_GET['q'] ?? NULL); // Fairly common (since PHP 7)
 
$search = filter_input(INPUT_GET, 'q');
 
$search = $request->input('q'); // Laravel
$search = $request->get('q'); // Symfony
$search = $this->request->getQuery('q'); // CakePHP
$search = $request->getGet('q'); // CodeIgniter
 
$value = mysqli_fetch_row($result);
$value = json_decode($json); // Invalid JSON, or nesting limit.
$value = array_pop($empty_array);

Some of these functions take a second parameter to specify what the default should be, or you could use strval() earlier… but be careful, your code may specifically check for NULL via ($a === NULL), and you don’t want to break that.

Many developers will not be aware that some of their variables can contain NULL – e.g. expecting a <form> (they created) to always submit all of the input fields; that might not happen due to network issues, browser extensions, user editing the DOM/URL in their browser, etc.


I’ve been looking at this problem for the best part of a year.

I started writing up two RFC’s to try and address this problem. The first was to update some of the functions to accept NULL (wasn’t ideal, as it upset the developers who used strict_types); and the second RFC was to allow NULL to continue being coerced in this context… but I didn’t put it to the vote, as I just got a load of negative feedback, and I didn’t want that rejection to be quoted in the future as to why this problem cannot be fixed (while the original change was barely discussed, this one would be).

It seems NULL is treated differently because it was never considered a “scalar value” – I don’t think many developers care about this distinction, but it comes up every now and again.

With the developers I’ve been working with, a most have just ignored this one (hoping it will be resolved later, which is probably not the best idea); e.g.

function ignore_null_coercion($errno, $errstr) {
  // https://github.com/php/php-src/blob/012ef7912a8a0bb7d11b2dc8d108cc859c51e8d7/Zend/zend_API.c#L458
  if ($errno === E_DEPRECATED && preg_match('/Passing null to parameter #.* of type .* is deprecated/', $errstr)) {
    return true;
  }
  return false;
}
set_error_handler('ignore_null_coercion', E_DEPRECATED);

And one team is trying to stick strval() around everything, e.g. trim(strval($search)). But they are still finding problems over a year later (they stated testing with 8.1 alpha 1).

One other option I’m considering is to create a library that re-defines all of these ~335 functions as nullable, under a namespace; e.g.

namespace allow_null_coercion;

function strlen(?string $string): int {
    return \strlen(\strval($string));
}

Then developers would include that library, and use the namespace themselves:

namespace allow_null_coercion;

$search = $request->input('q'); // Could return NULL

// ...

echo strlen($search);

Leave a Comment