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());
+ }
}