Skip to content

4.0: Exceptables

Adrian edited this page Mar 14, 2024 · 1 revision

The IsExceptable trait provides a complete base implementation for the Exceptable interface. A working implementation is just a few simple steps:

  • extend a Throwable class
  • implement the Exceptable interface
  • use the IsExceptable trait
  • define your error codes and INFO.

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

your first exceptable

Here's a brief example Exceptable:

<?php

use AT\Exceptable\Spl\RuntimeException;

class FooException extends RuntimeException {

  // define your error code.
  const UNKNOWN_FOO = 1;

  // define information about your errors (indexed by error code).
  // at a minimum, include a message.
  const INFO = [
    self::UNKNOWN_FOO => ["message" => "unknown foo"]
  ];

  // that's it
}

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

<?php

throw new FooException(FooException::UNKNOWN_FOO);
// Fatal error: Uncaught FooException: unknown foo ...

adding context

Note, our Exceptable set the proper exception message for us. But, this message is generic and fairly useless. Let's add some $context.

<?php

use AT\Exceptable\Spl\RuntimeException;

class FooException extends RuntimeException {

  const UNKNOWN_FOO = 1;

  const INFO = [
    self::UNKNOWN_FOO => [
      "message" => "unknown foo",
      "format" => "i don't know who, you think is foo, but it's not {foo}"
    ]
  ];
}

Here, format is a message formatting string. It takes named {placeholders} from contextual information your code will provide at runtime. If a value for a named placeholder is not provided, then the Exceptable will fall back on using the default message.

<?php

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

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 your error conditions have codes, your program can read Exceptables almost as well as you can - without looking at the message at all.

<?php

use AT\Exceptable\Spl\RuntimeException;

class FooException extends RuntimeException {

  const UNKNOWN_FOO = 1;
  const SPOOKY_FOO = 2;

  const INFO = [
    self::UNKNOWN_FOO => [
      "message" => "unknown foo",
      "format" => "i don't know who, you think is foo, but it's not {foo}"
    ],
    self::SCARY_FOO => [
      "message" => "woooo spooky foo",
      "format" => "Ph'nglui mglw'nafh {foo} R'lyeh wgah'nagl fhtagn"
    ]
  ];
}
<?php

try {
  $e = rand(0, 1) ?
    new FooException(FooException::UNKNOWN_FOO, ["foo" => "foobedobedoo"]) :
    new FooException(FooException::SPOOKY_FOO, ["foo" => "Cthulhu"]);
  throw $e;
} catch (FooException $e) {
  if (FooException::is($e, FooException::UNKNOWN_FOO)) {
    // we know how to handle this; everyone can be happy
    introduceFoo($e->getContext()["foo"]);
  } else {
    // nnnope
    RUN_AWAY_RUN_AWAY($e->getRoot()->getMessage());
  }
}

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 class and code.

Since we can pass a $context array to the Exceptable, it makes sense that we'd have a getContext() 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 getRoot() 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.

Clone this wiki locally