David NĂ©grier CTO

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?

Your code did not actually change! It's only a matter of adding a few "use function" statements at the top of your file.

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:

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:

  1. 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
  2. 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:

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.