ParameterisedException

Since v1.2016041701

Description

ParameterisedException is a base class that builds the exception's message from a supplied format string and data set. The data set is stored in the exception. This can be logged and/or inspected to help with debugging and troubleshooting.

Public Interface

ParameterisedException has the following public interface:

/**
 * ParameterisedException provides a data-oriented base class
 * for your exceptions
 */
class ParameterisedException extends RuntimeException
{
    /**
     * default values for extra data
     * @var array
     */
    static protected $defaultExtras = [];

    /**
     * default filter for the call stack
     * @var array
     */
    static protected $defaultCallStackFilter = [];

    /**
     * our constructor
     *
     * You should call one of the 'newFromXXX()' methods to create a new
     * exception to throw. These methods customise the format string and
     * exception data for different contexts.
     *
     * @param string $formatString
     *        the sprintf() format string for our human-readable message
     * @param array $data
     *        the data required to sprintf() with $formatString
     * @param int $code
     *        the error code that you want to set (if any)
     */
    public function __construct($formatString, $data = [], $code = 0);

    /**
     * create a new exception about your function / method's input parameter
     *
     * @param  mixed $fieldOrVar
     *         the input parameter that you're throwing an exception about
     * @param  string $fieldOrVarName
     *         the name of the input parameter in your code
     * @param  array $extraData
     *         extra data that you want to include in your exception
     * @param  int|null $typeFlags
     *         do we want any extra type information in the final
     *         exception message?
     * @param  array $callStackFilter
     *         are there any namespaces we want to filter out of
     *         the call stack?
     * @return ParameterisedException
     *         an fully-built exception for you to throw
     */
    public static function newFromInputParameter(
        $fieldOrVar,
        $fieldOrVarName,
        array $extraData = [],
        $typeFlags = null,
        array $callStackFilter = []
    );

    /**
     * create a new exception about a value generated by / returned to your
     * function or method
     *
     * @param  mixed $fieldOrVar
     *         the value that you're throwing an exception about
     * @param  string $fieldOrVarName
     *         the name of the value in your code
     * @param  array $extraData
     *         extra data that you want to include in your exception
     * @param  int|null $typeFlags
     *         do we want any extra type information in the final
     *         exception message?
     * @param  array $callStackFilter
     *         are there any namespaces we want to filter out of
     *         the call stack?
     * @return ParameterisedException
     *         an fully-built exception for you to throw
     */
    public static function newFromVar(
        $fieldOrVar,
        $fieldOrVarName,
        array $extraData = [],
        $typeFlags = null,
        array $callStackFilter = []
    );

    /**
     * create the format string and exception data
     *
     * @param  string $builder
     *         name of the ParameterBuilder class to use
     * @param  string $formatString
     *         the format string to customise
     * @param  array $backtrace
     *         a debug_backtrace()
     * @param  mixed $fieldOrVar
     *         the value that you're throwing an exception about
     * @param  string $fieldOrVarName
     *         the name of the value in your code
     * @param  array $extraData
     *         extra data that you want to include in your exception
     * @param  int|null $typeFlags
     *         do we want any extra type information in the
     *         final exception message?
     * @param  array $callStackFilter
     *         are there any namespaces we want to filter out of
     *         the call stack?
     * @return array
     *         0 - the formatString to use
     *         1 - the exception data to use
     */
    protected static function buildFormatAndData(
        $builder,
        $formatString,
        $backtrace,
        $fieldOrVar,
        $fieldOrVarName,
        array $extraData = [],
        $typeFlags = null,
        array $callStackFilter = []
    );

    /**
     * what was the data that we used to create the printable message?
     *
     * @return array
     */
    public function getMessageData();

    /**
     * what was the format string we used to create the printable message?
     *
     * @return string
     */
    public function getMessageFormat();
}

How To Use

Creating Your Own Exceptions

The idea is that you shouldn't throw ParameterisedException. You should extend it to create your own exceptions. Here's a real example of how to do so.

namespace GanbaroDigital\ExceptionHelpers\V1\BaseExceptions;

use GanbaroDigital\HttpStatus\Interfaces\HttpRuntimeErrorException;
use GanbaroDigital\HttpStatus\StatusProviders\RuntimeError\
  UnexpectedErrorStatusProvider;

class UnsupportedType
  extends ParameterisedException
  implements HttpRuntimeErrorException
{
    static protected $defaultFormat =
        "'%fieldOrVarName\$s' cannot be '%dataType\$s'";

    // adds 'getHttpStatus()' that returns a HTTP 500 status value object
    use UnexpectedErrorStatusProvider;
}

In this example, UnsupportedType:

There's no need for UnsupportedType to create its own static factory methods. It automatically inherits the ones defined by ParameterisedException. (They are discussed in more detail below).

Why You Need To Create Your Own Exception Classes

An exception's class name is used by PHP to tell what kind of error occurred.

  • PHP's catch mechanism can only catch exceptions that use different classes.
  • Stack traces and error messages that go into your logs also use the exception class's name to tell you which error occurred.

You'll need this information when your app isn't working in production. When that happens, you're normally under a lot of pressure to understand the problem and fix it as quickly as possible. It's normally a very stressful event.

The more detailed information you have available about an error, the easier it is to deal with production problems without making things worse.

Static Factory Methods

Static factory methods are static methods that create new exceptions for you to throw. Each static factory method should customise the exception's format string and exception data to suit the context that the exception is being thrown in.

ParameterisedException defines the following static factory methods:

Factory Method When To Use
::newFromInputParameter() when $fieldOrVar was passed to your function or method as a parameter
::newFromVar() when $fieldOrVar is the return value from calling a function or method, or is a value created by your function or method

Each static factory method returns a new exception for your code to throw.

throw UnsupportedType::newFromInputParameter($className, '$className');
throw UnsupportedValue::newFromVar($statusCode, '$statusCode');

We expect to add new static factory methods in the future.

Why Static Factory Methods?

There's a different static factory method for each reason why you'd want to throw an exception. Each one tailors both the format string and exception data that gets built into the exception that is thrown. (Format strings and exception data are described further down the page).

We tailor these to make it easier to debug your application, both in your debugger and when reading your application logs.

The Format String

The format string is the first parameter passed to ParameterisedException::__construct().

Internally, ParameterisedException uses vnsprintf() from PHP: The Missing Bits. This allows you to use named parameters in the format string:

$format = "Value must be between %min\$s and %max\$s inclusively";
$data = [
    'min' => 10,
    'max' => 20,
];
throw new ParameterisedException($format, $data);

The Format String In Your Own Exceptions

Our static factory methods expect you to add static protected $defaultFormat to your own exceptions. This will contain the format string that they will customise.

Factory Method Customised Format String
::newFromInputParameter() %calledByName$s: %thrownByName$s says $defaultFormat
::newFromVar() %thrownByName$s: $defaultFormat

Exception Data

Each ParameterisedException contains extra data for you to write to your logs or inspect in your debugger of choice. For example:

try {
    throw NoSuchClass::newFromInputParameter('UndefinedClass');
}
catch (NoSuchClass $e) {
    // extract the extra data
    // getMessagedData() returns a PHP array
    $exData = $e->getMessageData();
}

Here's the full list of the extra data that ParameterisedException can provide.

Parameter Type Description
thrownBy CodeCaller details of the function or method that is throwing the exception
thrownByName string human-readable summary of thrownBy
calledBy CodeCaller details of the function or method that has called the thrownBy function or method
calledByName string human-readable summary of calledBy
fieldOrVarName string the $fieldOrVarName passed into the factory method
fieldOrVar mixed the $fieldOrVar passed into the factory method
dataType string human-readable description of $fieldOrVar's PHP type

Here's a list of the extra data added by each factory method.

Factory Method Extra Data Added
::newFromInputParameter() thrownBy, thrownByName, calledBy, calledByName, fieldOrVarName, fieldOrVar, dataType
::newFromVar() thrownBy, thrownByName, fieldOrVarName, fieldOrVar, dataType

Adding Extra Exception Data In Your Own Exceptions

Each static factory method takes a parameter $extraData. This will be merged into the extra data added to your exception. Anything in $extraData can be used in your format string:

class OutOfRange extends ParameterisedException
{
    static protected $defaultFormat = "'%fieldOrVarName\$s' cannot be higher than '%maxRange\$d'";
}

throw OutOfRange::newFromVar($data, '$data', ['maxRange' => 100]);

Setting Default Values For Extra Exception Data In Your Own Exceptions

Each exception class can set the static property $defaultExtras to contain the default values of any extra data:

class OutOfRange extends ParameterisedException
{
    static protected $defaultFormat = "'%fieldOrVarName\$s' cannot be higher than '%maxRange\$d'";
    static protected $defaultExtras = ['maxRange' => 100];
}

Defaults mean that it is safe to call the static factory methods without providing an array of extra data:

throw OutOfRange::newFromVar($data, '$data');

But the defaults can still be overridden if necessary:

throw OutOfRange::newFromVar($data, '$data', ['maxRange' => 200]);

Filtering The Call Stack

The throwBy, thrownByName, calledBy and calledByName data is built when you call one of the static factory methods. A copy of the PHP call stack is taken, and the first two usable entries are used to build this data.

Sometimes, you'll want to skip over some of the entries in the call stack. This is normally done in libraries that are providing reusable robustness or quality checks, such as our own Contracts Library and Type-Checking Library.

You can filter the call stack by providing an array of classes as the $callStackFilter parameter:

throw UnsupportedType::newFromVar($var, '$var', [], null, [self::class]);

You can also define a default call stack filter in your exception:

class ContractFailed extends ParameterisedException
{
    // default filter for the call stack
    static protected $defaultCallStackFilter = [
        self::class,
        static::class,
        AssertValue::class,
        CheckContracts::class,
        Contracts::class,
    ];
}

// no need to provide a call stack filter
throw ContractFailed::newFromVar($var, '$var');

Class Contract

Here is the contract for this class:

GanbaroDigital\ExceptionHelpers\V1\BaseExceptions\ParameterisedException
 [x] Can instantiate
 [x] Is runtime exception
 [x] Can pass code into constructor
 [x] Sets exception message to formatted string
 [x] Format string supports named parameters
 [x] Can get message data
 [x] Can get message format string
 [x] can create from input parameter
 [x] can create from input parameter with extra data
 [x] can create from input parameter with default callerFilter
 [x] can create from input parameter with callerFilter
 [x] can create from PHP variable
 [x] can create from PHP variable with extra data
 [x] can create from PHP variable with default callerFilter
 [x] can create from PHP variable with callerFilter

Class contracts are built from this class's unit tests.

Future releases of this class will not break this contract.

Future releases of this class may add to this contract. New additions may include:

  • clarifying existing behaviour (e.g. stricter contract around input or return types)
  • add new behaviours (e.g. extra class methods)

When you use this class, you can only rely on the behaviours documented by this contract.

If you:

  • find other ways to use this class,
  • or depend on behaviours that are not covered by a unit test,
  • or depend on undocumented internal states of this class,

... your code may not work in the future.

Notes

None at this time.

See Also

Changelog

v1.2016061902

v1.2016061901