Repository: doctrine/orm (php)

Issue:
Doctrine\ORM\QueryBuilder::setParameter and ArrayParameterType in DBAL 4.0.0
<!--
Before reporting a BC break, please consult the upgrading document to make sure it's not an expected change: https://github.com/doctrine/orm/blob/2.9.x/UPGRADE.md
-->

### BC Break Report

<!-- Fill in the relevant information below to help triage your issue. -->

|    Q        |   A
|------------ | ------
| BC Break    | yes
| Version     | 3.0.0

#### Summary

<!-- Provide a summary describing the problem you are experiencing. -->

`Doctrine\ORM\QueryBuilder::setParameter()` needs an optional parameter for complex data types sometimes and in 3.x version, it expects `string|int|null` and can be linked via `Doctrine\DBAL\Connection` e.g. `Doctrine\DBAL\Connection::PARAM_STR_ARRAY`. These values used to be as int datatype and have been deprecated. Now these constants are linked to `Doctrine\DBAL\ArrayParameterType` class. In 4.0.0 class ArrayParameterType has been converted to Enums but setParameter() is still accepting old `string|int|null` resulting following error

More info at https://github.com/doctrine/dbal/issues/6309

Pull request description:
Allow (Array)ParameterType in QueryBuilder
Replaces #11279, fixes #11278.

This PR continues the work done by @hanishsingla and adds a few tests.

Changing the signature of `QueryBuilder::setParameter()` is technically a BC break, but we cannot fix the bug without breaking BC, I'm afraid. Let's hope nobody overrides the `setParameter()` method of 3.0 yet. 🤞🏻 

Patch:
diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php
index 3f5b1c777..0d9889b6e 100644
--- a/src/Mapping/ClassMetadata.php
+++ b/src/Mapping/ClassMetadata.php
@@ -2108,12 +2108,13 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
      * @param DiscriminatorColumnMapping|mixed[]|null $columnDef
      * @psalm-param DiscriminatorColumnMapping|array{
      *     name: string|null,
-     *     fieldName?: string|null,
-     *     type?: string|null,
-     *     length?: int|null,
+     *     fieldName?: string,
+     *     type?: string,
+     *     length?: int,
      *     columnDefinition?: string|null,
      *     enumType?: class-string<BackedEnum>|null,
-     *     options?: array<string, mixed>|null
+     *     options?:array<string,
+     *     mixed>|null
      * }|null $columnDef
      *
      * @throws MappingException
@@ -2135,9 +2136,13 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
                 throw MappingException::duplicateColumnName($this->name, $columnDef['name']);
             }
 
-            $columnDef['fieldName'] ??= $columnDef['name'];
-            $columnDef['type']      ??= 'string';
-            $columnDef['options']   ??= [];
+            if (! isset($columnDef['fieldName'])) {
+                $columnDef['fieldName'] = $columnDef['name'];
+            }
+
+            if (! isset($columnDef['type'])) {
+                $columnDef['type'] = 'string';
+            }
 
             if (in_array($columnDef['type'], ['boolean', 'array', 'object', 'datetime', 'time', 'date'], true)) {
                 throw MappingException::invalidDiscriminatorColumnType($this->name, $columnDef['type']);
diff --git a/src/Mapping/DiscriminatorColumnMapping.php b/src/Mapping/DiscriminatorColumnMapping.php
index 4ccb71c4b..cc23fdfdb 100644
--- a/src/Mapping/DiscriminatorColumnMapping.php
+++ b/src/Mapping/DiscriminatorColumnMapping.php
@@ -39,10 +39,10 @@ final class DiscriminatorColumnMapping implements ArrayAccess
      *     type: string,
      *     fieldName: string,
      *     name: string,
-     *     length?: int|null,
-     *     columnDefinition?: string|null,
-     *     enumType?: class-string<BackedEnum>|null,
-     *     options?: array<string, mixed>|null,
+     *     length?: int,
+     *     columnDefinition?: string,
+     *     enumType?: class-string<BackedEnum>,
+     *     options?: array<string, mixed>,
      * } $mappingArray
      */
     public static function fromMappingArray(array $mappingArray): self
@@ -58,7 +58,7 @@ final class DiscriminatorColumnMapping implements ArrayAccess
             }
 
             if (property_exists($mapping, $key)) {
-                $mapping->$key = $value ?? $mapping->$key;
+                $mapping->$key = $value;
             } else {
                 throw new Exception('Unknown property ' . $key . ' on class ' . static::class);
             }
diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php
index 115dea018..f03951d20 100644
--- a/src/QueryBuilder.php
+++ b/src/QueryBuilder.php
@@ -6,6 +6,8 @@ namespace Doctrine\ORM;
 
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Criteria;
+use Doctrine\DBAL\ArrayParameterType;
+use Doctrine\DBAL\ParameterType;
 use Doctrine\ORM\Internal\QueryType;
 use Doctrine\ORM\Query\Expr;
 use Doctrine\ORM\Query\Parameter;
@@ -428,12 +430,12 @@ class QueryBuilder implements Stringable
      *         ->setParameter('user_id', 1);
      * </code>
      *
-     * @param string|int      $key  The parameter position or name.
-     * @param string|int|null $type ParameterType::* or \Doctrine\DBAL\Types\Type::* constant
+     * @param string|int                                       $key  The parameter position or name.
+     * @param ParameterType|ArrayParameterType|string|int|null $type ParameterType::*, ArrayParameterType::* or \Doctrine\DBAL\Types\Type::* constant
      *
      * @return $this
      */
-    public function setParameter(string|int $key, mixed $value, string|int|null $type = null): static
+    public function setParameter(string|int $key, mixed $value, ParameterType|ArrayParameterType|string|int|null $type = null): static
     {
         $existingParameter = $this->getParameter($key);
 


Test patch:
diff --git a/tests/Tests/ORM/Functional/QueryParameterTest.php b/tests/Tests/ORM/Functional/QueryParameterTest.php
new file mode 100644
index 000000000..e7599cea5
--- /dev/null
+++ b/tests/Tests/ORM/Functional/QueryParameterTest.php
@@ -0,0 +1,124 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Doctrine\Tests\ORM\Functional;
+
+use Doctrine\DBAL\ArrayParameterType;
+use Doctrine\DBAL\ParameterType;
+use Doctrine\DBAL\Types\Types;
+use Doctrine\Tests\Models\CMS\CmsUser;
+use Doctrine\Tests\OrmFunctionalTestCase;
+use PHPUnit\Framework\Attributes\Group;
+
+#[Group('GH-11278')]
+final class QueryParameterTest extends OrmFunctionalTestCase
+{
+    private int $userId;
+
+    protected function setUp(): void
+    {
+        $this->useModelSet('cms');
+
+        parent::setUp();
+
+        $user            = new CmsUser();
+        $user->name      = 'John Doe';
+        $user->username  = 'john';
+        $user2           = new CmsUser();
+        $user2->name     = 'Jane Doe';
+        $user2->username = 'jane';
+        $user3           = new CmsUser();
+        $user3->name     = 'Just Bill';
+        $user3->username = 'bill';
+
+        $this->_em->persist($user);
+        $this->_em->persist($user2);
+        $this->_em->persist($user3);
+        $this->_em->flush();
+
+        $this->userId = $user->id;
+
+        $this->_em->clear();
+    }
+
+    public function testParameterTypeInBuilder(): void
+    {
+        $result = $this->_em->createQueryBuilder()
+            ->from(CmsUser::class, 'u')
+            ->select('u.name')
+            ->where('u.id = :id')
+            ->setParameter('id', $this->userId, ParameterType::INTEGER)
+            ->getQuery()
+            ->getArrayResult();
+
+        self::assertSame([['name' => 'John Doe']], $result);
+    }
+
+    public function testParameterTypeInQuery(): void
+    {
+        $result = $this->_em->createQueryBuilder()
+            ->from(CmsUser::class, 'u')
+            ->select('u.name')
+            ->where('u.id = :id')
+            ->getQuery()
+            ->setParameter('id', $this->userId, ParameterType::INTEGER)
+            ->getArrayResult();
+
+        self::assertSame([['name' => 'John Doe']], $result);
+    }
+
+    public function testDbalTypeStringInBuilder(): void
+    {
+        $result = $this->_em->createQueryBuilder()
+            ->from(CmsUser::class, 'u')
+            ->select('u.name')
+            ->where('u.id = :id')
+            ->setParameter('id', $this->userId, Types::INTEGER)
+            ->getQuery()
+            ->getArrayResult();
+
+        self::assertSame([['name' => 'John Doe']], $result);
+    }
+
+    public function testDbalTypeStringInQuery(): void
+    {
+        $result = $this->_em->createQueryBuilder()
+            ->from(CmsUser::class, 'u')
+            ->select('u.name')
+            ->where('u.id = :id')
+            ->getQuery()
+            ->setParameter('id', $this->userId, Types::INTEGER)
+            ->getArrayResult();
+
+        self::assertSame([['name' => 'John Doe']], $result);
+    }
+
+    public function testArrayParameterTypeInBuilder(): void
+    {
+        $result = $this->_em->createQueryBuilder()
+            ->from(CmsUser::class, 'u')
+            ->select('u.name')
+            ->where('u.username IN (:usernames)')
+            ->orderBy('u.username')
+            ->setParameter('usernames', ['john', 'jane'], ArrayParameterType::STRING)
+            ->getQuery()
+            ->getArrayResult();
+
+        self::assertSame([['name' => 'Jane Doe'], ['name' => 'John Doe']], $result);
+    }
+
+    public function testArrayParameterTypeInQuery(): void
+    {
+        $result = $this->_em->createQueryBuilder()
+            ->from(CmsUser::class, 'u')
+            ->select('u.name')
+            ->where('u.username IN (:usernames)')
+            ->orderBy('u.username')
+            ->getQuery()
+            ->setParameter('usernames', ['john', 'jane'], ArrayParameterType::STRING)
+            ->getArrayResult();
+
+        self::assertSame([['name' => 'Jane Doe'], ['name' => 'John Doe']], $result);
+    }
+}