Doctrine
SQL
Prepared Statements
Debugging
PHP

Doctrine - How to print out the real sql, not just the prepared statement?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

In Doctrine, query logging usually shows SQL with placeholders plus a separate parameter array. That is correct for execution, but it can be hard to read when debugging complex filters. The practical workflow is to capture SQL, parameters, and types together, then build a debug-only interpolated representation for human inspection.

Why You See Placeholders Instead of Final SQL

Doctrine uses prepared statements for safety and performance. A query such as WHERE status = ? is compiled once, then parameter values are bound separately. Because binding happens at execution, the logger naturally prints placeholders.

With QueryBuilder, the three key pieces are:

  • SQL text from getSQL()
  • parameter values from getParameters()
  • parameter types from each Parameter object

You need all three to understand what the database actually receives.

Capture SQL, Parameters, and Types

This service-style snippet logs the exact query parts for troubleshooting.

php
1<?php
2
3use Doctrine\ORM\EntityManagerInterface;
4
5final class UserRepositoryDebugHelper
6{
7    public function dumpQuery(EntityManagerInterface $em): void
8    {
9        $qb = $em->createQueryBuilder();
10        $qb->select('u')
11           ->from('App\Entity\User', 'u')
12           ->where('u.status = :status')
13           ->andWhere('u.createdAt >= :fromDate')
14           ->setParameter('status', 'active')
15           ->setParameter('fromDate', new DateTimeImmutable('2026-01-01'));
16
17        $query = $qb->getQuery();
18
19        echo "SQL: " . $query->getSQL() . PHP_EOL;
20        foreach ($query->getParameters() as $parameter) {
21            echo sprintf(
22                "name=%s value=%s type=%s" . PHP_EOL,
23                $parameter->getName(),
24                is_scalar($parameter->getValue()) ? (string) $parameter->getValue() : get_debug_type($parameter->getValue()),
25                (string) $parameter->getType()
26            );
27        }
28    }
29}

This is usually enough to resolve wrong filters, timezone issues, or type mismatches.

Build an Interpolated Debug String Safely

Sometimes you still want one string that looks like final SQL for logs. Do this only for debugging and never execute the interpolated string.

php
1<?php
2
3function interpolateSql(string $sql, array $params): string
4{
5    foreach ($params as $key => $value) {
6        $replacement = is_numeric($value) ? (string) $value : "'" . addslashes((string) $value) . "'";
7        $sql = preg_replace('/\?/', $replacement, $sql, 1);
8    }
9    return $sql;
10}
11
12$sql = 'SELECT * FROM users WHERE status = ? AND age > ?';
13$params = ['active', 18];
14
15echo interpolateSql($sql, $params) . PHP_EOL;

This representation is approximate, but very useful during diagnostics.

Use SQL Logger and Profilers

Doctrine DBAL supports SQL logging integrations, and frameworks such as Symfony can expose executed queries in a profiler panel. These tools show timing and bind values with minimal custom code.

If performance matters, pair SQL capture with EXPLAIN in your database shell. Seeing bound values and execution plans together often reveals missing indexes or poor filter order.

Practical Workflow in a Request Debug Session

A reliable request-level workflow is to assign a correlation identifier, log query parts with that identifier, and inspect only the failing request trace. This keeps logs readable when many requests run concurrently.

php
1$requestId = bin2hex(random_bytes(4));
2error_log("[$requestId] SQL: " . $query->getSQL());
3
4foreach ($query->getParameters() as $parameter) {
5    error_log(sprintf(
6        "[%s] param %s=%s",
7        $requestId,
8        $parameter->getName(),
9        json_encode($parameter->getValue())
10    ));
11}

With this pattern, support engineers can reconstruct one failing query path quickly without enabling global verbose logging.

Common Pitfalls

A common pitfall is assuming interpolated debug SQL is byte-for-byte identical to driver execution. Database drivers apply quoting and type handling internally, so your debug string is only a diagnostic approximation.

Another issue is logging sensitive values such as emails, tokens, or personal data. Always redact or mask sensitive fields before writing logs.

Timezone conversion mistakes are also frequent with date parameters. Log normalized values in UTC when investigating date filters.

Finally, avoid enabling verbose SQL logging permanently in production. It can increase overhead and leak internal details.

Summary

  • Doctrine shows placeholders by design because it uses prepared statements.
  • For reliable debugging, log SQL text, parameter values, and parameter types together.
  • Interpolated SQL strings are helpful for reading but should never be executed.
  • Use profiler tooling for timing and query visibility.
  • Redact sensitive values and keep heavy logging scoped to diagnostics.

Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design