opale/console-exception-bundle
Opale ConsoleExceptionBundle enhances exception handling for Symfony console commands, replacing noisy stack traces with clearer, more readable error output so CLI failures are easier to diagnose and debug. A lightweight integration for Symfony applications.
Read this first: you cannot use this package in a Laravel application. It is a Symfony bundle (Opale\...\ConsoleExceptionBundle), it is archived, its last release was June 2016, and it has zero dependents. Its composer.json will constrain symfony/symfony to the 2.x/3.x line, which directly conflicts with the symfony/console ^5/^6/^7 that illuminate/console requires. Composer will refuse the install, and even if you force it, a Symfony Bundle subclass needs AppKernel::registerBundles() and compiler passes that Laravel has no equivalent for. There is nothing to "get started" with.
What you can get started with is the problem it solves, because that problem is real and still exists in Laravel today.
The problem: Symfony Console catches exceptions thrown inside a command, renders them to stderr, and returns a non-zero exit code — it never reports them. In Laravel, Illuminate\Foundation\Console\Kernel::handle() does call reportException(), but that catch block is rarely the one that fires: Symfony's Application::doRunCommand() swallows the throwable first, renders it, and returns the exit code. So php artisan some:command failing inside handle() frequently produces a pretty terminal message and nothing in storage/logs. That is fatal for cron jobs, scheduled commands, and container entrypoints where stderr goes nowhere.
Where to look first (only if you inherited a legacy Symfony 2/3 app that already uses it): Resources/config/services.xml for the tagged listener service, then the listener class — almost always under Listener/ or EventSubscriber/. The entire package is under ~100 LOC. You are reading it to copy the idea, not to install it.
Your first use case: log every failing Artisan command with command name, exit code, and full stack trace. Confirm the gap exists before you build anything — make a command that throw new RuntimeException('boom'), run it, and check whether storage/logs/laravel.log grew.
The idiomatic Laravel port is a single service provider that attaches a Symfony ConsoleEvents::ERROR listener to the Artisan application's dispatcher. Symfony dispatches ConsoleEvents::ERROR inside doRunCommand()'s catch block before rendering, which is exactly the hook this bundle used.
// app/Providers/ConsoleErrorLoggingProvider.php
use Illuminate\Support\Facades\Log;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\EventDispatcher\EventDispatcher;
public function boot(): void
{
$dispatcher = new EventDispatcher();
$dispatcher->addListener(ConsoleEvents::ERROR, function (ConsoleErrorEvent $event) {
Log::channel('console')->error('Artisan command failed', [
'command' => $event->getCommand()?->getName(),
'exit_code' => $event->getExitCode(),
'exception' => $event->getError(), // object, not getMessage()
]);
});
$this->app->make(\Illuminate\Contracts\Console\Kernel::class)
->getArtisan()
->setDispatcher($dispatcher);
}
Guard it with if ($this->app->runningInConsole()) and verify getArtisan() is reachable in your version before relying on it — the console kernel is bound early, and calling getArtisan() from a provider triggers command discovery.
Patterns worth adopting around it:
introspection processor (enabled by default in the single/daily handlers) expands Throwable into class, file, line, trace, and the full getPrevious() chain. Logging $e->getMessage() — which the old bundle did in some configurations — throws away everything you need.console channel so alerting rules on "anything from the app logger" don't fire for benign command failures (e.g. migrate:status in a health check).$event->getInput() contains every --option and --argument, including --password and --token. Either omit input entirely or filter against a denylist before logging.$this->artisan('boom')->assertExitCodes([1]) plus Log::shouldReceive('error')->once() catches regressions when framework internals shift.Schedule::command(...)->onFailure(fn () => Log::error(...)) (Laravel 10.19+) where it fits — it is more explicit than a global listener and doesn't depend on Symfony event internals.AppKernel, ensure Monolog has a handler with a Psr\Log\LoggerInterface alias, and check the service tag — pre-4.4 it is console.exception, not console.error.console.exception became console.error in Symfony 4.4; the old name survived only as a deprecated alias and is gone in 5.0+. Any fork must target ConsoleEvents::ERROR.\Exception vs \Throwable. 2016-era typehints say \Exception. In PHP 7+/8, TypeError, Error, and OOM-adjacent fatals are \Throwable — which is what actually kills console commands. If you port the listener verbatim, keep the Throwable typehint.ConsoleErrorEvent listener or calling setExitCode(0) tells Symfony the error was handled; the command exits successfully and CI goes green on a broken build. Never touch the exit code unless you genuinely intend to suppress.Kernel::handle() if setCatchExceptions(false) is set. If you see two entries per failure, that's why — pick one hook and stay there.SIGTERM in a queue worker or a killed container bypasses the exception path entirely; log those via pcntl_signal or ConsoleEvents::TERMINATE, not this listener.-vvv changes rendering, not the exception. Verbosity affects the terminal output only. The logged object is identical either way — don't try to "improve" logging by bumping verbosity.app/Support/ behind your own interface rather than adding it to composer.json — you keep patch room, composer update can't break you, and composer audit stays clean.Psr\Log\LoggerInterface and expose channel + level config keys. Keep the listener idempotent (a re-entrant failure inside the logger must not recurse) and wrap the log call in its own try/catch.How can I help you explore Laravel packages today?