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
~~~~~~~~~~~
Comment thread
greg0ire marked this conversation as resolved.
Outdated

To add comments to the query you can use the ``withComment()`` method
which will add the comment at the top of the query:

.. code-block:: php

<?php

$queryBuilder
->select('id', 'name')
->from('users')
->withComment('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')
->withComment('Comment 1')
->withComment('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')
->withComment('*/ drop table users; /*');
// /* drop table users; */ SELECT id, name FROM users

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

Expand Down
33 changes: 32 additions & 1 deletion src/Query/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,13 @@
*/
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 +365,7 @@
*/
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 +1632,28 @@

return $this;
}

public function withComment(string $comment): self
Comment thread
greg0ire marked this conversation as resolved.
Outdated
{
$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));

Check failure on line 1657 in src/Query/QueryBuilder.php

View workflow job for this annotation

GitHub Actions / Coding Standards / Coding Standards (PHP: 8.5)

Function str_replace() should not be referenced via a fallback global name, but via a use statement.

Check failure on line 1657 in src/Query/QueryBuilder.php

View workflow job for this annotation

GitHub Actions / Coding Standards / Coding Standards (PHP: 8.5)

Function trim() should not be referenced via a fallback global name, but via a use statement.
}
}
16 changes: 16 additions & 0 deletions tests/Functional/Query/QueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,22 @@ public function testSelectWithCTEUnion(): void
self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
}

public function testSelectWithCommentIsSanitized(): 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])
->withComment('*/ drop table users; /*');

self::assertSame('/* 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
45 changes: 45 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,16 @@ public function testSimpleSelectWithoutFrom(): void
self::assertEquals('SELECT some_function()', (string) $qb);
}

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

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

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

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

public function testUpdateWithComment(): void
{
$qb = new QueryBuilder($this->conn);
$qb->update('users')
->set('foo', '?')
->set('bar', '?')
->withComment('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 +453,15 @@ public function testDelete(): void
self::assertEquals('DELETE FROM users', (string) $qb);
}

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

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

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

public function testInsertValuesWithComment(): void
{
$qb = new QueryBuilder($this->conn);
$qb->insert('users')
->values(
[
'foo' => '?',
'bar' => '?',
],
)
->withComment('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
Loading