johndodev/jlog-bundle
johndodev/jlog-bundle is a lightweight logging bundle for PHP applications, making it easy to capture, organize, and review log events with minimal configuration. Integrate structured logging into your project quickly and keep runtime diagnostics simple.
The single most important fact before you write a line: johndodev/jlog-bundle is a Symfony bundle, not a Laravel package. The -bundle suffix is Symfony's convention; it ships a Twig extension and a small JS logging asset so that server-side values are emitted into the browser console instead of halting the response. There is no Laravel service provider, no Blade directive, and no auto-discovery. With 0 stars and 0 dependents, read the source in full before wiring anything up — the value here is the pattern, and it's a few hundred lines at most.
Where to look first, in this order:
Resources/config/services.* — what is actually injectable and how the logger is constructed.Resources/public/js/* — the browser-side logger. Check whether it needs jQuery, how it serializes payloads, and whether it buffers.Minimal steps — if you're genuinely on Symfony (sidecar/admin app):
composer require johndodev/jlog-bundle
# register in config/bundles.php manually — no auto-discovery
bin/console assets:install --symlink
Then dump from a controller or template using the bundle's helper.
Minimal steps — if you're on Laravel (the realistic case): do not install it. Laravel cannot register a Symfony bundle's compiler passes or its Twig extension. Port the idea into ~40 lines instead: a service provider that registers a small JLog singleton with a per-request buffer, a Blade::directive('jlog', ...), and a Vite-compiled JS asset.
// app/Providers/JLogServiceProvider.php
public function register(): void
{
$this->app->singleton(JLog::class, fn () => new JLog(
enabled: (bool) config('jlog.enabled') && $this->app->environment('local', 'staging'),
));
}
public function boot(): void
{
Blade::directive('jlog', fn ($expr) => "<?php app(App\Support\JLog::class)->dump($expr); ?>");
}
{{-- layout.blade.php, before </body> --}}
@if(config('jlog.enabled'))
<script>window.__jlog = @json($jlogBuffer ?? []);</script>
@vite('resources/js/jlog.js')
@endif
First real use case: you're wiring a multi-step form (or any POST → redirect flow) and want to inspect the validated payload without dd()-ing the response. dd() costs you the session, the CSRF token, and the redirect chain. Emitting to the console lets the page render normally and you keep your state.
Dump-and-continue in controllers and middleware. The core workflow is mechanical: wherever you'd reach for dump() or dd(), emit to the console instead. This is disproportionately valuable in POST handlers, form requests, and middleware, where interrupting the response destroys the very thing you're debugging (the redirect, the flashed session, the authenticated state).
Buffer, then flush once per response. Don't emit one <script> block per call site — you'll fight with caching headers and produce unreachable markup. Collect into a request-scoped buffer and render it in your layout. This also gives you an obvious place to redact.
Gate hard on environment and config. Two independent switches (config('jlog.enabled') and app()->environment(...)) means a stray .env copy can't leak payloads to production. Pair it with a route-level or user-level allowlist if the app is internet-facing even in staging.
Use it for the browser, use Log for the record. This is a developer-console tool, not an observability channel. Anything you need to keep, alert on, or correlate belongs in Log::channel(...) / Monolog. Treat the console emitter as ephemeral and deletable.
Serialize deliberately. Pass ->toArray() or an explicit subset of an Eloquent model rather than the model itself — the JS serializer will choke on cycles, lazy-loaded relations, and internal attributes you didn't mean to expose.
Integration tip — Vite: ship the JS as a real entry (resources/js/jlog.js) rather than copying the bundle's raw asset into public/. That way it gets HMR in dev and a content-hashed build in production, and you don't accumulate a stale vendor file.
Extension points worth copying: the serializer (so you can teach it about your domain objects), the payload redactor (so password, token, ssn are scrubbed before they ever hit window), and the config tree (so enabling a named channel is a config change, not a code change).
Don't composer require this into a Laravel app and hope. The install may resolve fine — Laravel already depends on several Symfony components — but nothing will register it. Worse, a version constraint mismatch against Laravel's pinned Symfony components is a realistic failure mode that surfaces at composer update time, far from where you caused it.
Twig ≠ Blade. The extension's helper is not available in .blade.php files. There's no adapter worth writing; the port is smaller than the adapter.
Redirects will swallow your logs. If you emit into a response you're about to return redirect(), the browser never renders it. Either buffer into the session as a flash and emit on the next GET, or restrict emission to 200 responses in a terminable middleware.
Escape the payload. Dumping request input into a <script> block is a reflected-XSS vector in the making. Use @json(...) (or json_encode with JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT), never raw {!! !!}.
Octane / Horizon / queue workers will leak your buffer. A static array on a long-lived worker accumulates across requests. Reset it in a middleware handle() on the way in, and never let the buffer live on a singleton without a per-request reset.
Check the JS asset actually loaded before debugging your PHP. When nothing appears in the console: confirm window.__jlog exists (Network tab), confirm the asset published/compiled, and confirm the bundle's JS isn't silently requiring jQuery you never loaded. This burns more time than the integration itself.
Zero upstream support means zero upstream fixes. Pin an exact version, and if you adopt (rather than port), fork it under your org, wrap it behind an internal interface, and assign an owner. Do not let call sites depend on the package directly.
Debugging checklist, in order: bundle/asset registered → config enabled → environment matches → payload reaches the buffer → @json rendered → JS asset compiled → console open on the right frame (iframes hide console output).
The honest exit criterion: if after a one-day spike you can't articulate what this gives you that Log::debug() + a Blade::directive shim doesn't, delete the spike branch. For nearly every Laravel logging gap, the native stack closes it in less code than the integration.
How can I help you explore Laravel packages today?