Skip to content

Commit

Permalink
Merge pull request #11528 from norkunas/namedparams
Browse files Browse the repository at this point in the history
Add `createNamedParameter` to `QueryBuilder`
  • Loading branch information
greg0ire authored Jul 11, 2024
2 parents 4c2f104 + 0983d3a commit 1281707
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 0 deletions.
18 changes: 18 additions & 0 deletions docs/en/reference/query-builder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -611,3 +611,21 @@ same query of example 6 written using
->add('from', new Expr\From('User', 'u'))
->add('where', new Expr\Comparison('u.id', '=', '?1'))
->add('orderBy', new Expr\OrderBy('u.name', 'ASC'));
Binding Parameters to Placeholders
----------------------------------

It is often not necessary to know about the exact placeholder names when
building a query. You can use a helper method to bind a value to a placeholder
and directly use that placeholder in your query as a return value:

.. code-block:: php
<?php
// $qb instanceof QueryBuilder
$qb->select('u')
->from('User', 'u')
->where('u.email = ' . $qb->createNamedParameter($userInputEmail))
;
// SELECT u FROM User u WHERE email = :dcValue1
42 changes: 42 additions & 0 deletions src/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ class QueryBuilder implements Stringable

protected int $lifetime = 0;

/**
* The counter of bound parameters.
*
* @var int<0, max>
*/
private int $boundCounter = 0;

/**
* Initializes a new <tt>QueryBuilder</tt> that uses the given <tt>EntityManager</tt>.
*
Expand Down Expand Up @@ -1336,6 +1343,41 @@ public function resetDQLPart(string $part): static
return $this;
}

/**
* Creates a new named parameter and bind the value $value to it.
*
* The parameter $value specifies the value that you want to bind. If
* $placeholder is not provided createNamedParameter() will automatically
* create a placeholder for you. An automatic placeholder will be of the
* name ':dcValue1', ':dcValue2' etc.
*
* Example:
* <code>
* $qb = $em->createQueryBuilder();
* $qb
* ->select('u')
* ->from('User', 'u')
* ->where('u.username = ' . $qb->createNamedParameter('Foo', Types::STRING))
* ->orWhere('u.username = ' . $qb->createNamedParameter('Bar', Types::STRING))
* </code>
*
* @param ParameterType|ArrayParameterType|string|int|null $type ParameterType::*, ArrayParameterType::* or \Doctrine\DBAL\Types\Type::* constant
* @param non-empty-string|null $placeholder The name to bind with. The string must start with a colon ':'.
*
* @return non-empty-string the placeholder name used.
*/
public function createNamedParameter(mixed $value, ParameterType|ArrayParameterType|string|int|null $type = null, string|null $placeholder = null): string
{
if ($placeholder === null) {
$this->boundCounter++;
$placeholder = ':dcValue' . $this->boundCounter;
}

$this->setParameter(substr($placeholder, 1), $value, $type);

return $placeholder;
}

/**
* Gets a string representation of this QueryBuilder which corresponds to
* the final DQL query being constructed.
Expand Down
41 changes: 41 additions & 0 deletions tests/Tests/ORM/QueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Expr\Join;
Expand Down Expand Up @@ -1285,4 +1286,44 @@ public function testDeleteWithoutAlias(): void
$this->expectExceptionMessage('Doctrine\ORM\QueryBuilder::delete(): The alias for entity Doctrine\Tests\Models\CMS\CmsUser u must not be omitted.');
$qb->delete(CmsUser::class . ' u');
}

public function testCreateNamedParameter(): void
{
$qb = $this->entityManager->createQueryBuilder();

$qb->select('u')
->from(CmsUser::class, 'u')
->where(
$qb->expr()->eq('u.name', $qb->createNamedParameter('john doe', Types::STRING)),
)
->orWhere(
$qb->expr()->eq('u.rank', $qb->createNamedParameter(100, Types::INTEGER)),
);

self::assertEquals('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.name = :dcValue1 OR u.rank = :dcValue2', $qb->getDQL());
self::assertEquals('john doe', $qb->getParameter('dcValue1')->getValue());
self::assertEquals(Types::STRING, $qb->getParameter('dcValue1')->getType());
self::assertEquals(100, $qb->getParameter('dcValue2')->getValue());
self::assertEquals(Types::INTEGER, $qb->getParameter('dcValue2')->getType());
}

public function testCreateNamedParameterCustomPlaceholder(): void
{
$qb = $this->entityManager->createQueryBuilder();

$qb->select('u')
->from(CmsUser::class, 'u')
->where(
$qb->expr()->eq('u.name', $qb->createNamedParameter('john doe', Types::STRING, ':test')),
)
->andWhere(
$qb->expr()->eq('u.rank', $qb->createNamedParameter(100, Types::INTEGER)),
);

self::assertEquals('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.name = :test AND u.rank = :dcValue1', $qb->getDQL());
self::assertEquals('john doe', $qb->getParameter('test')->getValue());
self::assertEquals(Types::STRING, $qb->getParameter('test')->getType());
self::assertEquals(100, $qb->getParameter('dcValue1')->getValue());
self::assertEquals(Types::INTEGER, $qb->getParameter('dcValue1')->getType());
}
}

0 comments on commit 1281707

Please sign in to comment.