pioniro/contextable-exception
Contextable exceptions for Laravel and PHP: throw exceptions that carry structured context data, so you can attach the values, inputs, or state behind a failure and read them back later when handling or logging it. Lightweight and simple to adopt.
Install the package and confirm the trait/interface names it ships with before writing any application code — the repository is small and its README is the source of truth:
composer require pioniro/contextable-exception
There is no service provider to register and no config to publish in a package of this scope; it is a trait + interface pair you compose into your own exception classes. The first place to look is vendor/pioniro/contextable-exception/src — you need to know (a) the interface name, (b) the trait name, and (c) the exact accessor for context (getContext(), context(), or similar). Confirm the method signature once, then treat the package as frozen: last release was April 2020, so assume it will not change and that you own any compatibility fixes.
Your first use case should be a single domain exception in a service class, not a wholesale refactor:
// app/Exceptions/PaymentFailedException.php
namespace App\Exceptions;
use Exception;
use Pioniro\ContextableException\ContextableExceptionInterface;
use Pioniro\ContextableException\ContextableExceptionTrait;
class PaymentFailedException extends Exception implements ContextableExceptionInterface
{
use ContextableExceptionTrait;
}
throw (new PaymentFailedException('Gateway declined the charge'))
->withContext([
'order_id' => $order->id,
'gateway' => $gateway->name(),
'status' => $response->status(),
]);
That single throw gives you structured, greppable context in storage/logs/laravel.log instead of string-concatenated messages. Once that works end to end, expand to the two or three exception classes that actually get thrown in production.
Base exception class per domain. Rather than sprinkling the trait, create a shared abstract base and inherit:
abstract class DomainException extends Exception implements ContextableExceptionInterface
{
use ContextableExceptionTrait;
}
Every concrete exception then gets context support for free, and you can add render()/report() overrides once instead of per class.
Handler integration. The highest-leverage hook is app/Exceptions/Handler.php. Flatten the context into the log record so it is queryable:
public function report(Throwable $e): void
{
if ($e instanceof ContextableExceptionInterface) {
Log::withContext(['exception_context' => $e->getContext()]);
}
parent::report($e);
}
Log::withContext() is the right primitive in modern Laravel because it attaches to every subsequent log line in the request, including chained handlers.
Context enrichment as the exception bubbles. Because the context is mutable data on the object, you can layer it: the repository adds the query, the service adds the aggregate ID, the job adds the attempt number. Prefer one throw with a rich context over wrapping the same exception three times — wrapping loses the original message in most log formatters.
Carrying context through queued jobs. Queued exceptions are serialized into failed_jobs. Anything you put in context must be JSON-serializable — models will be hydrated if you pass them, so pass IDs and scalars instead. A practical pattern is a JobFailed listener that reads $event->exception->getContext() and dispatches an alert with a stable schema.
API boundary. If you expose error payloads, do not dump raw context to the client. Keep a whitelist:
public function render($request, Throwable $e)
{
if ($e instanceof PaymentFailedException) {
return response()->json([
'message' => 'Payment could not be processed.',
'code' => $e->getContext()['status'] ?? null,
], 422);
}
return parent::render($request, $e);
}
Testing. Context makes assertions much cheaper than message matching:
$this->assertSame($order->id, $caught->getContext()['order_id']);
Build a small helper trait in tests/ to catch-and-return exceptions so you do not repeat try/catch blocks.
withContext() is actually setContext() or the trait is named differently, adapt immediately rather than guessing from the README.failed_jobs payloads. Store IDs and primitives.laravel.log and in whatever log aggregation you ship to. Treat the context array as public-to-operators data and scrub at the source, or add a redaction pass in Handler::report().->take(20) and a 'truncated' => true flag.Handler, Log, or Http\Exceptions. Any rendering, reporting, or redaction behavior is yours to write. That is fine, but budget for it — the package is roughly one trait and one interface, so expect to write the integration yourself.getContext(): array on a class that already extends a vendor exception, you cannot add the trait to a parent — use composition or a ContextualException wrapper instead of an inheritance chain.implements the interface (the instanceof check in Handler::report silently fails otherwise), is report() overridden in the concrete exception or a parent (which skips your handler), and is the exception being caught and swallowed somewhere upstream. dd($e instanceof ContextableExceptionInterface) at the throw site resolves it in seconds.composer.json and consider forking it into app/Support/ if you rely on it heavily — it is ~100 lines, so vendoring removes an unmaintained dependency risk entirely at almost no cost.How can I help you explore Laravel packages today?