firehed/dbal-logger
A drop-in Doctrine DBAL logger that sends SQL queries and execution times to any PSR-3 logger, making database activity and slow queries easy to trace in PHP applications without extra instrumentation.
Why you're here: DBAL 3 deprecated SQLLogger and DBAL 4 removed it, so query logging silently disappeared on upgrade. DBAL's own Doctrine\DBAL\Logging\Middleware does exist, but it only logs the SQL string — no bound parameters, no types, no duration, and no failure signal. This package reimplements the old SQLLogger shape on top of the middleware API, which is the whole point: you get startQuery($sql, $params, $types) / stopQuery() back, plus a debug() hook for parity with Symfony's old DbalLogger.
As of 3.0.0 there is a single logger interface — DbalLogger. The previous QueryLogger / DbalLogger split is gone, all interface methods carry native type declarations, and stopQuery() now receives the exception when a statement fails. That last one closes the "did it throw?" blind spot that made 2.x awkward to instrument. Requirement is now PHP 8.2+ (8.1 dropped). If you're on 2.x, read UPGRADING.md before bumping — the interface rename and the added stopQuery() parameter are source-breaking for any custom logger you wrote.
Install
composer require firehed/dbal-logger:^3.0
The surface is tiny (middleware + DbalLogger interface + a PSR-3 adapter), so read the README end-to-end — it's short — and skim src/ for exact class names and signatures in your pinned version. The README was rewritten for 3.0, so it now actually matches the code.
Register the middleware. It must be attached to the Configuration before the connection is built; setting it afterwards is a no-op.
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\DriverManager;
$config = new Configuration();
$config->setMiddlewares([
new Firehed\DbalLogger\Middleware($logger),
]);
$conn = DriverManager::getConnection($params, $config);
First use case: log every query to a Monolog doctrine channel at debug, with params, and emit failures at warning. If you want something smaller, the second-most-common use case is a test-only logger that collects queries so you can assert "this endpoint issued exactly 3 queries" — and, now, "this endpoint issued 1 query and 1 failure."
1. PSR-3 adapter for day-to-day logging. Wrap your Monolog logger in the package's PSR-3 adapter and log sql, params, types, and duration at debug. Keep it on a dedicated channel so you can enable it per-environment (monolog.channels: ['doctrine']) without drowning in app noise.
2. Custom logger for structured output. Implement DbalLogger yourself when you need JSON lines with duration_ms, rows_affected, connection, outcome (ok/error), and a normalized SQL fingerprint (strip literals, collapse whitespace) so you can GROUP BY in your log aggregator. This is where you get real value over DBAL's built-in middleware. In 3.0 the signatures are natively typed, so your implementation must match exactly — copy the interface verbatim rather than typing from memory.
3. Slow-query gate + failure gate. Track hrtime() in startQuery, compute in stopQuery, and only emit when over a threshold — or when an exception came back. This turns a noisy debug tool into something you can leave on in staging permanently, and it means failures are never sampled away. One implementation handles both branches; don't fork into two loggers.
4. Test assertion logger. A trivial implementation that pushes {sql, params} into an array, combined with an assertQueryCount()/assertQueried('from users') helper. In 3.0 you can also record the exception in stopQuery() and assert assertQueryFailed('from users'), which is much better than asserting on a swallowed exception. Cap the buffer (see gotchas) and clear it in tearDown.
5. Composing with other middleware. Middleware nest. If you're also running a profiler or retry middleware, decide deliberately whether the logger sits inside or outside: inside a retry middleware you'll log each attempt (and now each failed attempt, with its exception); outside you'll log one logical query. Log the same query twice and your dashboards lie — verify the nesting empirically once, then comment it in services.yaml.
6. Symfony wiring.
services:
Firehed\DbalLogger\Middleware:
arguments: ['@App\Doctrine\QueryLogger']
tags: ['doctrine.middleware']
Tagging is supported by DoctrineBundle; no compiler pass needed. Clear the container cache after adding it, or you'll spend twenty minutes debugging a middleware that "isn't firing."
7. Selective logging. Filter inside startQuery (e.g. skip SELECT 1, skip health checks, skip a specific connection by injecting its name). Cheaper than filtering in the log pipeline and keeps volume down at the source. Remember to apply the same filter logic to the failure path, or skipped statements will still surface when they break the request.
8. Decorate, don't fork. The merged interface makes decoration cleaner than it was in 2.x: a TimingLogger wrapping the PSR-3 adapter, plus a FilteringLogger in front of that, gets you 90% of custom needs without touching vendored code. Each decorator implements DbalLogger, forwards to the inner one, and adds exactly one concern.
stopQuery() now receives the exception on failure, so the stale-open-query problem you used to work around is solvable at the source: record start time keyed to a stack and drain it in stopQuery() regardless of outcome. But this is opt-in: if your implementation ignores the new parameter, you get exactly the 2.x behaviour. Test it explicitly — run a query against a missing table and confirm you get a closed entry with the exception attached, not an orphaned open one.QueryLogger no longer exists. Any custom implementation, decorator, or type hint referencing it will fatal on upgrade — including ones buried in test helpers. Grep for QueryLogger before you bump, and check UPGRADING.md for the exact new signature rather than inferring it.json_encode() on a resource yields garbage, and reading a stream that the driver already consumed can throw. Detect is_resource() and log '<stream>' instead.DbalLogger didn't redact either — don't inherit that bug. Redact by key name and by type (Types::BINARY/BLOB) before the record ever leaves the process. This applies doubly to the new failure path: exceptions often carry the offending values in their messages, so scrub $e->getMessage() too if you log it.debug_backtrace() is expensive. If you log a call site per query, gate it behind an env flag — it's often 10–100× the cost of the query itself for simple statements.^3.0 but lock the exact minor, and accept that if the interface changes again you're patching one file. Given the code size, vendoring it into app/Doctrine/ remains defensible for a critical path — the 3.0 breaking change is the argument for it.Configuration object used by the connection; container cache cleared; Monolog channel/level actually permitting debug (and warning/error for the failure path — they're separate levels, so a channel scoped to debug only will drop failures); DBAL 3 vs 4 in composer.lock (the middleware interface moved); that you didn't call setMiddlewares() after DriverManager::getConnection(); and — new in 3.0 — that your logger implements DbalLogger and not the removed QueryLogger.How can I help you explore Laravel packages today?