Skip to content

Usage: Exceptables

Adrian edited this page Mar 27, 2024 · 22 revisions

These examples will all use the following Error enum:

use at\exceptable\ {
  Error,
  IsError
};

enum FooError : int implements Error {
  use IsError;

  case UnknownFoo = 1;
  case SpookyFoo = 2;

  public const MESSAGES = [
    self::UnknownFoo->name => "i don't know who, you think is foo, but it's not {foo}",
    self::SpookyFoo->name => "ph'nglui mglw'nafh {foo} r'lyeh wgah'nagl fhtagn"
  ];
}

learn more about Error enums.

your first exceptable

The IsExceptable trait provides a complete base implementation for the Exceptable interface. A working implementation is easy: extend a Throwable class, implement the Exceptable interface, and use the IsExceptable trait

Even easier, however, is to extend from one of the Spl Exceptables.

So here's a brief example Exceptable in three easy steps:

<?php

// step one: choose an Spl Exceptable base class
use at\exceptable\Spl\RuntimeException;

// step two: extend your new Exceptable class from it
class FooException extends RuntimeException {}

// step three:
// there's no step three

Exceptables have very straightforward constructors. The first, and often only, argument you'll need to provide is the Error case:

<?php

throw new FooException(FooError::UnknownFoo);
// Fatal error: Uncaught FooException: FooError.UnknownFoo

adding context

Note, our Exceptable set an exception message for us. But, this message has no details - why didn't it use the full message that FooError defined? Let's add some $context.

<?php

throw new FooException(FooError::UnknownFoo, ["foo" => "foobedobedoo"]);
// Fatal error: Uncaught FooException: FooError.UnknownFoo: i don't know who, you think is foo, but it's not foobedobedoo

"previous" exceptions

Just like normal exceptions, you can pass a $previous exception (exceptable or not) as the third argument.

handling exceptables

Uncaught exceptions are great and all, but what if we want to catch them? How do we know what to do with them? Because you have defined error cases, your program can read Exceptables almost as well as you can - without looking at the message at all.

<?php

try {
  aFunctionThatThrowsFooException();
} catch (FooException $x) {
  if ($x->is(FooException::UnknownFoo)) {
    // we know how to handle this; everyone can be happy
    introduceFoo($x->context()["foo"]);
  } else {
    // nnnnope too spooky
    throw $x;
  }
}

useful utilities

In the above examples, you might have noticed some of those useful utilities.

We can use the is() method to identify Exceptables by their Error case.

Similarly, there is has() to see if the Exceptable, or any "previous" exceptions, match a given Error case.

Since we can pass a $context array to the Exceptable, it makes sense that we'd have a context() method to get it back.

When you have a chain of previous exception(s), it's common that the initial exception is of more interest than other, intermediate exceptions; so we have root() to get it directly (if there is no previous exception, no problem).

extending exceptables

There is a test suite for the base IsExceptable trait, which is also be useful as a starting point for testing your own Exceptables. Run it with composer test:unit.