The libraries main aim is to generate dynamic Exceptions based on a set of criteria in any particular context, with minimum boilerplate code.
Exceptional combines a number of techniques to create a predictable and easy to use interface to the Exception generator mechanism.
Primarily, the main Exceptional
static class provides a __callStatic()
method that acts as a go-between to the Exception factory.
One major benefit of this structure is making use of the ability to pass arbitrary strings as method names to __callStatic()
.
Exceptional uses this feature as a means of passing through the projected type of exception to be generated, and parses that method name out to expand commas into an array:
Exceptional::{'AnythingGoesHere,BadMethodCall'}('Test exception');
// Internally
// $types = ['AnythingGoesHereException', 'BadMethodCallException'];
It is the sole responsibility of the Factory to actually generate an instance of an Exception for the calling code to throw.
It uses a combination of eval()
and anonymous classes to build a custom class specific to the current context containing a mix of interfaces and traits, to define type, message and functionality.
The exception Factory uses debug_backtrace()
to work out the namespace from which Exceptional was called and uses this to decide which interfaces need to be generated and what needs to be rolled into the final Exception class.
It's aim is to have an interface named with each of the types defined in the original call to the Factory (eg Runtime
, NotFound
) defined within the namespace of the originating call so that catch
blocks can reference the type directly.
namespace Any\Old\Namespace;
use DecodeLabs\Exceptional;
try {
throw Exceptional::Runtime('message');
} catch(
\RuntimeException |
RuntimeException |
Any\Old\Namespace\RuntimeException $e
) {
// do something
}
Secondary to that, if the requested types are listed as primary exception types by the Factory then there will also be an interface to represent it in the Exceptional namespace:
namespace Any\Old\Namespace;
use DecodeLabs\Exceptional;
try {
throw Exceptional::Runtime('message');
} catch(Exceptional\RuntimeException $e) {
// do something
}
On top of that, the Factory will ensure there is an interface named Exception
at every namespace level up the tree to the target namespace (so long as that name is free in that context) so that developers can choose the granularity of catch blocks, ad hoc:
namespace Any\Old\Namespace;
use MyLibrary\InnerNamespace\SomeClass;
$myLibrary = new SomeClass();
try {
// This method will throw an Exceptional Exception
$myLibrary->doAThing();
} catch(
MyLibrary\InnerNamespace\Exception $e |
MyLibrary\Exception $e |
Exceptional\Exception $e
) {
// All of the above tests will match
}
To increase compatibility with SPL exceptions, any types that have a corresponding SPL Exception class will extend from that type, rather than the root Exception class:
namespace Any\Old\Namespace;
use DecodeLabs\Exceptional;
try {
throw Exceptional::Runtime('message');
} catch(\RuntimeException $e) {
// do something
}
And then for any interface that is added to the final type definition, the equivalent <InterfaceName>Trait
trait will be added too, if it exists. This allows the inclusion of context specific functionality within a specific category of Exceptions without having to tie the functionality to a particular meaning.
As an example, given the fallowing Exceptional call:
namespace MyVendor\MyLibrary\SubFunctions;
use DecodeLabs\Exceptional;
trait RuntimeExceptionTrait {
public function extraFunction() {
return 'hello world';
}
}
try {
throw Exceptional::Runtime('message');
} catch(RuntimeException $e) {
echo $e->extraFunction();
}
The resulting anonymous class will include:
MyVendor\MyLibrary\SubFunctions\RuntimeException
interfaceMyVendor\MyLibrary\SubFunctions\RuntimeExceptionTrait
trait, withextraFunction()
DecodeLabs\Exceptional\RuntimeException
interfaceMyVendor\MyLibrary\SubFunctions\Exception
interfaceMyVendor\MyLibrary\Exception
interfaceMyVendor\Exception
interfaceRuntimeException
base class
Once the Factory has generated an Exception for a particular subgroup of requested types within a specific namespace, it is hashed and cached so that repeated calls to the Factory within the same context can just return a new instance of the anonymous class. The resulting performance overhead of general usage of Exception exceptions then tends to be trivial, while the development overhead is massively reduced as there is no need to define individual Exception classes for every type of error in all of your libraries.