Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions docs/en/cookbook/dql-custom-walkers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ Now this is all awfully technical, so let me come to some use-cases
fast to keep you motivated. Using walker implementation you can for
example:


- Modify the Output walker to get the raw SQL via ``Query->getSQL()``
with interpolated parameters.
- Modify the AST to generate a Count Query to be used with a
paginator for any given DQL query.
- Modify the Output Walker to generate vendor-specific SQL
Expand All @@ -50,7 +51,7 @@ example:
- Modify the Output walker to pretty print the SQL for debugging
purposes.

In this cookbook-entry I will show examples of the first two
In this cookbook-entry I will show examples of the first three
points. There are probably much more use-cases.

Generic count query for pagination
Expand Down Expand Up @@ -215,3 +216,38 @@ huge benefits with using vendor specific features. This would still
allow you write DQL queries instead of NativeQueries to make use of
vendor specific features.

Modifying the Output Walker to get the raw SQL with interpolated parameters
---------------------------------------------------------------------------

Sometimes we may want to log or trace the raw SQL being generated from its DQL,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should elaborate here. I think your need is to get a SQL statement that you can replay yourself, with an SQL client. If so, please say so explicitly and clearly.

Copy link
Contributor Author

@n0099 n0099 Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me there's two main usage of raw SQL: log the SQL being executed to debug possible slow queries in future, and UNION results of multiple DQL with WHERE as only DBAL query supports UNION as in https://stackoverflow.com/questions/4155288/how-to-write-union-in-doctrine-2-0/79647475#79647475 and https://github.com/n0099/open-tbm/blob/c960f09242097937403880a6ebd02ef4946ce3a8/be/src/PostsQuery/QueryResult.php#L219-L237.

Copy link
Contributor Author

@n0099 n0099 Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Sometimes we may want to log or trace the raw SQL being generated from its DQL,
Sometimes we may want to log or trace the raw SQL being generated from its DQL
for profiling slow queries in the future or audit queries that changed many rows

``$query->getSQL()`` will give us the prepared statement being passed to database
with all values of SQL parameters being replaced by positional ``?`` or named ``:name``
as parameters are interpolated into prepared statements by the database while executing the SQL.
``$query->getParameters()`` will give us details about SQL parameters that we've provided.
So we can create an output walker to interpolate all SQL parameters that will be
passed into prepared statement in PHP before database handle them internally:

.. literalinclude:: dql-custom-walkers/InterpolateParametersSQLOutputWalker.php
:language: php

Then you may get the raw SQL with this output walker:

.. code-block:: php

<?php
$query
->where('t.int IN (:ints)')->setParameter(':ints', [1, 2])
->orWhere('t.string IN (?0)')->setParameter(0, ['3', '4'])
->orWhere("t.bool = ?1")->setParameter('?1', true)
->orWhere("t.string = :string")->setParameter(':string', 'ABC')
->setHint(\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER, InterpolateParametersSQLOutputWalker::class)
->getSQL();

The where clause of the returned SQL should be like:

.. code-block:: sql

WHERE t0_.int IN (1, 2)
OR t0_.string IN ('3', '4')
OR t0_.bool = 1
OR t0_.string = 'ABC'
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Types\BooleanType;
use Doctrine\DBAL\Types\Exception\ValueNotConvertible;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Query\AST;
use Doctrine\ORM\Query\SqlOutputWalker;

class InterpolateParametersSQLOutputWalker extends SqlOutputWalker
{
/** {@inheritdoc} */
public function walkInputParameter(AST\InputParameter $inputParam): string
{
$parameter = $this->getQuery()->getParameter($inputParam->name);
if ($parameter === null) {
return '?';
}

$value = $parameter->getValue();
/** @var ParameterType|ArrayParameterType|int|string $typeName */
/** @see \Doctrine\ORM\Query\ParameterTypeInferer::inferType() */
$typeName = $parameter->getType();
$platform = $this->getConnection()->getDatabasePlatform();
$processParameterType = static fn(ParameterType $type) => static fn($value): string =>
(match ($type) { /** @see Type::getBindingType() */
ParameterType::NULL => 'NULL',
ParameterType::INTEGER => $value,
ParameterType::BOOLEAN => (new BooleanType())->convertToDatabaseValue($value, $platform),
ParameterType::STRING, ParameterType::ASCII => $platform->quoteStringLiteral($value),
default => throw new ValueNotConvertible($value, $type->name)
});

if (is_string($typeName) && Type::hasType($typeName)) {
return Type::getType($typeName)->convertToDatabaseValue($value, $platform);
}
if ($typeName instanceof ParameterType) {
return $processParameterType($typeName)($value);
}
if ($typeName instanceof ArrayParameterType && is_array($value)) {
$type = ArrayParameterType::toElementParameterType($typeName);
return implode(', ', array_map($processParameterType($type), $value));
}

throw new ValueNotConvertible($value, $typeName);
}
}