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/phpunit.xml b/phpunit.xml
index 6d69639a..8c5f02c9 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -32,6 +32,7 @@
+ tests
tests
diff --git a/tests/Type/Doctrine/Query/MysqlQueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/MysqlQueryResultTypeWalkerTest.php
new file mode 100644
index 00000000..af84e121
--- /dev/null
+++ b/tests/Type/Doctrine/Query/MysqlQueryResultTypeWalkerTest.php
@@ -0,0 +1,23 @@
+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);
+ return __DIR__ . '/../data/QueryResult/entity-manager.php';
}
- /** @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
- */
public function getTestData(): iterable
{
$ormVersion = InstalledVersions::getVersion('doctrine/orm');
@@ -590,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
',
];
@@ -1591,113 +1390,4 @@ public function getTestData(): iterable
];
}
- /**
- * @param array $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 $arrays
- *
- * @return iterable
- */
- 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..319ac1a3
--- /dev/null
+++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTestCase.php
@@ -0,0 +1,349 @@
+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
+ */
+ abstract public function getTestData(): iterable;
+
+ /**
+ * @param array $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 $arrays
+ *
+ * @return iterable
+ */
+ 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', '<');
+ }
+
+}
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
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 @@
+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
+);