For historic reasons, the PHP core functions return false on failure instead of throwing exceptions. Let's see how we can fix this!
TL;DR
We recently wrote a new PHP library. It's called safe. It wraps all PHP core functions that return false on failure into another function that throws an exception.
Project is currently in beta and we need feedback!
Introduction
At TheCodingMachine, we are huge fans of PHPStan. PHPStan is an open-source static analysis tool for your PHP code. It helps you find bugs before your program is run. We are actually so fan of PHPStan that we are happy to sponsor the project.
PHPStan has this notion of "levels" and we strive on each of our projects to reach "level 7" (the maximum level). But PHPStan is constantly improving, and reaching level 7 becomes harder and harder as the tool becomes more strict (this is a good thing!).
Look at the code below.
$content = file_get_contents('foobar.json');
$foobar = json_decode($content);
It seems legit, right?
... right?
With the release of PHPStan 0.10, a new check was added. The code below will issue a warning:
Parameter #1 $json of function json_decode expects string, string|false given.
Whaaaat?
It turns out PHPStan is perfectly right.
The file_get_contents
function can return either a string
(the content of the file) or false
if an error occurred.
And our code does not check if the file_get_contents
succeeded or not. A lot of things could go wrong:
- The file could not exist
- or maybe the user does not have the rights to access the file
- or the disk could be damaged
- ...
So we need to check for errors. The "fixed" code looks like this:
$content = file_get_contents('foobar.json');
if ($content === false) {
throw new FileLoadingException('Could not load file foobar.json');
}
$foobar = json_decode($content);
if ($foobar === null) {
throw new FileLoadingException('foobar.json does not contain valid JSON: '.json_last_error());
}
The code is correct, but it is definitely less easy to read. We lost a lot of expressiveness.
A bit of history
At this point, you may wonder why the core PHP functions (like file_get_contents
or json_decode
) are not throwing
exceptions instead of returning false
or null
on error.
This goes back to the roots of PHP. I could not find a PHP history book, but I believe that most of the core PHP
functions have been added to PHP before exception support was added to the language. So most PHP functions are
using the old PHP error handlers (error messages with E_WARNING
, E_ERROR
...) rather than the (more powerful) exceptions.
It is also interesting to see that the core PHP developers are doing what they can to fix the issue, while retaining
backward compatibility. For instance, in PHP 7.3, the json_decode
function will accept a new JSON_THROW_ON_ERROR option
that will let json_decode
throw an exception instead of returning null
on error.
// In PHP 7.3+
$json = json_decode($jsonString, $assoc = true, $depth = 512, JSON_THROW_ON_ERROR);
The problem is that PHP core developers must maintain backward compatibility, so the "correct" behaviour has to be optional.
If you forget to pass the JSON_THROW_ON_ERROR
, you are back to the old "return null on error" behaviour.
So we cannot too much hope for PHP core functions to radically change their behaviour in the next releases (this is actually a good thing as backward compatibility is important for a mature language like PHP).
So let's have a look at user-land solutions.
The Guzzle team used to handle quite a bit of JSON encoding and decoding.
To ease their pain, they wrote 2 function wrappers for json_encode
and json_decode
.
The code looks like this:
namespace GuzzleHttp;
// ...
function json_decode($json, $assoc = false, $depth = 512, $options = 0)
{
$data = \json_decode($json, $assoc, $depth, $options);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new \InvalidArgumentException(
'json_decode error: ' . json_last_error_msg()
);
}
return $data;
}
Ok, so they are creating a json_decode
function that has exactly the same name and signature as the core json_decode
function,
except it is in a namespace. It calls the core json_decode
function and will throw an exception if an error occurred.
It means that in my code, I simply need to add a single use
statement and I will use the "GuzzleHttp" version:
use function json_decode;
// This is not valid JSON, this will throw an exception.
json_decode('{ "foo": bar" ');
Woot!
We instantly became fan of using this little helper function that makes our life so much simpler. Guzzle developers rock!
But Guzzle does only provide a wrapper for json_decode
and json_encode
. What about the ton of other PHP functions
that still return false
(like mkdir
, fopen
, file_get_contents
, sort
, ...)? So frustrating!
Starting the Safe-PHP project
This is where we decided to start the "Safe-PHP" project.
The idea is simple: wrapping every single PHP core function that returns "false" on failure in a function that throws an exception instead.
With "Safe-PHP", you can now write:
use function Safe\file_get_contents;
use function Safe\json_decode;
// This code is both safe and simple!
$content = file_get_contents('foobar.json');
$foobar = json_decode($content);
If something goes wrong, an exception will be thrown, so there is no risk you will pass "false" values down the pipe. The code will halt at the line where the problem occurred, which makes debugging waaaaaay easier.
And did you notice?
Detecting "unsafe" functions
There is still an issue... The "safe" functions are available, but does this means the developers will think about using them?
Obviously no!
I'm one of the developer of Safe-PHP, and I wouldn't trust myself into thinking of using the Safe variant of a function in each page of a project.
So we need an automated way to let us know that we should use the "safe" variant of a function.
And this is where PHPStan comes into play. Did I told you about PHPStan? Oh! Of course I did! :)
So we wrote a PHPStan extension that parses your code, and will let you know if you are using an unsafe variant. The PHPStan message will look like this:
Function json_encode is unsafe to use. It can return FALSE instead of throwing an exception. Please add 'use function Safe\json_encode;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library.
Using the PHPStan rule, now, I know that I will not forget to use the "safe" variant of any unsafe function out there. This is cool!
How does it work?
If you are wondering about the nitty-gritty details, here is some more information.
There is a huge number of functions in PHP, and a lot of them are returning false on failure. We needed to detect those but were too lazy to read the whole documentation. So we decided to parse the documentation.
We fetched the documentation from the SVN doc repository and we started scanning the files. Hopefully, the documentation is in XML format, and is heavily relying on XML entities.
This is cool because instead of writing "return false for failure" in the XML document, they use the shortcut &return.falseforfailure;
.
And it is (almost) consistent in the whole documentation. It is therefore not too difficult to parse the whole documentation for this string.
The result? After removing all the functions from extensions that are not supported in PHP 7, there are 930 functions remaining that can return false on failure.
930 functions to wrap, that's a lot of work. Rather than doing this manually, we wrote a PHP code generator that parses the XML documents and generates the wrapper out of the documentation. The PHP documentation is crossed with more precise data from PHPStan regarding the possible types of arguments and return types.
The PHPStan data itself comes from another static analyzer called Phan. So that's really open source at its best :)
In the process of writing this code generator, I stumbled upon a number of issues with the PHP documentation and submitted patches that were accepted quickly. So I'm now a proud contributor to the PHP documentation. Yeah!
I also stumbled on a PHPStorm performance issue that was quickly fixed by the PHPStorm folks. Yeah again!
Does it work?
Yes it does! Still, the project is currently in beta, and it needs a lot of testing, so do not hesitate to open issues / pull-requests.
What remains to be done
We plan to add support for PHP core objects (like DateTimeImmutable
...) if the project is successful.
Alternatives
There are a number of alternatives to "safe".
Using high-level packages
First, a number of packages are wrapping the native function calls but also adding some functionality. For instance, if you are doing a lot of filesystem manipulation, I highly recommand using one of:
- symfony/filesystem: a low level library on top of PHP filesystem functions
- PHPLeague's Flysystem: a high level abstraction library
There is also nette/utils that wraps most PHP core functions while slightly changing their signature.
There are tons of high quality PHP packages that are adding a lot of value on top of the row PHP core functions. Use them!
Using strict error handling
PHP comes with a customizable error handler. Most of the functions that return false
on failure will also issue a warning.
You could catch the warning and throw an exception instead with a custom error handler:
set_error_handler(function($severity, $message, $file, $line) {
throw new ErrorException($message, 0, $severity, $file, $line);
});
This solution is great and you should use it by all means.
However, it has 2 drawbacks:
- the behaviour of your code now depends on an error handler that is not directly declared in your code. If you are writing PHP code that is meant to be re-used in different projects, you have no guarantee that a strict error handler
- static analyzers like PHPStan have no knowledge of the global error handler so you will have to configure them in a more relaxed way. The less strict the analyser, the more issues will escape.
That's not to say you should not use this approach. I very much believe you should use it, and use "Safe" in addition.
Feedback welcome
"Safe" development has just started and there are certainly a number of comments to be done and a number of bugs to be found and fixed.
More than anything, we would value some feedback, so do not hesitate:
- to post a comment on Reddit
- to open issues on Github
- to star the project on Github if you find it interesting
Finally, if you are interested into Safe related news, you can follow me on Twitter
About the author
David is CTO and co-founder of TheCodingMachine and WorkAdventure. He is the co-editor of PSR-11, the standard that provides interoperability between dependency injection containers. He is also the lead developper of GraphQLite, a framework-agnostic PHP library to implement a GraphQL API easily.