Skip to content
43 changes: 43 additions & 0 deletions docs/en/reference/query-builder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,49 @@ Multiple CTEs can be defined by calling the with method multiple times.

Values of parameters used in a CTE should be defined in the main QueryBuilder.

Comments
~~~~~~~~

To add comments to the query, use the ``addComment()`` method which
will add the comment before the query:

.. code-block:: php

<?php

$queryBuilder
->select('id', 'name')
->from('users')
->addComment('This is a comment');
// /* This is a comment */ SELECT id, name FROM users

Multiple comments can be added by calling the method multiple times:

.. code-block:: php

<?php

$queryBuilder
->select('id', 'name')
->from('users')
->addComment('Comment 1')
->addComment('Comment 2');
// /* Comment 1 */ /* Comment 2 */ SELECT id, name FROM users

Comments containing ``/*`` and ``*/`` will be sanitized in order to prevent
comment injection. Each occurrence of aforementioned tokens will be replaced
by an empty string and trimmed.

.. code-block:: php

<?php

$queryBuilder
->select('id', 'name')
->from('users')
->addComment('*/ drop table users; /*');
// /* drop table users; */ SELECT id, name FROM users

Building Expressions
--------------------

Expand Down
35 changes: 34 additions & 1 deletion src/Query/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
use function implode;
use function is_object;
use function sprintf;
use function str_replace;
use function substr;
use function trim;

/**
* QueryBuilder class is responsible to dynamically create SQL queries.
Expand Down Expand Up @@ -170,6 +172,13 @@ class QueryBuilder
*/
private array $commonTableExpressions = [];

/**
* Comments that will be added to the query.
*
* @var string[]
*/
private array $comments = [];

/**
* The query cache profile used for caching results.
*/
Expand Down Expand Up @@ -358,7 +367,7 @@ public function executeStatement(): int|string
*/
public function getSQL(): string
{
return $this->sql ??= match ($this->type) {
return $this->sql ??= $this->getComments() . match ($this->type) {
QueryType::INSERT => $this->getSQLForInsert(),
QueryType::DELETE => $this->getSQLForDelete(),
QueryType::UPDATE => $this->getSQLForUpdate(),
Expand Down Expand Up @@ -1625,4 +1634,28 @@ public function disableResultCache(): self

return $this;
}

public function addComment(string $comment): self
{
$this->comments[] = $this->sanitizeComment($comment);

$this->sql = null;

return $this;
}

private function getComments(): string
{
$comments = '';
foreach ($this->comments as $comment) {
$comments .= sprintf('/* %s */ ', $comment);
}

return $comments;
}

private function sanitizeComment(string $comment): string
{
return trim(str_replace(['*/', '/*'], '', $comment));
}
}
20 changes: 20 additions & 0 deletions tests/Functional/Query/QueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,26 @@ public function testSelectWithCTEUnion(): void
self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
}

public function testSelectAddCommentIsSanitized(): void
{
$expectedRows = $this->prepareExpectedRows([['id' => 1], ['id' => 2]]);
$qb = $this->connection->createQueryBuilder();

$select = $qb
->select('id')
->from('for_update')
->where('id IN (?, ?)')
->setParameters([1, 2], [ParameterType::INTEGER, ParameterType::INTEGER])
->addComment('Test comment')
->addComment('*/ drop table users; /*');

self::assertSame(
'/* Test comment */ /* drop table users; */ SELECT id FROM for_update WHERE id IN (?, ?)',
$select->getSQL(),
);
self::assertSame($expectedRows, $select->executeQuery()->fetchAllAssociative());
}

public function testPlatformDoesNotSupportCTE(): void
{
if ($this->platformSupportsCTEs()) {
Expand Down
66 changes: 66 additions & 0 deletions tests/Query/QueryBuilderTest.php
Comment thread
greg0ire marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,37 @@ public function testSimpleSelectWithoutFrom(): void
self::assertEquals('SELECT some_function()', (string) $qb);
}

public function testSimpleSelectAddComment(): void
{
$qb = new QueryBuilder($this->conn);

$qb->select('some_function()')
->addComment('Test comment');

self::assertEquals('/* Test comment */ SELECT some_function()', (string) $qb);
}

public function testSimpleSelectAddMultilineComment(): void
{
$qb = new QueryBuilder($this->conn);

$qb->select('some_function()')
->addComment(
<<<'COMMENT'
This is a multiline comment.
It can span multiple lines.
COMMENT,
);

self::assertEquals(
<<<'SQL'
/* This is a multiline comment.
It can span multiple lines. */ SELECT some_function()
SQL,
(string) $qb,
);
}

public function testSimpleSelect(): void
{
$qb = new QueryBuilder($this->conn);
Expand Down Expand Up @@ -414,6 +445,17 @@ public function testUpdate(): void
self::assertEquals('UPDATE users SET foo = ?, bar = ?', (string) $qb);
}

public function testUpdateAddComment(): void
{
$qb = new QueryBuilder($this->conn);
$qb->update('users')
->set('foo', '?')
->set('bar', '?')
->addComment('Test comment');

self::assertEquals('/* Test comment */ UPDATE users SET foo = ?, bar = ?', (string) $qb);
}

public function testUpdateWhere(): void
{
$qb = new QueryBuilder($this->conn);
Expand All @@ -432,6 +474,15 @@ public function testDelete(): void
self::assertEquals('DELETE FROM users', (string) $qb);
}

public function testDeleteAddComment(): void
{
$qb = new QueryBuilder($this->conn);
$qb->delete('users');
$qb->addComment('Test comment');

self::assertEquals('/* Test comment */ DELETE FROM users', (string) $qb);
}

public function testDeleteWhere(): void
{
$qb = new QueryBuilder($this->conn);
Expand All @@ -455,6 +506,21 @@ public function testInsertValues(): void
self::assertEquals('INSERT INTO users (foo, bar) VALUES(?, ?)', (string) $qb);
}

public function testInsertValuesAddComment(): void
{
$qb = new QueryBuilder($this->conn);
$qb->insert('users')
->values(
[
'foo' => '?',
'bar' => '?',
],
)
->addComment('Test comment');

self::assertEquals('/* Test comment */ INSERT INTO users (foo, bar) VALUES(?, ?)', (string) $qb);
}

public function testInsertReplaceValues(): void
{
$qb = new QueryBuilder($this->conn);
Expand Down