From 0983d3a4af20124cc9f884727c555be34efdf34b Mon Sep 17 00:00:00 2001 From: Tomas Date: Thu, 27 Jun 2024 09:48:45 +0300 Subject: [PATCH] Add `createNamedParameter` to `QueryBuilder` --- docs/en/reference/query-builder.rst | 18 ++++++++++++ src/QueryBuilder.php | 42 ++++++++++++++++++++++++++++ tests/Tests/ORM/QueryBuilderTest.php | 41 +++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/docs/en/reference/query-builder.rst b/docs/en/reference/query-builder.rst index 3070cc234df..41ef31420ae 100644 --- a/docs/en/reference/query-builder.rst +++ b/docs/en/reference/query-builder.rst @@ -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 + + select('u') + ->from('User', 'u') + ->where('u.email = ' . $qb->createNamedParameter($userInputEmail)) + ; + // SELECT u FROM User u WHERE email = :dcValue1 diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index a6a39a964b8..fe2d750e338 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -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 QueryBuilder that uses the given EntityManager. * @@ -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: + * + * $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)) + * + * + * @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. diff --git a/tests/Tests/ORM/QueryBuilderTest.php b/tests/Tests/ORM/QueryBuilderTest.php index 577c86581bf..fd610bced44 100644 --- a/tests/Tests/ORM/QueryBuilderTest.php +++ b/tests/Tests/ORM/QueryBuilderTest.php @@ -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; @@ -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()); + } }