PHP Exception Handling: PHP Exception Class Hierarchy

PHP Exception Handling – AssertionError

Today we’ll continue with our exploratory PHP Exception Handling series by diving deeper into the PHP AssertionError. As you can probably guess, an AssertionError is (sometimes) thrown when an assert() call fails. However, a number of default settings and configuration differences based on PHP versions means that AssertionErrors may not always behave as you expect them to.

Let’s jump right in by first looking where the AssertionError fits in the overall PHP Exception Hierarchy, then we’ll take a look at some functional code samples to help illustrate how different PHP versions and configurations may alter your own experiences. Let’s get started!

The Technical Rundown

  • All PHP errors implement the Throwable interface, or are extended from another inherited class therein.
  • Error implements the Throwable interface.
  • AssertionError extends the Error class.

When Should You Use It?

Let’s begin by jumping right into some code to test out some assertions. Normally we’d begin with the entire code sample at once so it’s easier to copy for your own experimentation, but in this case we’ll need to check (and change) a number of PHP configuration settings throughout our tests, so we’ll be using the same functions multiple times for that.

For those unaware, the assert() function accepts up to two arguments, but the single required argument should be a testable condition, either in the form of a string or a boolean condition, that will be tested by the PHP engine. If the assertion succeeds, nothing happens. However, if the condition fails PHP will perform one of a handful of (possible) actions, depending on settings, PHP version, and the like. For our purposes, our goal is to have a failed assert() call throw an AssertionError at us.

Additionally, it’s worth noting that this example code is running on a default PHP 7 installation. We’re also using a simple Logging class to help with output, which can be found below:

Let’s start with a basic function called assertEquality(), which asserts that both parameters are equal to each other:

We’ll test this out by calling it twice with two sets of arguments:

We’d expect that the first call would succeed, while the second will fail since 1 != 2, but our log output shows both were successful:

What gives? As it turns out, PHP 7 comes with a default php.ini configuration setting for the zend.assertions value of -1. As you can see from the description taken from the php.ini file itself, this means that assert() function calls are not even compiled:

We can resolve this by setting zend.assertions = 1. In addition, there’s also the setting that defaults to On, so we typically don’t need to change it, but it’s worth double-checking if your own installation is misbehaving.

Now, after running our two assertion tests again we get much different output for the second (failing) assertion call:

We’re starting to get somewhere now that our assert(1 == 2) test failed, but the PHP engine issued a Warning instead of an Error that we expected. This means that our try-catch block wasn’t able to catch the AssertionError that we were after.

This failure to throw an AssertionError is due to another php.ini setting of assert.exception. When this is set to On (or 1), it forces the PHP engine to throw an AssertionError when an assertion fails, rather than the default of issuing a warning only. Again, the official documentation description further elaborates:

  • 1: throw when the assertion fails, either by throwing the object provided as the exception or by throwing a new AssertionError object if exception wasn’t provided
  • 0: use or generate a Throwable as described above, but only generate a warning based on that object rather than throwing it (compatible with PHP 5 behaviour)

So we see that, as of PHP 7, the default behavior is to generate a Throwable if one is passed (as the second parameter), but not to actually throw it (unless assert.exception is enabled).

In our case, after setting assert.exception = On and rerunning our tests, we get the following output:

Now we’re starting to get somewhere, as we were able to throw and catch the produced AssertionError. Yet assert() has a few more options that can be used to modify its behavior; namely the assert_options() function:

Here we’ve configure a few of the assert() behavioral options, most of which are available in the php.ini as well, setting them to their default values. However, we’re also setting ASSERT_BAIL to true, which will attempt to terminate the executing process if a failed assertion is encountered. Thus, if we reorder our assertEquality() function tests so our failed test comes first (as seen above), our output dramatically changes once again:

That’s quite a mess, but what’s important to note is that this produces an uncaught error. This is because ASSERT_BAIL = true forces the process to immediately terminate the moment the failed assertion is encountered, rather than completing execution. Thus, surrounding catch blocks and the like are never processed.

The last setting we’ll mess with is ASSERT_CALLBACK, which allows us to specify a callback function that will receive a handful of arguments when an assertion fails, which we can use for additional, custom processing. For our purposes we have the onAssertionFailure() function, which just accepts the handful of arguments that are passed and outputs a readable failure message:

Executing our failed assertEquality(1, 2) function one last time with an ASSERT_CALLBACK function specified produces our new, modified output:

We still generate and throw an AssertionError, but the callback function is a convenient way to handle such failures, regardless of any try/catch or assertion exception settings.

Check out the Airbrake-PHP library, designed to quickly and easily integrate into any PHP project, giving you and your team access to real-time error monitoring and reporting throughout your application’s entire life cycle. With automatic, instantaneous error and exception notifications at your fingertips, you’ll be constantly aware of your application’s health, including any issues that may arise. Best of all, with Airbrake’s robust web dashboard cataloging every error that occurs, you and your team can immediately dive into the exact details of what went wrong, making it easy to quickly recognize and resolve problems.