forci/catchable-bundle
Symfony bundle for Forci Catchable that brings catchable exception handling to PHP applications. It integrates with Symfony’s error and exception workflow, helping centralize failure handling, reduce repetitive try/catch blocks, and keep controllers clean and consistent.
Before anything else, confirm what you're actually holding. forci/catchable-bundle is a Symfony bundle, not a Laravel package. There is no extra.laravel auto-discovery, no service provider, and no config/catchable.php to publish. If you're on Laravel, "getting started" means deciding between two paths: read the source as a design reference (recommended), or stop entirely. If you're on a Symfony app, the steps below apply directly.
If you're on Symfony:
composer.json first. A 0-star bundle with 0 dependents is frequently pinned to an older Symfony major. Check the require block for symfony/framework-bundle, php, and the doctrine/annotations version before you run anything. If it wants Symfony 3/4 while you're on 6/7, the install will fail or drag in a conflicting graph — resolve that before writing a line of code.composer require forci/catchable-bundle. With Flex it may auto-register via the bundle's Resources/config/bundles.php recipe; otherwise add the FQCN (typically Forci\Bundle\CatchableBundle\CatchableBundle) to config/bundles.php. Copy the class name from the repo — don't guess it.src/DependencyInjection/Configuration.php (the config tree and alias) → src/Resources/config/*.yml (service definitions) → any EventSubscriber/EventListener classes (this is where it hooks kernel.exception). Verify the alias resolves with bin/console debug:config <alias> and the wiring with bin/console debug:container --tag=kernel.event_subscriber.OrderNotFound) to a structured 404/422 JSON response. Run that one endpoint, throw the exception deliberately, and confirm the response shape before scaling to other controllers.If you're on Laravel: treat this as a reference implementation of a pattern you'll re-implement in ~30 lines. The equivalent surface in Laravel is Illuminate\Foundation\Exceptions\Handler::renderable() / reportable(), plus render() methods on exception classes. Read src/ for the mapping idea, then close the tab.
The core pattern. The bundle lets you attach exception→response mappings to a controller class or individual action, so the HTTP concern lives next to the code that throws, not in a global handler. Per-class defaults, per-method overrides, and a fallback handler for anything unmatched. Internally it's an event listener on Symfony's exception event that inspects the target controller and dispatches to the mapped handler.
Day-to-day workflow (Symfony):
Response objects or status codes inside your domain layer; let the mapping layer translate. That separation is the main thing the bundle buys you over scattered try/catch.Porting the pattern to Laravel. If you've read the source and want the behaviour, replicate it with native hooks:
// app/Exceptions/Handler.php
public function register()
{
$this->renderable(function (OrderNotFoundException $e, $request) {
return $request->expectsJson()
? response()->json(['error' => 'order_not_found'], 404)
: back()->with('error', 'Order not found');
});
$this->renderable(function (Throwable $e, $request) {
report($e); // let your existing reporter see it
return response()->json(['error' => 'server_error'], 500);
});
}
Layer reportable() for the collection side (log channel, queue job, Sentry) and you've reproduced the bundle's surface area with framework-native, upgrade-safe code. If your exception classes are yours, Laravel also lets each exception define its own render() — that's the closest analogue to the annotation approach and keeps errors close to their definition.
Integration tips regardless of framework:
error, code, optional meta) so clients don't branch on shape.Framework fit is the headline gotcha. Symfony bundles register through Kernel::registerBundles() and the DI extension lifecycle, none of which executes under Laravel's service provider boot. The advertised capability relies on Symfony's kernel.exception event, which has no direct analogue in Laravel's handler pipeline. Any adoption on Laravel is a rewrite, not a configuration step. Budget it as such or skip it.
Licensing blocks production use. The license reads NOASSERTION. That is not a grant to use, modify, or redistribute. Do not put this on a production path or copy code out of it until someone with authority resolves the terms — for a repo this inactive, that resolution may never come.
Zero adoption means you own every bug. No upstream, no issue history, no CVE path, no one watching the code that runs on every request. The exception path is exactly where you least want an unmaintained dependency.
Recursive failure is the classic trap. If a handler throws while handling, you can mask the original exception or loop. Wrap handler execution in a try/catch that degrades silently to the framework's native handler. This is the single most important thing to verify when testing.
Silent swallowing looks like stability. A catch-all that converts everything to a tidy response and never reports it will make your error dashboards go quiet. That's the worst outcome — it looks like improvement while regressions leak. Always report before responding.
PII and secrets land in exception messages. Request bodies, headers, and cookies routinely end up in traces and messages. Decide scrubbing and retention before you persist anything; the bundle does not solve this for you.
Config quirks to check. Find the config alias in Configuration.php and confirm it with bin/console debug:config <alias>. Verify precedence between class-level and method-level mappings by throwing the same exception from both — don't assume method wins. Confirm the listener is actually tagged and registered; a bundle that installs cleanly can still be functionally inert if the compiler pass doesn't run.
Composer resolution drift. Abandoned bundles pin old Symfony/PHP constraints and quietly freeze your upgrade path. If you vendor a fork, modernise it for PHP 8.2+ first — check for create_function, each(), and other removed constructs before assuming it runs.
Debugging tip. Temporarily dump the resolved handler inside the listener (dd-equivalent or dump + die) on a single route to confirm which mapping fired. Far faster than guessing from the response body.
Extension points that actually matter. The fallback handler and per-class overrides are the two surfaces worth configuring. Everything else is small enough to read in under an hour — which is the honest case for just re-implementing the pattern natively rather than inheriting the dependency.
How can I help you explore Laravel packages today?