silpo-tech/exception-handler-bundle
A PHP exception-handler bundle that centralizes error handling, turning thrown exceptions into consistent responses and reducing repetitive try/catch code across your application. It helps standardize logging, error output, and handler configuration in one place.
Before anything else, note the mismatch: despite the -bundle suffix and Symfony conventions, this is a Symfony bundle, not a Laravel package. There is no service provider, no config/exception-handler.php, and no Handler::render() hook. A Laravel developer's first move is to decide whether to (a) port the logic into App\Exceptions\Handler / a custom exception renderer, or (b) skip it and use spatie/laravel-ignition + a Problem Details package. Given the zero-star, zero-dependent, zero-score profile, option (b) is the pragmatic default.
If you are in a Symfony codebase (or evaluating for one):
composer require silpo-tech/exception-handler-bundleconfig/bundles.php (Symfony Flex usually does this automatically):
SilpoTech\ExceptionHandlerBundle\ExceptionHandlerBundle::class => ['all' => true],
DependencyInjection/Configuration.php (config tree) and the EventListener/ or ExceptionHandler/ directory — that is where the exception→response mapping lives. There is no docs site; the source is the documentation.The package's reason for existing is almost certainly standardizing API error responses across Silpo's services — i.e. a JSON error contract — so start by reading the default response shape and mapping table before writing any custom exception classes.
Central mapping over per-controller try/catch. The bundle's value is that controllers throw plain exceptions and a listener converts them. Resist wrapping in try/catch; instead register your exception in the mapping config and let the handler normalize it.
Custom exception hierarchy. Define App\Exception\DomainException (or similar) and have the bundle map that base class to a default status (e.g. 400), then override specific children to 404/409/422. This keeps the mapping table small and declarative.
Config-driven status codes. Typical bundle config:
# config/packages/exception_handler.yaml
exception_handler:
map:
App\Exception\NotFoundException: 404
App\Exception\ValidationException: 422
default_status: 500
debug: '%kernel.debug%'
Treat this file as your error contract — review it in PRs the way you review routes.
Listener priority matters. Symfony's own ErrorListener also handles kernel.exception. If your custom listener runs at the wrong priority you get either an HTML page or a double-render. Check the bundle's getSubscribedEvents() / #[AsEventListener(priority: ...)] and ensure it fires before ErrorListener (default priority is -128; the bundle should be higher).
Pair with a normalizer, not string messages. If the bundle exposes a response factory or serializer hook, feed it a structured payload (code, title, detail, traceId) rather than raw $e->getMessage(). This is what makes the envelope useful to clients.
Log at the boundary, not in the handler. Let the exception handler render; let Monolog/Sentry capture via a separate listener. Mixing them makes it impossible to render a 404 for a NotFoundHttpException without a false-positive error log.
Testing. Use KernelBrowser/WebTestCase and assert on application/problem+json (or whatever content type the bundle emits) plus the status code. Add one test per mapped exception class so the contract is pinned.
Laravel port pattern, if you go that route: reimplement the mapping as a match(true) inside a custom ExceptionRenderer contract implementation, and register it alongside Laravel's default HttpException renderer. You lose the bundle's listeners and get none of its maintenance — which, at zero dependents, you weren't getting anyway.
%kernel.debug%, Symfony typically re-throws or renders a trace page. Verify the bundle respects debug and does not leak stack traces or SQL in production responses. Audit this explicitly — it's the most common bug class in exception bundles.2026-10-23 is in the future relative to any reasonable "now." That suggests either a metadata/feed error, an unpushed tag, or a package that is effectively unmaintained and mis-tagged. Do not assume semantic-versioning stability; pin an exact version and inspect the tag.silpo-tech org makes this a de-facto internal bundle published publicly. Internal bundles frequently hardcode assumptions (logging channels, APP_ENV names, response shapes) that won't match yours.getMessage() for client output. It leaks internals and is not stable across locales. Map exception types to client-facing strings.kernel.exception recursion. If your handler itself throws (e.g. the serializer fails), Symfony falls back to a bare 500. Guard the handler with a try/catch that logs and returns a minimal envelope.response_factory or normalizer service alias in the DI config. If present, override it in services.yaml — that's the cleanest customization seam. If absent, you'll be decorating the listener, which is fragile.How can I help you explore Laravel packages today?