To Boolean
: “When converting to bool, the following values are considered false [...] the special type NULL”
Or, NULL “can be coerced to the type requested by the hint without data loss and without creation of likely unintended data” (
ref
).
PHP 7.0 introduced the ability for user-defined functions to specify parameter types via the
Scalar Type Declarations RFC
, where the implementation triggered Type Errors for those using
strict_types=1
, and otherwise used coercion for string/int/float/bool, but not NULL, PHP 8.1 updated internal function parameters to work in the same way.
print
(
1.2
)
;
print
(
false
)
;
print
(
NULL
)
;
// Fine, still coerced to empty string.
echo
NULL
;
// Fine, still coerced to empty string.
var_dump
(
3
+
'5'
+
NULL
)
;
// Fine, int(8)
var_dump
(
NULL
/
6
)
;
// Fine, int(0)
$o
=
[
]
;
$o
[
]
=
(
''
==
''
)
;
$o
[
]
=
(
''
==
NULL
)
;
// Fine, still coerced to empty string.
$o
[
]
=
'ConCat '
.
'A'
;
$o
[
]
=
'ConCat '
.
123
;
$o
[
]
=
'ConCat '
.
1.2
;
$o
[
]
=
'ConCat '
.
false
;
$o
[
]
=
'ConCat '
.
NULL
;
// Fine, still coerced to empty string.
$o
[
]
=
sprintf
(
'%s'
,
'A'
)
;
$o
[
]
=
sprintf
(
'%s'
,
1
)
;
$o
[
]
=
sprintf
(
'%s'
,
1.2
)
;
$o
[
]
=
sprintf
(
'%s'
,
false
)
;
$o
[
]
=
sprintf
(
'%s'
,
NULL
)
;
// Fine, still coerced to empty string.
$o
[
]
=
htmlspecialchars
(
'A'
)
;
$o
[
]
=
htmlspecialchars
(
1
)
;
$o
[
]
=
htmlspecialchars
(
1.2
)
;
$o
[
]
=
htmlspecialchars
(
false
)
;
$o
[
]
=
htmlspecialchars
(
NULL
)
;
// Deprecated in 8.1, Fatal Error in 9.0?
With user-defined functions, this inconsistency is also noted:
function user_function(string $s, int $i, float $f, bool $b) {
var_dump($s, $i, $f, $b);
user_function('1', '1', '1', '1');
// string(1) "1" / int(1) / float(1) / bool(true)
user_function(2, 2, 2, 2);
// string(1) "2" / int(2) / float(2) / bool(true)
user_function(3.3, 3.3, 3.3, 3.3);
// string(3) "3.3" / int(3), lost precision / float(3.3) / bool(true)
user_function(false, false, false, false);
// string(0) "" / int(0) / float(0) / bool(false)
user_function(NULL, NULL, NULL, NULL);
// Fatal error, Uncaught TypeError x4!
Scalar Types
The
Scalar Type Declarations
RFC
says “it should be possible for existing userland libraries to add scalar type declarations without breaking compatibility”, but this is not the case, because of NULL. This has made adoption of type declarations harder, as it does not work like the following:
function my_function($s, $i, $f, $b) {
$s = strval($s);
$i = intval($i);
$f = floatval($f);
$b = boolval($b);
var_dump($s, $i, $f, $b);
function my_function(string $s, int $i, float $f, bool $b) {
var_dump($s, $i, $f, $b);
my_function(NULL, NULL, NULL, NULL);
George Peter Banyard
notes that “Userland scalar types [...] did not include coercion from NULL for
very
good reasons”.
The
Scalar Type Declarations
RFC
says “The only exception to this is the handling of NULL: in order to be consistent with our existing type declarations for classes, callables and arrays, NULL is not accepted by default, unless it is a parameter and is explicitly given a default value of NULL”, which goes against the documentation (as noted above) where the coercion from NULL is well defined (i.e. NULL is more like a string/int/float/bool, rather than an object/callable/array).
Common sources of NULL:
$search = (isset($_GET['q']) ? $_GET['q'] : NULL);
$search = ($_GET['q'] ?? NULL); // 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 = array_pop($empty_array);
$value = mysqli_fetch_row($result);
Examples functions, often working with user input, where NULL has always been accepted/coerced:
$rounded_value = round($value);
$search_trimmed = trim($search);
$search_len = strlen($search);
$search_upper = strtoupper($search);
$search_hash = hash('sha256', $search);
echo htmlspecialchars($search);
echo 'https://example.com/?q=' . urlencode($search);
preg_match('/^[a-z]/', $search);
exec('/path/to/cmd ' . escapeshellarg($search));
socket_write($socket, $search);
xmlwriter_text($writer, $search);
And developers have used NULL to skip certain parameters, e.g.
setcookie('q', $search, NULL, NULL, NULL, true, true); // x4
substr($string, NULL, 3);
mail('[email protected]', 'subject', 'message', NULL, '[email protected]');
HTML
Templating engines like
Laravel Blade
suppress this deprecation with null-coalescing (
patch
); or
Symphony Twig
which preserves NULL, but it's often passed to
echo
(which accepts it, despite the
echo documentation
saying it accepts non-nullable strings).
I'd argue strict type checking (that prevents all forms of coercion) should be done via Static Analysis or via
strict_types=1
opt-in, like how a string (e.g. '15') being provided to integer parameter could identify a problem in some development environments.
There are approximately
335 parameters affected by this deprecation
.
As an aside, there are also roughly
104 questionable
and
558 problematic
parameters which probably shouldn't accept NULL
or
an Empty String. For these parameters, a different
RFC
could consider updating them to reject both NULL and Empty Strings, e.g.
$needle
in
strpos()
, and
$characters
in
trim()
; in the same way that
$separator
in
explode()
already has a “cannot be empty” Fatal Error.
This is considered fine by these tools:
composer require --dev "squizlabs/php_codesniffer=*"
./vendor/bin/phpcs -p ./src/
E 1 / 1 (100%)
[...]
2 | ERROR | Missing file doc comment
[...]
composer require friendsofphp/php-cs-fixer
./vendor/bin/php-cs-fixer fix src --diff --allow-risky=yes
Loaded config default.
Using cache file ".php-cs-fixer.cache".
1) src/index.php
---------- begin diff ----------
--- src/index.php
+++ src/index.php
@@ -1,4 +1,4 @@
$nullable = ($_GET['a'] ?? null);
echo htmlentities($nullable);
\ No newline at end of file
----------- end diff -----------
Fixed all files in 0.012 seconds, 12.000 MB memory used
composer require --dev phpcompatibility/php-compatibility
sed -i '' -E 's/(PHPCSHelper::getConfigData)/(string) \1/g' vendor/phpcompatibility/php-compatibility/PHPCompatibility/Sniff.php
./vendor/bin/phpcs --config-set installed_paths vendor/phpcompatibility/php-compatibility
./vendor/bin/phpcs -p ./src/ --standard=PHPCompatibility --runtime-set testVersion 8.1
. 1 / 1 (100%)
Note: Juliette (@jrfnl) has confirmed that getting PHPCompatibility to solve this problem will be “pretty darn hard to do” because it's “not reliably sniffable” (
source
).
composer require --dev phpstan/phpstan
./vendor/bin/phpstan analyse -l 9 ./src/
[OK] No errors
composer require --dev phpstan/phpstan-strict-rules
composer require --dev phpstan/extension-installer
./vendor/bin/phpstan analyse -l 9 ./src/
[OK] No errors
Note: There are
Stricter Analysis
options for PHPStan, but they don't seem to help with this problem.
composer require --dev vimeo/psalm
./vendor/bin/psalm --init ./src/ 4
./vendor/bin/psalm
No errors found!
Note: Psalm can detect this at
levels 1, 2, and 3
(don't use a baseline).
Since
21st June 2022
, Rector can modify 362 function arguments via
NullToStrictStringFuncCallArgRector
:
mkdir -p rector/src;
cd rector/;
composer require --dev rector/rector;
echo '<?= htmlspecialchars($var) ?>' > src/index.php;
echo '<?php
use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector;
use Rector\Config\RectorConfig;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([__DIR__ . "/src"]);
$rectorConfig->rule(NullToStrictStringFuncCallArgRector::class);
' > rector.php;
./vendor/bin/rector process;
This will litter the code with the use of
(string)
type casting, e.g.
-<?= htmlspecialchars($var) ?>
+<?= htmlspecialchars((string) $var) ?>
For a typical project (which won't be using
strict_types=1
), expect thousands of changes to be made; and note how this does not improve code quality.
Alternatively you can use
set_error_handler()
, with something like:
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 some developers are simply
patching php-src
(risky).
While making each change is fairly easy - they are still difficult to find, there are many of them, and the updates used are often pointless, e.g.