PHP 8.0: Internal function warnings now throw TypeError and ValueError exceptions


In PHP 8, internal function parameters have types and value validations enforced, and will throw \TypeError or \ValueError exceptions if the expected type or value is not allowed.

Prior to PHP 8, this resulted in a PHP warning.

Not all PHP warnings emitted by internal functions are transformed to exceptions, but majority of the functions will throw \TypeError or \ValueError exceptions if the provided type is not allowed, or the provided value is invalid. This includes functions that accept multiple types (such as a string or an array) because PHP 8 comes with Union Types.

Out of all PHP 8 changes, this will likely be the biggest pain-point when you upgrade existing code.

Reasons behind this decision

Many of the PHP internal functions gracefully handle unexpected values by raising a PHP warning, but still returning a "false-ish" value such as null, false, or 0. This can lead to subtle bugs that are later discovered, if discovered at all, in different parts of the program.

For example, json_decode() function accepts a $depth parameter that must be a positive integer. This was not enforced with an exception prior to PHP 8. If you call json_decode() with an invalid $depth, json_decode() function will raise a warning, but still return null, which is an acceptable return type of the original json-encoded value is also null.

With types and values enforced, json_decode() function throws an exception when it encounters an unexpected type or a value.

This can result in applications that dismissed the warning prior to PHP 8 to fail due to the unexpected exception. However, this results in fewer bugs once fixed because PHP makes sure to defend aggressively against invalid values.

\TypeError Examples

Warning to Exception

Most of the PHP internal functions that accept a typed parameter now throw \TypeError exceptions instead of warnings. This can eliminate a lot of subtle bugs because most of these string functions return either false or null on such unexpected types, which can result in a bug somewhere else.

substr('foo', []);

PHP versions prior to 8 will raise a warning and return null instead of throwing a \TypeError and refusing to go forward. The type is now enforced and throws exceptions in PHP 8.

- Warning: substr() expects parameter 2 to be int, array given in ... on line ...
+ Fatal error: Uncaught TypeError: substr(): Argument #2 ($start) must be of type int, array given in ...:...

New \TypeError without prior warnings

Some functions, such as method_exists() did not throw exceptions on unexpected values, but returned a value that fulfills the semantic return values of the function.

Union types are used when they are deemed necessary. For example, method_exists() function accepts either a class name (string) or an object (object). This is enforced as a Union Type of string|object.

method_exists([], 'getName');

This will now throw a \TypeError. Prior to PHP 8, it returned false if the provided parameter is not a string or an object, but did not raise any warnings.

+ Fatal error: Uncaught TypeError: method_exists(): Argument #1 ($object_or_class) must be of type object|string, array given in ...:...

\ValueError examples

PHP throws \ValueError exceptions if the provided value is of correct type, but not acceptable in the context.

json_decode('"foo"', true, -1);

Prior to PHP 8, setting a depth less than 0 resulted in a warning, which is now promoted to a \ValueError exception:

- Warning: json_decode(): Depth must be greater than zero in ... on line ...
+ Fatal error: Uncaught ValueError: json_decode(): Argument #3 ($depth) must be greater than 0 in ...:...

Many SPL functions, mbstring_ functions, password_* functions, etc now throw \ValueError exceptions when the provided values cannot be used to go further.

Backwards compatibility impact

Except for certain cases such as method_exists, the \TypeError and \ValueError exceptions PHP 8 and later throws resulted in a warning prior to PHP 8.

Unless you went out of your way to silent (e.g @strlen([])) the warning, or dismissed in from the error log, you should not encounter any major problems.

Except for the few exceptional cases, all the changes you make because of the new \TypeError and \ValueError exceptions will work all the same in prior PHP versions as well.

The new \ValueError exception class can be polyfilled, but that does not mean internal functions will throw exceptions instead of raising warnings.

class ValueError extends Error {}

It is possible to have a try/catch block that accepts \ValueError exceptions without having the \ValueError exception class declared. It will not be thrown in prior versions, which makes the try/catch block a harmless no-op in older PHP versions.

try {}
catch (\ValueError $exception) {}

Implementations (multiple pull-requests)