Skip to content

Logging

Muhammet Şafak edited this page May 24, 2026 · 1 revision

Logging

DBAL ships two independent logging facilities, each focused on a different use case.

Facility Configured via When entries are written
Query log buffer queryLogs flag Every executed query, success or failure.
Critical logger log credential Only on query failure — receives the failure message.

The query log is an in-memory ring; the critical logger is a pluggable sink (PSR-3 logger, callable, file path, or duck-typed object).

Query log buffer

Enable

$db = new Connection(['queryLogs' => true]);
// or at runtime:
$db->setQueryLogs(true);

Inspect

foreach ($db->getQueryLogs() as $entry) {
    printf("%6.3fs  %s\n", $entry['timer'], $entry['query']);
}

Each entry has the shape:

[
    'query' => 'SELECT id FROM users WHERE id = :id',
    'args'  => ['id' => 7],
    'timer' => 0.000234,   // seconds, 6-decimal precision
]

Caveats

  • The buffer lives in process memory and never rotates. Long-running workers should drain it periodically.
  • Recording is cheap but not free. Leave it off in production unless you are debugging a hot path.
  • The buffer captures bound parameters as PHP values, not the expanded SQL. There is no string-interpolation fallback.

Critical logger

Connection::createLog() is called automatically when a query fails. The message it receives looks like:

SQLSTATE[HY000]: ... no such table: users
SQL : SELECT * FROM users

If debug is true, a JSON dump of the bound parameters is appended:

SQL : SELECT * FROM users WHERE id = :id
PARAMS : {"id":1}

Sink resolution

The log credential is checked at the moment createLog() runs, in this order:

  1. Psr\Log\LoggerInterfacecritical($message, $context) is called with the PSR-3 {placeholder} template left intact.
  2. callable — receives the interpolated message string.
  3. object with a public critical() method — duck-typed fallback for codebases that ship their own logger but don't pull in psr/log.
  4. string — treated as a file path; file_put_contents with FILE_APPEND. The path may contain time placeholders.

If none of the above apply (or no sink is configured), createLog() returns false.

PSR-3 (Monolog and friends)

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$logger = new Logger('db');
$logger->pushHandler(new StreamHandler('php://stderr'));

$db = new \InitORM\DBAL\Connection\Connection([
    'log' => $logger,
    // ...
]);

This is the recommended setup. Install psr/log (it ships in your container's logger anyway) and pass the instance through.

Callable sink

For ad-hoc or test logging:

$messages = [];
$db = new Connection([
    'log' => static function (string $message) use (&$messages): void {
        $messages[] = $message;
    },
]);

The callable receives a single string — placeholders have already been replaced.

File sink with placeholders

$db = new Connection([
    'log' => __DIR__ . '/logs/db-{date}.log',
]);

The path template may contain any of these placeholders:

Token Replaced with
{timestamp} time()
{date} date('Y-m-d')
{datetime} date('Y-m-d-H-i-s')
{year} date('Y')
{month} date('m')
{day} date('d')
{hour} date('H')
{minute} date('i')
{second} date('s')

Each call appends one line.

Duck-typed critical() object

For backwards compatibility with logger classes that predate PSR-3:

class LegacyLogger
{
    public function critical(string $message): void { /* ... */ }
}

$db = new Connection(['log' => new LegacyLogger()]);

Calling it yourself

createLog() is part of the public API. You can use it for your own non-failure events too:

$db->createLog('user {id} signed in', ['id' => 42]);

PSR-3 style {placeholder} substitution is honoured. Context values that are arrays or non-Stringable objects are skipped (their placeholder remains in the message) — this keeps the call defensive against accidental serialisation crashes.

Choosing between the two

Question Use
"What ran during this request?" Query log buffer.
"How fast was each query?" Query log buffer (timer).
"I want every failure in my app log." Critical logger (log cred.).
"I want a per-day file of every query." Both, draining the buffer at end.

What's next

Clone this wiki locally