PHP (and, as it happens, most modern languages) rely on a fairly rudimentary concept of Exceptions to handle errors at runtime. The principle is generally sound, however the implementation suffers from a handful of key flaws.
Primarily, meaning is inferred by the class name of the Exception being thrown.
throw new OutOfBoundsException('Index is not in range');
While this works, it is fundamentally limiting; PHP does not have multiple-inheritance and so can only convey one meaning directly via the class name, does not imply any context of scope (ie, where the error occurred), and requires writing unnecessary boilerplate code to represent every form of meaning being relayed.
namespace MyLibrary {
class TooMuchTypingException extends \RuntimeException {}
}
namespace MyOtherLibrary {
class TooMuchTypingException extends \RuntimeException {}
}
Having libraries that need to convey the same meaning but from different contexts compound this problem by either having to redefine the same class in their own namespace, or rely on traits to share functionality.
The structure of the class that makes an Exception should be dedicated to providing the functionality required to convey the state of the application in the context it is used.
While classes cannot convey multiple messages, interfaces can.
namespace MyLibrary;
interface NotFoundException {}
interface FailedServiceException {}
class MethodNotFoundException extends \RuntimeException implements NotFoundException, FailedServiceException {}
try {
throw new MethodNotFoundException('Test');
} catch(NotFoundException | FailedServiceException $e) {}
However interfaces alone cannot immediately infer where the problem originated as you still require a class to be defined for each context from which the Exception may be thrown.
Also, this requires writing and loading lots of boilerplate code to represent what are ultimately simple, static messages.
Instead of defining a class for every Exception that may be thrown, interfaces can be generated at runtime to represent a specific meaning of an error, and assigned to anonymous classes as and when they are needed.
The generated interfaces can be placed throughout the namespace tree so that try / catch blocks can check for the same message at any level of namespace depth, and the resulting anonymous class can automatically extend from PHP's built in set of named Exceptions.
Exceptional attempts to do all of this automatically from the minimum amount of input.
namespace MyLibrary\AThingThatDoesStuff;
use DecodeLabs\Exceptional;
class Amazeballs {
public function doStuff() {
throw Exceptional::{'NotFound,FailedService'}(
'Service "doStuff" cannot be found'
);
}
}
The resulting object would look something like this:
namespace DecodeLabs\Exceptional {
interface Exception {}
interface NotFoundException {
const EXTEND = 'RuntimeException';
}
}
namespace MyLibrary {
interface Exception extends
DecodeLabs\Exceptional\Exception {}
}
namespace MyLibrary\AThingThatDoesStuff {
interface Exception extends
MyLibrary\Exception {}
interface NotFoundException extends
MyLibrary\AThingThatDoesStuff\Exception,
DecodeLabs\Exceptional\NotFoundException {}
interface EailedServiceException extends
MyLibrary\AThingThatDoesStuff\Exception {}
$e = new class($message) extends \RuntimeException implements
MyLibrary\AThingThatDoesStuff\NotFoundException,
MyLibrary\AThingThatDoesStuff\FailedServiceException {}
}
The generated Exception can be checked for in a try / catch block with any of those scoped interfaces, root interfaces or PHP's RuntimeException.
Any functionality that the Exception then needs to convey the state of the error can then either be mixed in via traits, or by extending from an intermediate class that defines the necessary methods.