Debugging And Testing PHP With assert()

8th August 2021 - 16 minutes read time

PHP has a built in debugging and testing tool called assertions, which is essentially the assert() function and a few configuration options.

With this feature you can add additional checks to your application whilst you are developing it so that when you deploy to production you can be sure that things will run correctly. It is simple to run and allows you to embed testing code within your production code without having an adverse effect on performance.

I've seen the use of assert() in a couple of open source PHP projects, but had not really dug into how it works or what it does. As it turns out, assert() is actually very useful and can also be found in other languages like C/C++, Python and JavaScript.

Assertions have been a part of PHP since PHP4, and so it's very well supported. Although they have undergone some changes in recent versions so you really need to be aware of how they have changed. I will detail these changes in the article, but first, let's look at how they work.

Basic Usage

Let's say we were creating a function the took two numbers as arguments and divided them together, returning the result.

The base function might look like this.

function divideNumbers($number1, $number2) {
  return $number1 / $number2;
}

Although we could just use this function (or even unit test it) there is a danger that we might pass a string or a zero as one of the parameters, which might cause an error. One solution would be to add some checks to the function to prevent this being a problem, throwing an exception when we find a problem with any of the parameters.

function divideNumbers($number1, $number2) {
  if (!is_int($number1)) {
    throw new InvalidArgumentException('Parameter $number1 is not a number');
  }

  if (!is_int($number2)) {
    throw new InvalidArgumentException('Parameter $number2 is not a number');
  }

  if ($number2 === 0) {
    throw new InvalidArgumentException('Divide by zero error');
  }

  return $number1 / $number2;
}

We can also use the assert() function to do pretty much the same thing, we just swap out the if statements with calls to the assert() function.

function divideNumbers($number1, $number2) {
  assert(is_int($number1));
  assert(is_int($number2));
  assert($number2 !== 0);
  return $number1 / $number2;
}

If we did pass a non-numeric value to the calculation we would now see that PHP triggers a warning message. This is the default behaviour of assert() in PHP, although this can be extended and turned off (which I will look at alter).

PHP Warning:  assert(): Parameter $number1 is not a number failed in test.php on line 17

Warning: assert(): Parameter $number1 is not a number failed in test.php on line 17

Notice the difference between the throwing exception code and the assert() calls. When we call assert() we need to pass in a test that results in a true value. We are essentially saying to PHP that "this is what is correct" and any value that is passed that results in a false being passed to assert() will cause the program to error in this way.

Turning On And Off Assertions

Using assertions to test parameters is fine, but why use this over if statements and throwing exceptions?

The key difference to normal code is that we can disable the call to assert() in PHP. This means that we can have a series of strict tests and assertions on our development environment but as we don't necessarily need those checks on production we can disable them entirely. This frees up resources that would otherwise be used to always test the parameters.

By default, assert() is active in PHP, but there are a couple of ways to disable assert() in PHP.

The first is to alter the php.ini file and change the assert.active setting to 0.

assert.active = 0

You can also opt to disable assert() in code using the ini_set() function. This is perhaps the more usual option as it allows for cross platform compatibility.

ini_set('assert.active', '0');

Controlling the use of assert in you code is also useful from a testing point of view. When you kick off your unit tests you can also turn on assert so that if any assertions fail during tests you will know about it.

Setting assert.active to 0 will disable them in code, but the code will still be parsed and executed. In order to control this you need to use the zend.assertions setting. This setting can only be changed in your php.ini file and there are three different settings.

Setting zend.assertions to 1 means that everything will be active. The code will be generated and executed as normal.

zend.assertions = 1

Setting zend.assertions to 0 means that the code will be generated but it is ignored during run time.

zend.assertions = 0

Setting zend.assertions to -1 means that the code will not be generated at all. This should be the setting used for production.

​zend.assertions = -1

This is an important consideration when setting up your php.ini file is that turning off assertions using "zend.assertions = -1" also turns of the comparison code inside assert() functions. This means that having an assert() function call in your code has literally zero effect on your production code performance. The assert() function calls and any comparisons made do not make it into the compiled code for PHP and are therefore never run.

Configuring Assertions

Aside from assert.active, there are a few different options that can be passed to the ini_set() function to control assertions. Note that there is a function called assert_options(), but use of this function is discouraged as it will likely be deprecated in future versions of PHP.

The following options will change how the assert() function acts in your codebase.

assert.warning

If this is active PHP will issue a warning error for each failed assertion. If this is inactive you will not receive any output unless you have also configured an assertion callback (which we will cover later).

To turn this on in your php.ini file use this.

assert.warning = 1

Or, to activate it in your PHP code use this.

ini_set('assert.warning', 1);

This flag is useful if you want to cut down on the noise produced by your application. By default this is active.

assert.bail

This will tell PHP to stop all code execution is any assert() call receives a negative result. In other words, your program will die if any assertion fails. This is useful when developing as it helps your spot and solve problems quickly.

To turn this on in your php.ini file use this.

assert.bail = 1

Or, to activate it in your PHP code use this.

ini_set('assert.bail', 1);

By default this is inactive, so assertions will just produce warnings and continue the execution of the program.

assert.callback

This setting is used to create a callback that will be used every time an assertion test is false. This is useful if you want to, for example, log all failed assertions to a log or print out a message to the console.

To turn this on in your php.ini file use this.

assert.callback = 'assertion_error_callback'

Or, to activate it in your PHP code use this.

ini_set('assert.callback', 'assertion_error_callback');

The callback itself needs to accept a number of arguments that will point to the line of code that caused the problem. You can also pass in a description that can be optionally printed out.

function assertion_error_callback(string $file, int $line, string $code, string $desc = null) {
  echo "Assertion failed at $file:$line: $code";
  if ($desc) {
    echo ": $desc";
  }
  echo "\n";
}

Taking the example of the divideNumbers() function at the top of the article, if we run it again with the assert.callback turned on we will see the following printed out (assuming we passed a string as the first parameter).

Assertion failed at test.php:28: : assert(is_int($number1))

If we modify the code to inject the description parameter then the divideNumbers() function will look like this.

function divideNumbers($number1, $number2) {
  assert(is_int($number1), 'Parameter $number1 is not a number');
  assert(is_int($number2), 'Parameter $number2 is not a number');
  assert($number2 !== 0, 'Divide by zero error.');

  return $number1 / $number2;
}

Now, with the assertion callback active, we will see the messages like this being printed out if the same assertion fails.

Assertion failed at test.php:29: : Parameter $number1 is not a number.

By default this is null so no callback will be triggered.

assert.exception

This setting was added in PHP 7 and is used to control if a failed assert() test will throw an exception or not.

Setting this to 1 will cause any failed assertion tests to throw an exception using the built in AssertionError object. It is also possible to pass in a custom exception object (which I will cover later in this article).

To make failed assertion generate a PHP warning instead of throwing an exception. This can be set in the php.ini file like this.  

assert.exception = 0

This can also be altered using ini_set()

ini_set('assert.exception', 0);

By default this is set to 0, which means PHP will create a warning if an assert() fails, rather than throw an exception.

In PHP 8 this setting is set to 1 as a default, which will cause exceptions to always be thrown.

I should also note that the option assert.quiet_eval exists, but that this doesn't do much. It was previously possible to use evaluated strings in assert() and this option allowed you to turn off any errors that happened when the string was evaluated. The support for this has been removed in PHP8 and so shouldn't be used. Best practice is therefore to use actual PHP code in the assert() function calls. This means that assert.quiet_eval does nothing and will be removed in future versions of PHP.

Throwing Custom Exceptions

As of PHP 7 it is possible to pass a throwable object as the second parameter instead of passing in a description string. This just needs to be a class that implements the Throwable interface, but there is a class in PHP called AssertionError that we can use with assertions in the following way.

assert(is_int($number1), new AssertionError('Parameter $number1 is not a number'));

We can also create a custom class that we can throw. This is done by extending the AssertionError class from PHP.

class NumberError extends AssertionError {}

To use this class we just need to pass it as the second parameter to the assert() function.

assert(is_int($number1), new NumberError('Parameter $number1 is not a number'));

The output of this function depends on the assert.exception setting. If this setting is set to a 0 then a PHP warning will be generated using the throwable object as a transport mechanism for the information about the error. Essentially, if we fail an assertion we will receive output that looks like this.

PHP Warning:  assert(): NumberError: Parameter $number1 is not a number in test.php:8
Stack trace:
#0 test.php(14): divideNumbers('1', 0)
#1 {main} failed in test.php on line 8

Warning: assert(): NumberError: Parameter $number1 is not a number in test.php:8
Stack trace:
#0 test.php(14): divideNumbers('1', 0)
#1 {main} failed in test.php on line 8

Note! This warning assumes that you have assert.warning set to 1 (i.e. "on") otherwise no output will be printed. The different settings in the assert library will interact with each other so if you see no output then check the other settings you are using.

Setting assert.exception to 1 will cause an exception to be produced, which will cause a fatal error in our example code as we aren't catching this exception at all.

PHP Fatal error:  Uncaught NumberError: Parameter $number1 is not a number in test.php:8
Stack trace:
#0 test.php(14): divideNumbers('1', 0)
#1 {main}
  thrown in test.php on line 8

Fatal error: Uncaught NumberError: Parameter $number1 is not a number in test.php:8
Stack trace:
#0 test.php(14): divideNumbers('1', 0)
#1 {main}
  thrown in test.php on line 8

This is useful if you want to react to different types of assert fails.

Where To Use assert()?

Assertions are a versatile way of further checking your code in more detail than unit tests can do. They can augment your unit tests with data consistency checks inside functions, and even be activated when unit tests are run.

If you have a function then you should also include calls to assert() in order to ensure that your code works as expected. The asset() functions will allow you to easily spot problems straight away and look at finding solutions to them.

As assertions can be entirely turned off in production there is no reason not to use them.

Don't rely too much on them though. The use of assert() should not replace data input checks as you would still need to check that user input is correctly checked. The assert() function is excellent to use in the configuration and setup of your application.

Use Of assert() In Open Source Projects

I first heard about the assert() function when looking at open source projects (which prompted me to look deeper). I thought I would demonstrate where I have found the assert() function in use in a couple of projects.

In Drupal, the abstract class AccessResult has a static method called neutral() that is used by the system to create an AccessResultNeutral class. This is used when the system has no opinion on the permission being checked. The function itself contains a call to assert() to check that the reason parameter is a string or is null.

  public static function neutral($reason = NULL) {
    assert(is_string($reason) || is_null($reason));
    return new AccessResultNeutral($reason);
  }

In SimpleSAMLphp, there is a class called Configuration that is used to manage the loaded configuration for the system. The function to set the configuration directory is called setConfigDir(), and this function contains a couple of calls to assert to ensure that the $path and $configSet parameters are strings. 

    public static function setConfigDir($path, $configSet = 'simplesaml')
    {
        assert(is_string($path));
        assert(is_string($configSet));

        self::$configDirs[$configSet] = $path;
    }

See if you can find assert() being used in other open source projects.

Don't forget to check out the documentation about assert() on the php.net website.

Add new comment

The content of this field is kept private and will not be shown publicly.