Joseph Edmonds

PHP Exceptions: Writing for clarity and testability

by Joseph Edmonds

by Joseph Edmonds

PHP has had exceptions for a long time. Most PHP developers are all too familiar with dealing with exceptions. In this post I’m going to suggest a way of writing your PHP Exception code in a way that is easy to test and easy to refactor.

Personally I have chosen this style as it makes my life easy and I find it clear and easy to work with.

Take a look at this PHPUnit test:

     * @test
     * @covers \MicroDeps\Curl\CurlException
    public function itExceptsOnInvalidLogFolder(): void
                'mkdir(): Permission denied'

The test is going to do something that is expected to throw an exception of a specific type and the exception message should also be an explicit string.

PHPUnit has excellent support for testing exceptions as documented here:

Keeping it DRY

Now, hopefully you are already familiar with the concept of DRY when coding. DRY stands for “Don’t Repeat Yourself” and the principle is quite simple – you should not be copying and pasting stuff around. Instead there should be a single source for things that you can then include in your code base in multiple places are required.

In this code, we are using a constant to define the exception message and that means that we can easily change the actual wording of the exception message, without breaking this test, nor having to do any bulk find/replace type shenanigans.

The exception message is stored as a public constant on the exception class that it is related to:

public const MSG_DIRECTORY_NOT_CREATED      = 'Directory "%s" was not created, error: %s';

By storing the exception message as a constant, we make writing tests a lot easier and can be very precise about the expected exception message.

Message Format

The exception message is a special kind of string that implements a “format” which can then be used with the sprintf built in function. The format has placeholders for values that are then passed in when calling the function.

When we want to throw the exception, we need to also use the same constant and pass in the required parameters. For example:

           if (!is_dir($logFileDir) && !@mkdir($logFileDir, 0755, true) && !is_dir($logFileDir)) {
                $error = error_get_last();
                throw CurlException::withFormat(
                    $error['message'] ?? 'unknown error'

Static Exception Create Method

You’ll notice that the throw statement does not use the common style of throw new CurlException but instead is calling a static method on the CurlException class which acts as a factory method to create an instance of the exception with a specific message.

Here is that method:

    public static function withFormat(string $format, string ...$values): self
        return new self(sprintf($format, ...$values));

Previous Exceptions

Where the Exception is being thrown after catching a previous Exception, it’s important that we log that correctly. For that reason there is a separate withFormatAndPrevious method:

public static function withFormatAndPrevious(string $format, Throwable $previous, string ...$values): self
        return new self(sprintf($format, ...$values), $previous);

This is used when catching another Exception but wanting to ensure that our code emits a standard Exception type so that it can be handled easily, for example:

     try {
            curl_setopt_array($this->handle, $this->options->get());
        } catch (ValueError $valueError) {
            $valid   = array_flip(get_defined_constants(true)['curl']);
            $invalid = array_diff_key($this->options->get(), $valid);
            throw CurlException::withFormatAndPrevious(
                print_r($invalid, true),
                /* @phpstan-ignore-next-line confused by curl_version return type */

Private Constructor

To enforce that we always throw our exceptions with a format, I’ve actually made the constructor for the Exception class private.

    private function __construct(string $message, Throwable $previous = null)
        $message = $this->appendInfo($message);
        parent::__construct($message, 0, $previous);


This is just my style. I’m not saying its the only way to do things, but I do think it’s nice and worth considering.

This approach to structuring your Exception classes and predefining the exception message as constants within the Exception class is, I believe, a nice tidy way of managing your exception messages and also making them easy to test.

This approach allows you to have “general” Exception classes that can be used in multiple error scenarios without feeling the need to create a specific Exception class for each and every error scenario.

Most importantly, it allows you to easily and accurately test your exception throwing code to confirm the exact message.

About Joseph Edmonds

About Joseph Edmonds

Personal musings, tech tips, trivia and other things that might be interesting.

Get in touch today

Send me a message concerning your idea, problem, or question – and I’ll personally respond as soon as possible.

Connect with me on LinkedIn.