hoa/exception
Hoa\Exception is a PHP library for richer exception handling. It treats exceptions as objects with formatted, parameterized messages, supports exception groups, chaining, and customizable rendering, making error reporting clearer and more structured in PHP applications.
hoa/exception is an archived Hoa package that models exceptions and "exception-shaped values" with an API designed before PHP 7's Throwable. Its final release (2.17.08.30, 2017-08-30) moved the library to PHP 7 and dropped PHP 5, PHP 7.0, and HHVM, so treat PHP 7.1 as the effective floor. It is installed with Composer:
composer require hoa/exception
Because that was the last release and it predates PHP 7.4/8.x, pin the version and test it against your runtime before adopting it. In a legacy Hoa codebase it will already be present.
Look first at four classes. Since this release moved the code into a Source/ directory, the files live at vendor/hoa/exception/Source/:
Source/Exception.php — the throwable base class.Source/Idle.php — an exception-shaped value meant to be collected, not thrown.Source/Group.php — an exception that aggregates other exceptions.Source/Throwable.php — the package's own interface, implemented by both Exception and Idle.The first use case is a message template plus arguments. Instead of concatenating strings at throw time, pass the raw format and the values; call getFormattedMessage() when you log or display it.
use Hoa\Exception\Exception;
function openFile(string $path, string $mode)
{
$handle = @fopen($path, $mode);
if (false === $handle) {
throw new Exception(
'Cannot open %s with mode %s.',
0,
null,
$path,
$mode
);
}
return $handle;
}
try {
openFile('/etc/shadow', 'r');
} catch (Exception $e) {
error_log($e->getFormattedMessage());
}
Extra constructor arguments after $message, $code, and $previous are used as format arguments. The message stays raw until formatted, which is useful for logging and translation pipelines.
Use Idle in validation or import code that should not abort on the first problem.
use Hoa\Exception\Idle;
$errors = [];
foreach ($rows as $i => $row) {
if (false === filter_var($row['email'], FILTER_VALIDATE_EMAIL)) {
$errors[] = new Idle(
'Row %d has invalid email %s.',
0,
null,
$i,
$row['email']
);
}
}
Then convert the collection into something throwable at the boundary: a Group or a normal Exception.
Group extends Exception and implements ArrayAccess, Countable, and IteratorAggregate, so it can be thrown as one exception and inspected as a list.
use Hoa\Exception\Exception;
use Hoa\Exception\Group;
$group = new Group('Validation failed.');
$group[] = new Exception('Field %s is required.', 0, null, 'email');
$group[] = new Exception('Field %s is too short.', 0, null, 'password');
throw $group;
try {
$validator->validate($input);
} catch (Group $group) {
foreach ($group as $error) {
$messages[] = $error->getFormattedMessage();
}
}
Catch Group before Exception when you need per-item handling. Otherwise the group is handled as one exception and the individual messages are lost.
Override getId() in domain exception subclasses to get a machine-readable key independent of the message text.
final class PaymentDeclined extends \Hoa\Exception\Exception
{
public function getId()
{
return 'payment.declined';
}
}
Log both the identifier and the formatted message:
catch (\Hoa\Exception\Exception $e) {
$logger->warning($e->getId(), [
'message' => $e->getFormattedMessage(),
'raw' => $e->getRawMessage(),
'args' => $e->getArguments(),
]);
}
If a method accepts either a thrown exception or an idle value, use Hoa\Exception\Throwable:
use Hoa\Exception\Throwable;
function normalizeError(Throwable $e): array
{
return [
'id' => $e->getId(),
'message' => $e->getFormattedMessage(),
];
}
Do not type-hint native \Throwable if you need to accept Idle; it does not satisfy the PHP 7 \Throwable contract.
Most frameworks and PSR-3 loggers call getMessage() or stringify the exception. That can expose the raw format string. Add a small normalizer at the boundary:
function loggable(\Throwable $e): array
{
if ($e instanceof \Hoa\Exception\Throwable) {
return [
'class' => get_class($e),
'id' => $e->getId(),
'message' => $e->getFormattedMessage(),
];
}
return [
'class' => get_class($e),
'message' => $e->getMessage(),
];
}
This keeps formatted messages in logs without changing every throw site.
Source/. If you copy files out of vendor/hoa/exception, vendor a fork, or maintain a classmap/exclude rule, update the paths (vendor/hoa/exception/Source/Exception.php, etc.). Composer autoloading is unaffected, but hardcoded paths in scripts and CI config break silently.Throwable design, despite the PHP 7 port. The final release modernised the runtime requirement, but the type model is unchanged: Idle still does not implement native \Throwable. Do not assume the PHP 7 migration made the package idiomatic for PHP 7 error handling.hoa/consistency is pinned via this release. The update to hoa/consistency shipped here constrains transitive resolution; on a modern app it can drag other Hoa packages to old versions. Isolate this dependency or avoid it in new services.getMessage() may not be formatted. The raw format string can leak into logs, HTTP responses, and framework error pages. Always call getFormattedMessage() for display or logging, or normalize exceptions centrally.getFormattedMessage() is called, often inside a catch block. On PHP 8, sprintf-style mismatches can throw ArgumentCountError or ValueError; that secondary exception can mask the original. Validate argument counts and escape literal percent signs as %%.%s, %n, or stray % sequences in the input change the message or trigger warnings.Idle is for collection, not throwing. It exists so you can build exception-like values without interrupting control flow. Convert it to a real Exception or Group at the boundary rather than relying on it propagating.Group is still an Exception. A broad catch (Exception $e) catches the group as one item. If you need all underlying errors, catch Group explicitly and iterate.Throwable is not native \Throwable. Type hints against \Throwable will not accept Idle. Use Hoa\Exception\Throwable where both are expected, and native \Throwable where only real throwables are expected.getRawMessage() and getArguments() intact.getPrevious() can return native exceptions as well as Hoa ones. When walking the chain, check instanceof Hoa\Exception\Throwable before calling getFormattedMessage().Hoa\Exception\Exception per domain and override getId(). Avoid putting logic in the constructor; keep message templates static so monitoring can group by identifier.Source/Exception.php, Source/Idle.php, and Source/Group.php first; the API surface is small.How can I help you explore Laravel packages today?