From 6e40d3f83ab4956d79e79dc1568297cac133826c Mon Sep 17 00:00:00 2001
From: Vincent Langlet <vincentlanglet@hotmail.fr>
Date: Sat, 17 Feb 2024 18:02:15 +0100
Subject: [PATCH 1/4] Refacto queryResultTypeWalkerTest

---
 phpunit.xml                                   |   1 +
 ...t.php => PDOQueryResultTypeWalkerTest.php} | 312 +---------------
 .../Query/QueryResultTypeWalkerTestCase.php   | 346 ++++++++++++++++++
 3 files changed, 350 insertions(+), 309 deletions(-)
 rename tests/Type/Doctrine/Query/{QueryResultTypeWalkerTest.php => PDOQueryResultTypeWalkerTest.php} (81%)
 create mode 100644 tests/Type/Doctrine/Query/QueryResultTypeWalkerTestCase.php

diff --git a/phpunit.xml b/phpunit.xml
index 6d69639a..8c5f02c9 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -32,6 +32,7 @@
 
     <testsuites>
         <testsuite name="PHPStan for Doctrine">
+            <directory suffix="TestCase.php">tests</directory>
             <directory suffix="Test.php">tests</directory>
         </testsuite>
     </testsuites>
diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/PDOQueryResultTypeWalkerTest.php
similarity index 81%
rename from tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php
rename to tests/Type/Doctrine/Query/PDOQueryResultTypeWalkerTest.php
index ea1e0aea..9a8312b5 100644
--- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php
+++ b/tests/Type/Doctrine/Query/PDOQueryResultTypeWalkerTest.php
@@ -6,232 +6,35 @@
 use Composer\Semver\VersionParser;
 use DateTime;
 use DateTimeImmutable;
-use Doctrine\Common\Collections\ArrayCollection;
-use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\Mapping\Column;
-use Doctrine\ORM\Query\AST\TypedExpression;
-use Doctrine\ORM\Tools\SchemaTool;
-use PHPStan\Testing\PHPStanTestCase;
-use PHPStan\Type\Accessory\AccessoryNumericStringType;
-use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
 use PHPStan\Type\Constant\ConstantIntegerType;
 use PHPStan\Type\Constant\ConstantStringType;
-use PHPStan\Type\ConstantTypeHelper;
-use PHPStan\Type\Doctrine\DescriptorRegistry;
 use PHPStan\Type\FloatType;
-use PHPStan\Type\IntegerRangeType;
 use PHPStan\Type\IntegerType;
-use PHPStan\Type\IntersectionType;
 use PHPStan\Type\MixedType;
 use PHPStan\Type\NullType;
 use PHPStan\Type\ObjectType;
 use PHPStan\Type\StringType;
 use PHPStan\Type\Type;
 use PHPStan\Type\TypeCombinator;
-use PHPStan\Type\UnionType;
-use PHPStan\Type\VerbosityLevel;
-use QueryResult\Entities\Embedded;
-use QueryResult\Entities\JoinedChild;
 use QueryResult\Entities\Many;
 use QueryResult\Entities\ManyId;
-use QueryResult\Entities\NestedEmbedded;
 use QueryResult\Entities\One;
 use QueryResult\Entities\OneId;
-use QueryResult\Entities\SingleTableChild;
-use QueryResult\EntitiesEnum\EntityWithEnum;
 use QueryResult\EntitiesEnum\IntEnum;
 use QueryResult\EntitiesEnum\StringEnum;
-use Throwable;
-use function array_merge;
-use function array_shift;
 use function assert;
 use function class_exists;
-use function count;
 use function property_exists;
-use function sprintf;
 use function strpos;
-use function version_compare;
 use const PHP_VERSION_ID;
 
-final class QueryResultTypeWalkerTest extends PHPStanTestCase
+final class PDOQueryResultTypeWalkerTest extends QueryResultTypeWalkerTestCase
 {
 
-	/** @var EntityManagerInterface */
-	private static $em;
-
-	/** @var DescriptorRegistry */
-	private $descriptorRegistry;
-
-	public static function getAdditionalConfigFiles(): array
-	{
-		return [
-			__DIR__ . '/../data/QueryResult/config.neon',
-		];
-	}
-
-	public static function setUpBeforeClass(): void
-	{
-		$em = require __DIR__ . '/../data/QueryResult/entity-manager.php';
-		self::$em = $em;
-
-		$schemaTool = new SchemaTool($em);
-		$classes = $em->getMetadataFactory()->getAllMetadata();
-		$schemaTool->createSchema($classes);
-
-		$dataOne = [
-			'intColumn' => [1, 2],
-			'stringColumn' => ['A', 'B'],
-			'stringNullColumn' => ['A', null],
-		];
-
-		$dataMany = [
-			'intColumn' => [1, 2],
-			'stringColumn' => ['A', 'B'],
-			'stringNullColumn' => ['A', null],
-		];
-
-		$dataJoinedInheritance = [
-			'parentColumn' => [1, 2],
-			'parentNullColumn' => [1, null],
-			'childColumn' => [1, 2],
-			'childNullColumn' => [1, null],
-		];
-
-		$dataSingleTableInheritance = [
-			'parentColumn' => [1, 2],
-			'parentNullColumn' => [1, null],
-			'childNullColumn' => [1, null],
-		];
-
-		$id = 1;
-
-		foreach (self::combinations($dataOne) as $combination) {
-			[$intColumn, $stringColumn, $stringNullColumn] = $combination;
-			$one = new One();
-			$one->id = (string) $id++;
-			$one->intColumn = $intColumn;
-			$one->stringColumn = $stringColumn;
-			$one->stringNullColumn = $stringNullColumn;
-			$embedded = new Embedded();
-			$embedded->intColumn = $intColumn;
-			$embedded->stringColumn = $stringColumn;
-			$embedded->stringNullColumn = $stringNullColumn;
-			$nestedEmbedded = new NestedEmbedded();
-			$nestedEmbedded->intColumn = $intColumn;
-			$nestedEmbedded->stringColumn = $stringColumn;
-			$nestedEmbedded->stringNullColumn = $stringNullColumn;
-			$embedded->nestedEmbedded = $nestedEmbedded;
-			$one->embedded = $embedded;
-			$one->manies = new ArrayCollection();
-
-			foreach (self::combinations($dataMany) as $combinationMany) {
-				[$intColumnMany, $stringColumnMany, $stringNullColumnMany] = $combinationMany;
-				$many = new Many();
-				$many->id = (string) $id++;
-				$many->intColumn = $intColumnMany;
-				$many->stringColumn = $stringColumnMany;
-				$many->stringNullColumn = $stringNullColumnMany;
-				$many->datetimeColumn = new DateTime('2001-01-01 00:00:00');
-				$many->datetimeImmutableColumn = new DateTimeImmutable('2001-01-01 00:00:00');
-				$many->simpleArrayColumn = ['foo'];
-				$many->one = $one;
-				$one->manies->add($many);
-				$em->persist($many);
-			}
-
-			$em->persist($one);
-		}
-
-		foreach (self::combinations($dataJoinedInheritance) as $combination) {
-			[$parentColumn, $parentNullColumn, $childColumn, $childNullColumn] = $combination;
-			$child = new JoinedChild();
-			$child->id = (string) $id++;
-			$child->parentColumn = $parentColumn;
-			$child->parentNullColumn = $parentNullColumn;
-			$child->childColumn = $childColumn;
-			$child->childNullColumn = $childNullColumn;
-			$em->persist($child);
-		}
-
-		foreach (self::combinations($dataSingleTableInheritance) as $combination) {
-			[$parentColumn, $parentNullColumn, $childNullColumn] = $combination;
-			$child = new SingleTableChild();
-			$child->id = (string) $id++;
-			$child->parentColumn = $parentColumn;
-			$child->parentNullColumn = $parentNullColumn;
-			$child->childNullColumn = $childNullColumn;
-			$em->persist($child);
-		}
-
-		if (property_exists(Column::class, 'enumType') && PHP_VERSION_ID >= 80100) {
-			assert(class_exists(StringEnum::class));
-			assert(class_exists(IntEnum::class));
-
-			$entityWithEnum = new EntityWithEnum();
-			$entityWithEnum->id = '1';
-			$entityWithEnum->stringEnumColumn = StringEnum::A;
-			$entityWithEnum->intEnumColumn = IntEnum::A;
-			$entityWithEnum->intEnumOnStringColumn = IntEnum::A;
-			$em->persist($entityWithEnum);
-		}
-
-		$em->flush();
-	}
-
-	public static function tearDownAfterClass(): void
-	{
-		self::$em->clear();
-	}
-
-	public function setUp(): void
+	protected static function getEntityManagerPath(): string
 	{
-		$this->descriptorRegistry = self::getContainer()->getByType(DescriptorRegistry::class);
-	}
-
-	/** @dataProvider getTestData */
-	public function test(Type $expectedType, string $dql, ?string $expectedExceptionMessage = null, ?string $expectedDeprecationMessage = null): void
-	{
-		$em = self::$em;
-
-		$query = $em->createQuery($dql);
-
-		$typeBuilder = new QueryResultTypeBuilder();
-
-		if ($expectedExceptionMessage !== null) {
-			$this->expectException(Throwable::class);
-			$this->expectExceptionMessage($expectedExceptionMessage);
-		} elseif ($expectedDeprecationMessage !== null) {
-			$this->expectDeprecation();
-			$this->expectDeprecationMessage($expectedDeprecationMessage);
-		}
-
-		QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry);
-
-		$type = $typeBuilder->getResultType();
-
-		self::assertSame(
-			$expectedType->describe(VerbosityLevel::precise()),
-			$type->describe(VerbosityLevel::precise())
-		);
-
-		// Double-check our expectations
-
-		$query = $em->createQuery($dql);
-
-		$result = $query->getResult();
-		self::assertGreaterThan(0, count($result));
-
-		foreach ($result as $row) {
-			$rowType = ConstantTypeHelper::getTypeFromValue($row);
-			self::assertTrue(
-				$type->accepts($rowType, true)->yes(),
-				sprintf(
-					"The inferred type\n%s\nshould accept actual type\n%s",
-					$type->describe(VerbosityLevel::precise()),
-					$rowType->describe(VerbosityLevel::precise())
-				)
-			);
-		}
+		return __DIR__ . '/../data/QueryResult/entity-manager.php';
 	}
 
 	/**
@@ -1591,113 +1394,4 @@ public function getTestData(): iterable
 		];
 	}
 
-	/**
-	 * @param array<int,array{0: ConstantIntegerType|ConstantStringType, 1: Type, 2?: bool}> $elements
-	 */
-	private function constantArray(array $elements): Type
-	{
-		$builder = ConstantArrayTypeBuilder::createEmpty();
-
-		foreach ($elements as $element) {
-			$offsetType = $element[0];
-			$valueType = $element[1];
-			$optional = $element[2] ?? false;
-			$builder->setOffsetValueType($offsetType, $valueType, $optional);
-		}
-
-		return $builder->getArray();
-	}
-
-	private function numericStringOrInt(): Type
-	{
-		return new UnionType([
-			new IntegerType(),
-			new IntersectionType([
-				new StringType(),
-				new AccessoryNumericStringType(),
-			]),
-		]);
-	}
-
-	private function numericString(): Type
-	{
-		return new IntersectionType([
-			new StringType(),
-			new AccessoryNumericStringType(),
-		]);
-	}
-
-	private function uint(): Type
-	{
-		return IntegerRangeType::fromInterval(0, null);
-	}
-
-	private function intStringified(): Type
-	{
-		return TypeCombinator::union(
-			new IntegerType(),
-			$this->numericString()
-		);
-	}
-	private function uintStringified(): Type
-	{
-		return TypeCombinator::union(
-			$this->uint(),
-			$this->numericString()
-		);
-	}
-
-	private function numericStringified(): Type
-	{
-		return TypeCombinator::union(
-			new FloatType(),
-			new IntegerType(),
-			$this->numericString()
-		);
-	}
-
-	private function unumericStringified(): Type
-	{
-		return TypeCombinator::union(
-			new FloatType(),
-			IntegerRangeType::fromInterval(0, null),
-			$this->numericString()
-		);
-	}
-
-	private function hasTypedExpressions(): bool
-	{
-		return class_exists(TypedExpression::class);
-	}
-
-	/**
-	 * @param array<mixed[]> $arrays
-	 *
-	 * @return iterable<mixed[]>
-	 */
-	private static function combinations(array $arrays): iterable
-	{
-		if ($arrays === []) {
-			yield [];
-			return;
-		}
-
-		$head = array_shift($arrays);
-
-		foreach ($head as $elem) {
-			foreach (self::combinations($arrays) as $combination) {
-				yield array_merge([$elem], $combination);
-			}
-		}
-	}
-
-	private function isDoctrine211(): bool
-	{
-		$version = InstalledVersions::getVersion('doctrine/orm');
-
-		return $version !== null
-			&& version_compare($version, '2.11', '>=')
-			&& version_compare($version, '2.12', '<');
-	}
-
 }
diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTestCase.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTestCase.php
new file mode 100644
index 00000000..39f79124
--- /dev/null
+++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTestCase.php
@@ -0,0 +1,346 @@
+<?php declare(strict_types = 1);
+
+namespace PHPStan\Type\Doctrine\Query;
+
+use Composer\InstalledVersions;
+use DateTime;
+use DateTimeImmutable;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\Mapping\Column;
+use Doctrine\ORM\Query\AST\TypedExpression;
+use Doctrine\ORM\Tools\SchemaTool;
+use PHPStan\Testing\PHPStanTestCase;
+use PHPStan\Type\Accessory\AccessoryNumericStringType;
+use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
+use PHPStan\Type\Constant\ConstantIntegerType;
+use PHPStan\Type\Constant\ConstantStringType;
+use PHPStan\Type\ConstantTypeHelper;
+use PHPStan\Type\Doctrine\DescriptorRegistry;
+use PHPStan\Type\FloatType;
+use PHPStan\Type\IntegerRangeType;
+use PHPStan\Type\IntegerType;
+use PHPStan\Type\IntersectionType;
+use PHPStan\Type\StringType;
+use PHPStan\Type\Type;
+use PHPStan\Type\TypeCombinator;
+use PHPStan\Type\UnionType;
+use PHPStan\Type\VerbosityLevel;
+use QueryResult\Entities\Embedded;
+use QueryResult\Entities\JoinedChild;
+use QueryResult\Entities\Many;
+use QueryResult\Entities\NestedEmbedded;
+use QueryResult\Entities\One;
+use QueryResult\Entities\SingleTableChild;
+use QueryResult\EntitiesEnum\EntityWithEnum;
+use QueryResult\EntitiesEnum\IntEnum;
+use QueryResult\EntitiesEnum\StringEnum;
+use Throwable;
+use function array_merge;
+use function array_shift;
+use function assert;
+use function class_exists;
+use function count;
+use function property_exists;
+use function sprintf;
+use function version_compare;
+use const PHP_VERSION_ID;
+
+abstract class QueryResultTypeWalkerTestCase extends PHPStanTestCase
+{
+	/** @var EntityManagerInterface */
+	private static $em;
+
+	/** @var DescriptorRegistry */
+	private $descriptorRegistry;
+
+	abstract protected static function getEntityManagerPath(): string;
+
+	public static function getAdditionalConfigFiles(): array
+	{
+		return [
+			__DIR__ . '/../data/QueryResult/config.neon',
+		];
+	}
+
+	public static function setUpBeforeClass(): void
+	{
+		$em = require static::getEntityManagerPath();
+		self::$em = $em;
+
+		$schemaTool = new SchemaTool($em);
+		$classes = $em->getMetadataFactory()->getAllMetadata();
+		$schemaTool->createSchema($classes);
+
+		$dataOne = [
+			'intColumn' => [1, 2],
+			'stringColumn' => ['A', 'B'],
+			'stringNullColumn' => ['A', null],
+		];
+
+		$dataMany = [
+			'intColumn' => [1, 2],
+			'stringColumn' => ['A', 'B'],
+			'stringNullColumn' => ['A', null],
+		];
+
+		$dataJoinedInheritance = [
+			'parentColumn' => [1, 2],
+			'parentNullColumn' => [1, null],
+			'childColumn' => [1, 2],
+			'childNullColumn' => [1, null],
+		];
+
+		$dataSingleTableInheritance = [
+			'parentColumn' => [1, 2],
+			'parentNullColumn' => [1, null],
+			'childNullColumn' => [1, null],
+		];
+
+		$id = 1;
+
+		foreach (self::combinations($dataOne) as $combination) {
+			[$intColumn, $stringColumn, $stringNullColumn] = $combination;
+			$one = new One();
+			$one->id = (string) $id++;
+			$one->intColumn = $intColumn;
+			$one->stringColumn = $stringColumn;
+			$one->stringNullColumn = $stringNullColumn;
+			$embedded = new Embedded();
+			$embedded->intColumn = $intColumn;
+			$embedded->stringColumn = $stringColumn;
+			$embedded->stringNullColumn = $stringNullColumn;
+			$nestedEmbedded = new NestedEmbedded();
+			$nestedEmbedded->intColumn = $intColumn;
+			$nestedEmbedded->stringColumn = $stringColumn;
+			$nestedEmbedded->stringNullColumn = $stringNullColumn;
+			$embedded->nestedEmbedded = $nestedEmbedded;
+			$one->embedded = $embedded;
+			$one->manies = new ArrayCollection();
+
+			foreach (self::combinations($dataMany) as $combinationMany) {
+				[$intColumnMany, $stringColumnMany, $stringNullColumnMany] = $combinationMany;
+				$many = new Many();
+				$many->id = (string) $id++;
+				$many->intColumn = $intColumnMany;
+				$many->stringColumn = $stringColumnMany;
+				$many->stringNullColumn = $stringNullColumnMany;
+				$many->datetimeColumn = new DateTime('2001-01-01 00:00:00');
+				$many->datetimeImmutableColumn = new DateTimeImmutable('2001-01-01 00:00:00');
+				$many->simpleArrayColumn = ['foo'];
+				$many->one = $one;
+				$one->manies->add($many);
+				$em->persist($many);
+			}
+
+			$em->persist($one);
+		}
+
+		foreach (self::combinations($dataJoinedInheritance) as $combination) {
+			[$parentColumn, $parentNullColumn, $childColumn, $childNullColumn] = $combination;
+			$child = new JoinedChild();
+			$child->id = (string) $id++;
+			$child->parentColumn = $parentColumn;
+			$child->parentNullColumn = $parentNullColumn;
+			$child->childColumn = $childColumn;
+			$child->childNullColumn = $childNullColumn;
+			$em->persist($child);
+		}
+
+		foreach (self::combinations($dataSingleTableInheritance) as $combination) {
+			[$parentColumn, $parentNullColumn, $childNullColumn] = $combination;
+			$child = new SingleTableChild();
+			$child->id = (string) $id++;
+			$child->parentColumn = $parentColumn;
+			$child->parentNullColumn = $parentNullColumn;
+			$child->childNullColumn = $childNullColumn;
+			$em->persist($child);
+		}
+
+		if (property_exists(Column::class, 'enumType') && PHP_VERSION_ID >= 80100) {
+			assert(class_exists(StringEnum::class));
+			assert(class_exists(IntEnum::class));
+
+			$entityWithEnum = new EntityWithEnum();
+			$entityWithEnum->id = '1';
+			$entityWithEnum->stringEnumColumn = StringEnum::A;
+			$entityWithEnum->intEnumColumn = IntEnum::A;
+			$entityWithEnum->intEnumOnStringColumn = IntEnum::A;
+			$em->persist($entityWithEnum);
+		}
+
+		$em->flush();
+	}
+
+	public static function tearDownAfterClass(): void
+	{
+		self::$em->clear();
+	}
+
+	public function setUp(): void
+	{
+		$this->descriptorRegistry = self::getContainer()->getByType(DescriptorRegistry::class);
+	}
+
+	/** @dataProvider getTestData */
+	public function test(Type $expectedType, string $dql, ?string $expectedExceptionMessage = null, ?string $expectedDeprecationMessage = null): void
+	{
+		$em = self::$em;
+
+		$query = $em->createQuery($dql);
+
+		$typeBuilder = new QueryResultTypeBuilder();
+
+		if ($expectedExceptionMessage !== null) {
+			$this->expectException(Throwable::class);
+			$this->expectExceptionMessage($expectedExceptionMessage);
+		} elseif ($expectedDeprecationMessage !== null) {
+			$this->expectDeprecation();
+			$this->expectDeprecationMessage($expectedDeprecationMessage);
+		}
+
+		QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry);
+
+		$type = $typeBuilder->getResultType();
+
+		self::assertSame(
+			$expectedType->describe(VerbosityLevel::precise()),
+			$type->describe(VerbosityLevel::precise())
+		);
+
+		// Double-check our expectations
+
+		$query = $em->createQuery($dql);
+
+		$result = $query->getResult();
+		self::assertGreaterThan(0, count($result));
+
+		foreach ($result as $row) {
+			$rowType = ConstantTypeHelper::getTypeFromValue($row);
+			self::assertTrue(
+				$type->accepts($rowType, true)->yes(),
+				sprintf(
+					"The inferred type\n%s\nshould accept actual type\n%s",
+					$type->describe(VerbosityLevel::precise()),
+					$rowType->describe(VerbosityLevel::precise())
+				)
+			);
+		}
+	}
+
+	/**
+	 * @return iterable<string,array{Type,string,2?:string|null}>
+	 */
+	abstract public function getTestData(): iterable;
+
+	/**
+	 * @param array<int,array{0: ConstantIntegerType|ConstantStringType, 1: Type, 2?: bool}> $elements
+	 */
+	protected function constantArray(array $elements): Type
+	{
+		$builder = ConstantArrayTypeBuilder::createEmpty();
+
+		foreach ($elements as $element) {
+			$offsetType = $element[0];
+			$valueType = $element[1];
+			$optional = $element[2] ?? false;
+			$builder->setOffsetValueType($offsetType, $valueType, $optional);
+		}
+
+		return $builder->getArray();
+	}
+
+	protected function numericStringOrInt(): Type
+	{
+		return new UnionType([
+			new IntegerType(),
+			new IntersectionType([
+				new StringType(),
+				new AccessoryNumericStringType(),
+			]),
+		]);
+	}
+
+	protected function numericString(): Type
+	{
+		return new IntersectionType([
+			new StringType(),
+			new AccessoryNumericStringType(),
+		]);
+	}
+
+	protected function uint(): Type
+	{
+		return IntegerRangeType::fromInterval(0, null);
+	}
+
+	protected function intStringified(): Type
+	{
+		return TypeCombinator::union(
+			new IntegerType(),
+			$this->numericString()
+		);
+	}
+
+	protected function uintStringified(): Type
+	{
+		return TypeCombinator::union(
+			$this->uint(),
+			$this->numericString()
+		);
+	}
+
+	protected function numericStringified(): Type
+	{
+		return TypeCombinator::union(
+			new FloatType(),
+			new IntegerType(),
+			$this->numericString()
+		);
+	}
+
+	protected function unumericStringified(): Type
+	{
+		return TypeCombinator::union(
+			new FloatType(),
+			IntegerRangeType::fromInterval(0, null),
+			$this->numericString()
+		);
+	}
+
+	protected function hasTypedExpressions(): bool
+	{
+		return class_exists(TypedExpression::class);
+	}
+
+	/**
+	 * @param array<mixed[]> $arrays
+	 *
+	 * @return iterable<mixed[]>
+	 */
+	private static function combinations(array $arrays): iterable
+	{
+		if ($arrays === []) {
+			yield [];
+			return;
+		}
+
+		$head = array_shift($arrays);
+
+		foreach ($head as $elem) {
+			foreach (self::combinations($arrays) as $combination) {
+				yield array_merge([$elem], $combination);
+			}
+		}
+	}
+
+	protected function isDoctrine211(): bool
+	{
+		$version = InstalledVersions::getVersion('doctrine/orm');
+
+		return $version !== null
+			&& version_compare($version, '2.11', '>=')
+			&& version_compare($version, '2.12', '<');
+	}
+
+}

From 6fa0a11d15c940497f5d1f7c9525fe2c4a0e66fc Mon Sep 17 00:00:00 2001
From: Vincent Langlet <vincentlanglet@hotmail.fr>
Date: Sat, 17 Feb 2024 18:51:02 +0100
Subject: [PATCH 2/4] Poc

---
 .github/workflows/test-platforms.yml          | 58 +++++++++++++++++++
 Makefile                                      |  2 +-
 .../Query/MysqlQueryResultTypeWalkerTest.php  | 17 ++++++
 .../Query/PDOQueryResultTypeWalkerTest.php    |  6 +-
 .../Query/QueryResultTypeWalkerTestCase.php   |  7 ++-
 .../data/QueryResult/entity-manager-mysql.php | 40 +++++++++++++
 6 files changed, 122 insertions(+), 8 deletions(-)
 create mode 100644 .github/workflows/test-platforms.yml
 create mode 100644 tests/Type/Doctrine/Query/MysqlQueryResultTypeWalkerTest.php
 create mode 100644 tests/Type/Doctrine/data/QueryResult/entity-manager-mysql.php

diff --git a/.github/workflows/test-platforms.yml b/.github/workflows/test-platforms.yml
new file mode 100644
index 00000000..aec2723d
--- /dev/null
+++ b/.github/workflows/test-platforms.yml
@@ -0,0 +1,58 @@
+# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
+
+name: "Test on platforms"
+
+on:
+  pull_request:
+  push:
+    branches:
+      - "1.3.x"
+
+jobs:
+  test-mysql:
+    name: "Tests mysql"
+    runs-on: "ubuntu-latest"
+
+    env:
+      DATABASE_URL: mysql://root@127.0.0.1:3306/phpstan_doctrine
+
+    services:
+      mysql:
+        image: mysql:${{ matrix.mysql-version }}
+        options: >-
+          --health-cmd "mysqladmin ping --silent"
+          --name=mysqlcontainer
+          -e MYSQL_ALLOW_EMPTY_PASSWORD=yes
+          -e MYSQL_DATABASE=phpstan_doctrine
+        ports:
+          - 3306:3306
+
+    strategy:
+      fail-fast: false
+      matrix:
+        php-version:
+          - "8.2"
+        mysql-version:
+          - '5.7'
+          - '8.0'
+
+    steps:
+      - name: "Checkout"
+        uses: actions/checkout@v3
+
+      - name: "Install PHP"
+        uses: "shivammathur/setup-php@v2"
+        with:
+          coverage: "none"
+          php-version: "${{ matrix.php-version }}"
+          ini-file: development
+          extensions: "mongodb"
+
+      - name: "Install dependencies"
+        run: "composer update --no-interaction --no-progress"
+
+      - name: Change mysql sql_mode
+        run: "docker exec mysqlcontainer mysql -u root -e \"SET GLOBAL sql_mode = '';\""
+
+      - name: "Tests"
+        run: "vendor/bin/phpunit --group=mysql"
diff --git a/Makefile b/Makefile
index 2ec6452c..382dfa5c 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@ check: lint cs tests phpstan
 
 .PHONY: tests
 tests:
-	php vendor/bin/phpunit
+	php vendor/bin/phpunit --exclude-group=platform
 
 .PHONY: lint
 lint:
diff --git a/tests/Type/Doctrine/Query/MysqlQueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/MysqlQueryResultTypeWalkerTest.php
new file mode 100644
index 00000000..cefeee97
--- /dev/null
+++ b/tests/Type/Doctrine/Query/MysqlQueryResultTypeWalkerTest.php
@@ -0,0 +1,17 @@
+<?php declare(strict_types = 1);
+
+namespace PHPStan\Type\Doctrine\Query;
+
+/**
+ * @group platform
+ * @group mysql
+ */
+class MysqlQueryResultTypeWalkerTest extends PDOQueryResultTypeWalkerTest
+{
+
+	protected static function getEntityManagerPath(): string
+	{
+		return __DIR__ . '/../data/QueryResult/entity-manager-mysql.php';
+	}
+
+}
diff --git a/tests/Type/Doctrine/Query/PDOQueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/PDOQueryResultTypeWalkerTest.php
index 9a8312b5..4473d152 100644
--- a/tests/Type/Doctrine/Query/PDOQueryResultTypeWalkerTest.php
+++ b/tests/Type/Doctrine/Query/PDOQueryResultTypeWalkerTest.php
@@ -15,7 +15,6 @@
 use PHPStan\Type\NullType;
 use PHPStan\Type\ObjectType;
 use PHPStan\Type\StringType;
-use PHPStan\Type\Type;
 use PHPStan\Type\TypeCombinator;
 use QueryResult\Entities\Many;
 use QueryResult\Entities\ManyId;
@@ -29,7 +28,7 @@
 use function strpos;
 use const PHP_VERSION_ID;
 
-final class PDOQueryResultTypeWalkerTest extends QueryResultTypeWalkerTestCase
+class PDOQueryResultTypeWalkerTest extends QueryResultTypeWalkerTestCase
 {
 
 	protected static function getEntityManagerPath(): string
@@ -37,9 +36,6 @@ protected static function getEntityManagerPath(): string
 		return __DIR__ . '/../data/QueryResult/entity-manager.php';
 	}
 
-	/**
-	 * @return iterable<string,array{Type,string,2?:string|null}>
-	 */
 	public function getTestData(): iterable
 	{
 		$ormVersion = InstalledVersions::getVersion('doctrine/orm');
diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTestCase.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTestCase.php
index 39f79124..319ac1a3 100644
--- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTestCase.php
+++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTestCase.php
@@ -48,8 +48,9 @@
 
 abstract class QueryResultTypeWalkerTestCase extends PHPStanTestCase
 {
+
 	/** @var EntityManagerInterface */
-	private static $em;
+	protected static $em;
 
 	/** @var DescriptorRegistry */
 	private $descriptorRegistry;
@@ -182,7 +183,9 @@ public function setUp(): void
 		$this->descriptorRegistry = self::getContainer()->getByType(DescriptorRegistry::class);
 	}
 
-	/** @dataProvider getTestData */
+	/**
+	 * @dataProvider getTestData
+	 */
 	public function test(Type $expectedType, string $dql, ?string $expectedExceptionMessage = null, ?string $expectedDeprecationMessage = null): void
 	{
 		$em = self::$em;
diff --git a/tests/Type/Doctrine/data/QueryResult/entity-manager-mysql.php b/tests/Type/Doctrine/data/QueryResult/entity-manager-mysql.php
new file mode 100644
index 00000000..c7887826
--- /dev/null
+++ b/tests/Type/Doctrine/data/QueryResult/entity-manager-mysql.php
@@ -0,0 +1,40 @@
+<?php declare(strict_types = 1);
+
+use Cache\Adapter\PHPArray\ArrayCachePool;
+use Doctrine\Common\Annotations\AnnotationReader;
+use Doctrine\DBAL\DriverManager;
+use Doctrine\ORM\Configuration;
+use Doctrine\ORM\EntityManager;
+use Doctrine\ORM\Mapping\Column;
+use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
+use Doctrine\ORM\Mapping\Driver\AttributeDriver;
+use Doctrine\Persistence\Mapping\Driver\MappingDriverChain;
+
+$config = new Configuration();
+$config->setProxyDir(__DIR__);
+$config->setProxyNamespace('PHPstan\Doctrine\OrmProxies');
+$config->setMetadataCache(new ArrayCachePool());
+
+$metadataDriver = new MappingDriverChain();
+
+$metadataDriver->addDriver(new AnnotationDriver(
+	new AnnotationReader(),
+	[__DIR__ . '/Entities']
+), 'QueryResult\Entities\\');
+
+if (property_exists(Column::class, 'enumType') && PHP_VERSION_ID >= 80100) {
+	$metadataDriver->addDriver(new AnnotationDriver(
+		new AnnotationReader(),
+		[__DIR__ . '/EntitiesEnum']
+	), 'QueryResult\EntitiesEnum\\');
+}
+
+$config->setMetadataDriverImpl($metadataDriver);
+
+return new EntityManager(
+	DriverManager::getConnection([
+		'driver' => 'mysql',
+		'url' => 'mysql://root@127.0.0.1:3306/phpstan_doctrine',
+	]),
+	$config
+);

From 4498953c3fb14994d4d66ccdcca8f61e6037f099 Mon Sep 17 00:00:00 2001
From: Vincent Langlet <vincentlanglet@hotmail.fr>
Date: Sat, 17 Feb 2024 19:33:57 +0100
Subject: [PATCH 3/4] Fix query

---
 tests/Type/Doctrine/Query/PDOQueryResultTypeWalkerTest.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/Type/Doctrine/Query/PDOQueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/PDOQueryResultTypeWalkerTest.php
index 4473d152..f433ed09 100644
--- a/tests/Type/Doctrine/Query/PDOQueryResultTypeWalkerTest.php
+++ b/tests/Type/Doctrine/Query/PDOQueryResultTypeWalkerTest.php
@@ -389,7 +389,7 @@ public function getTestData(): iterable
 				[new ConstantIntegerType(1), new MixedType()],
 			]),
 			'
-				SELECT		(SELECT	m.intColumn FROM QueryResult\Entities\Many m)
+				SELECT		(SELECT	COUNT(m.intColumn) FROM QueryResult\Entities\Many m)
 				FROM		QueryResult\Entities\Many m2
 			',
 		];

From 7f753d465960cf8a27fa7dfc212c00cbd300bf52 Mon Sep 17 00:00:00 2001
From: Vincent Langlet <vincentlanglet@hotmail.fr>
Date: Tue, 20 Feb 2024 11:23:32 +0100
Subject: [PATCH 4/4] Add missing config

---
 .../Type/Doctrine/Query/MysqlQueryResultTypeWalkerTest.php | 6 ++++++
 tests/Type/Doctrine/data/QueryResult/config-mysql.neon     | 7 +++++++
 2 files changed, 13 insertions(+)
 create mode 100644 tests/Type/Doctrine/data/QueryResult/config-mysql.neon

diff --git a/tests/Type/Doctrine/Query/MysqlQueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/MysqlQueryResultTypeWalkerTest.php
index cefeee97..af84e121 100644
--- a/tests/Type/Doctrine/Query/MysqlQueryResultTypeWalkerTest.php
+++ b/tests/Type/Doctrine/Query/MysqlQueryResultTypeWalkerTest.php
@@ -8,6 +8,12 @@
  */
 class MysqlQueryResultTypeWalkerTest extends PDOQueryResultTypeWalkerTest
 {
+	public static function getAdditionalConfigFiles(): array
+	{
+		return [
+			__DIR__ . '/../data/QueryResult/config-mysql.neon',
+		];
+	}
 
 	protected static function getEntityManagerPath(): string
 	{
diff --git a/tests/Type/Doctrine/data/QueryResult/config-mysql.neon b/tests/Type/Doctrine/data/QueryResult/config-mysql.neon
new file mode 100644
index 00000000..588a9821
--- /dev/null
+++ b/tests/Type/Doctrine/data/QueryResult/config-mysql.neon
@@ -0,0 +1,7 @@
+includes:
+	- ../../../../../extension.neon
+parameters:
+	doctrine:
+		objectManagerLoader: entity-manager-mysql.php
+	featureToggles:
+		listType: true