rcsofttech/console-profiler-bundle
Console Profiler Bundle adds a Symfony web profiler panel for console commands, capturing arguments, input, output, execution time, memory usage, and exceptions. Inspect CLI command runs directly in the profiler to debug performance and failures faster.
The bundle is small (16 stars, MIT, single-digit thousand lines at most), so the fastest orientation is the source, not the docs. Read in this order:
README.md — install line and the dashboard command name.src/ConsoleProfilerBundle.php (or similarly named bundle class) — confirms it is a standard Symfony bundle, i.e. Symfony\Component\HttpKernel\Bundle\Bundle.Symfony\Component\Console\ConsoleEvents::COMMAND, ::TERMINATE, and possibly ::ERROR. This is the entire mechanism: start a stopwatch on COMMAND, record duration/peak memory/exit code on TERMINATE, then let a separate TUI process poll and render.Terminal clear), where it reads state from (file, cache pool, in-memory).Install in a Symfony app:
composer require rcsofttech/console-profiler-bundle
With Symfony Flex the bundle is enabled automatically. Without Flex, register it manually in config/bundles.php:
return [
// ...
Rcsofttech\ConsoleProfilerBundle\ConsoleProfilerBundle::class => ['dev' => true, 'test' => true],
];
Check composer.json constraints first — this targets Symfony 6/7 console components; on a Symfony 5 app Composer will refuse or you will get a runtime ConsoleEvents mismatch.
First use case: open two panes. In pane A run your slowest command (bin/console app:import --env=dev), in pane B run the profiler command from the README. You should see duration, memory, and exit status update without touching a single line of your command code.
Be clear-eyed: this is a Symfony bundle and Laravel does not boot bundles. There is no config/bundles.php in Laravel and no Bundle base class. You cannot composer require this into a Laravel app and expect it to appear in php artisan. Two realistic routes:
Route A — use it where it runs. Any Symfony service, internal tool, or Composer package you maintain that ships against symfony/console can use it directly. This is the day-to-day win if you straddle both ecosystems.
Route B — port the 30-line pattern. Laravel's Artisan is Symfony Console, and Laravel exposes exactly the hooks this bundle builds on:
// app/Providers/ConsoleProfilerServiceProvider.php
use Illuminate\Console\Events\CommandStarting;
use Illuminate\Console\Events\CommandFinished;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Cache;
public function boot(): void
{
if (! $this->app->isLocal()) {
return;
}
Event::listen(CommandStarting::class, function (CommandStarting $e) {
Cache::put("profiler:{$e->command}", [
'start' => microtime(true),
'mem' => memory_get_usage(true),
], now()->addHour());
});
Event::listen(CommandFinished::class, function (CommandFinished $e) {
$start = Cache::pull("profiler:{$e->command}");
if (! $start) return;
Cache::put("profiler:last:{$e->command}", [
'duration_ms' => (microtime(true) - $start['start']) * 1000,
'peak_mb' => round(memory_get_peak_usage(true) / 1048576, 2),
'exit_code' => $e->exitCode,
'finished_at' => now()->toIso8601String(),
], now()->addHour());
});
}
Then a php artisan profiler:watch command that loops, reads the cache keys, and renders with Termwind or Laravel\Prompts. That is the same architecture as the bundle, minus the Symfony dependency.
Observer, never inline. The dashboard and the profiled command must be separate OS processes. Launching the TUI from inside your command fights for the same TTY and will garble output. Standard workflow: tmux split, iTerm/Windows Terminal split pane, or two SSH sessions.
Zero instrumentation. Because the bundle hooks console events, existing commands are profiled without edits. This is the main argument for it over hand-rolled microtime() calls sprinkled through handle(). Treat any command that needs --profile flags as a smell — the event listener should be sufficient.
Gate it to dev. In Symfony, put the bundle class in ['dev' => true, 'test' => true] in config/bundles.php, and wrap its config in a when@dev: block in config/packages/. The listener overhead is small but non-zero, and you do not want a file-writing subscriber in production.
Split the storage from the renderer. If the bundle exposes a storage/repository class (look for a Storage, Repository, or State directory), you can:
/tmp or var/profiler/ and share it across containers,--no-ansi),CI pattern. TUI is interactive-only. For pipeline visibility, run the command and have a small wrapper dump json_encode of the collected metrics, then assert thresholds:
bin/console app:import --no-ansi --no-interaction
php -r 'exit(json_decode(file_get_contents("var/profiler/last.json"), true)["duration_ms"] > 30000 ? 1 : 0);'
Laravel-side integration. If you standardise on Route B above, register it in a provider that only boots in local — $this->app->isLocal() — and store in Cache::store('file') or the database rather than Redis, so php artisan cache:clear does not wipe a run mid-flight.
Bundle ≠ ServiceProvider. The number one mistake a Laravel developer makes here is adding the bundle class to config/app.php's providers array. It is not a provider; it has no register()/boot() contract and will fatal. Same for trying to composer require it into Laravel and expecting artisan to see the commands.
Short commands may never appear. The dashboard polls. A command finishing in under one refresh interval may be overwritten before you look. Either lower the refresh interval or — better — make sure the storage is append-only keyed by command name, so you see the last run rather than only the currently-running one.
SIGINT / Ctrl+C. ConsoleEvents::TERMINATE fires on normal exit and on exceptions, but signal-terminated processes on some PHP builds bypass it. If your dashboard shows a command "running" forever, that is the cause. The listener typically registers pcntl_signal(SIGINT, ...) — check the source; if not, that is a legitimate patch to submit.
Memory numbers mislead. memory_get_peak_usage(true) reports the allocator's arena (what the OS gave PHP), not what your objects actually hold. memory_get_peak_usage(false) is closer to "useful" but still includes opcache and framework bootstrap. When comparing commands, use the delta from CommandStarting, not the absolute peak.
TTY and ANSI. Any --no-ansi, --no-interaction, or piped output will change what the dashboard can render. If colours or redraws look broken, check stream_isatty(STDOUT) and the TERM variable; inside Docker without -t, the TUI degrades badly.
Concurrency. Two commands running at once will race on the same storage key. If you routinely run parallel imports (--queue, &, Process::pool), verify the storage is keyed by PID or command instance, not just command name, before trusting the dashboard.
Extension points to look for. In a repo this size there are usually one or two seams: a collector interface you can implement for custom metrics, or a configuration key for the storage path/refresh rate. If neither exists, the event subscriber is a plain class — decorating or replacing it is a five-minute fork, and the MIT licence makes upstreaming a PR trivial.
Version drift. Release date is 2026-03-25 and the package has 0 dependents, so nothing else is pinning it. Treat minor bumps as potentially breaking and pin an exact version in composer.json until you've read the diff.
How can I help you explore Laravel packages today?