From 09084c5d26a2285d55c75ba8074108a87f6f76b6 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 26 Dec 2022 21:06:02 +0100 Subject: [PATCH 01/15] Improve QueryResultDynamicReturnTypeExtension --- .../QueryResultDynamicReturnTypeExtension.php | 168 +++++++- .../Doctrine/data/QueryResult/queryResult.php | 400 +++++++++++++++++- 2 files changed, 537 insertions(+), 31 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index 20e23831..a46f96be 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -10,14 +10,21 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Doctrine\ObjectMetadataResolver; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\IterableType; +use PHPStan\Type\MixedType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeTraverser; +use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VoidType; +use function count; final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -32,6 +39,23 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn 'getSingleResult' => 0, ]; + private const METHOD_HYDRATION_MODE = [ + 'getArrayResult' => AbstractQuery::HYDRATE_ARRAY, + 'getScalarResult' => AbstractQuery::HYDRATE_SCALAR, + 'getSingleColumnResult' => AbstractQuery::HYDRATE_SCALAR_COLUMN, + 'getSingleScalarResult' => AbstractQuery::HYDRATE_SINGLE_SCALAR, + ]; + + /** @var ObjectMetadataResolver */ + private $objectMetadataResolver; + + public function __construct( + ObjectMetadataResolver $objectMetadataResolver + ) + { + $this->objectMetadataResolver = $objectMetadataResolver; + } + public function getClass(): string { return AbstractQuery::class; @@ -39,7 +63,8 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]); + return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]) + || isset(self::METHOD_HYDRATION_MODE[$methodReflection->getName()]); } public function getTypeFromMethodCall( @@ -50,21 +75,23 @@ public function getTypeFromMethodCall( { $methodName = $methodReflection->getName(); - if (!isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) { - throw new ShouldNotHappenException(); - } - - $argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName]; - $args = $methodCall->getArgs(); + if (isset(self::METHOD_HYDRATION_MODE[$methodName])) { + $hydrationMode = new ConstantIntegerType(self::METHOD_HYDRATION_MODE[$methodName]); + } elseif (isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) { + $argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName]; + $args = $methodCall->getArgs(); - if (isset($args[$argIndex])) { - $hydrationMode = $scope->getType($args[$argIndex]->value); + if (isset($args[$argIndex])) { + $hydrationMode = $scope->getType($args[$argIndex]->value); + } else { + $parametersAcceptor = ParametersAcceptorSelector::selectSingle( + $methodReflection->getVariants() + ); + $parameter = $parametersAcceptor->getParameters()[$argIndex]; + $hydrationMode = $parameter->getDefaultValue() ?? new NullType(); + } } else { - $parametersAcceptor = ParametersAcceptorSelector::selectSingle( - $methodReflection->getVariants() - ); - $parameter = $parametersAcceptor->getParameters()[$argIndex]; - $hydrationMode = $parameter->getDefaultValue() ?? new NullType(); + throw new ShouldNotHappenException(); } $queryType = $scope->getType($methodCall->var); @@ -98,12 +125,34 @@ private function getMethodReturnTypeForHydrationMode( return $this->originalReturnType($methodReflection); } - if (!$this->isObjectHydrationMode($hydrationMode)) { - // We support only HYDRATE_OBJECT. For other hydration modes, we - // return the declared return type of the method. + if (!$hydrationMode instanceof ConstantIntegerType) { return $this->originalReturnType($methodReflection); } + $singleResult = false; + switch ($hydrationMode->getValue()) { + case AbstractQuery::HYDRATE_OBJECT: + break; + case AbstractQuery::HYDRATE_ARRAY: + $queryResultType = $this->getArrayHydratedReturnType($queryResultType); + break; + case AbstractQuery::HYDRATE_SCALAR: + $queryResultType = $this->getScalarHydratedReturnType($queryResultType); + break; + case AbstractQuery::HYDRATE_SINGLE_SCALAR: + $singleResult = true; + $queryResultType = $this->getSingleScalarHydratedReturnType($queryResultType); + break; + case AbstractQuery::HYDRATE_SIMPLEOBJECT: + $queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType); + break; + case AbstractQuery::HYDRATE_SCALAR_COLUMN: + $queryResultType = $this->getScalarColumnHydratedReturnType($queryResultType); + break; + default: + return $this->originalReturnType($methodReflection); + } + switch ($methodReflection->getName()) { case 'getSingleResult': return $queryResultType; @@ -115,6 +164,10 @@ private function getMethodReturnTypeForHydrationMode( $queryResultType ); default: + if ($singleResult) { + return $queryResultType; + } + if ($queryKeyType->isNull()->yes()) { return AccessoryArrayListType::intersectWith(new ArrayType( new IntegerType(), @@ -128,13 +181,86 @@ private function getMethodReturnTypeForHydrationMode( } } - private function isObjectHydrationMode(Type $type): bool + private function getArrayHydratedReturnType(Type $queryResultType): Type + { + $objectManager = $this->objectMetadataResolver->getObjectManager(); + + return TypeTraverser::map( + $queryResultType, + static function (Type $type, callable $traverse) use ($objectManager): Type { + $isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type); + if ($isObject->no()) { + return $traverse($type); + } + if ( + $isObject->maybe() + || !$type instanceof TypeWithClassName + || $objectManager === null + ) { + return new MixedType(); + } + + return $objectManager->getMetadataFactory()->hasMetadataFor($type->getClassName()) + ? new ArrayType(new MixedType(), new MixedType()) + : $traverse($type); + } + ); + } + + private function getScalarHydratedReturnType(Type $queryResultType): Type + { + if (!$queryResultType instanceof ArrayType) { + return new ArrayType(new MixedType(), new MixedType()); + } + + $itemType = $queryResultType->getItemType(); + $hasNoObject = (new ObjectWithoutClassType())->isSuperTypeOf($itemType)->no(); + $hasNoArray = $itemType->isArray()->no(); + + if ($hasNoArray && $hasNoObject) { + return $queryResultType; + } + + return new ArrayType(new MixedType(), new MixedType()); + } + + private function getSimpleObjectHydratedReturnType(Type $queryResultType): Type { - if (!$type instanceof ConstantIntegerType) { - return false; + if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) { + return $queryResultType; + } + + return new MixedType(); + } + + private function getSingleScalarHydratedReturnType(Type $queryResultType): Type + { + $queryResultType = $this->getScalarHydratedReturnType($queryResultType); + if (!$queryResultType instanceof ConstantArrayType) { + return new MixedType(); + } + + $values = $queryResultType->getValueTypes(); + if (count($values) !== 1) { + return new MixedType(); + } + + return $queryResultType->getFirstIterableValueType(); + } + + private function getScalarColumnHydratedReturnType(Type $queryResultType): Type + { + $queryResultType = $this->getScalarHydratedReturnType($queryResultType); + if (!$queryResultType instanceof ConstantArrayType) { + return new MixedType(); + } + + $values = $queryResultType->getValueTypes(); + if (count($values) !== 1) { + return new MixedType(); } - return $type->getValue() === AbstractQuery::HYDRATE_OBJECT; + return $queryResultType->getFirstIterableValueType(); } private function originalReturnType(MethodReflection $methodReflection): Type diff --git a/tests/Type/Doctrine/data/QueryResult/queryResult.php b/tests/Type/Doctrine/data/QueryResult/queryResult.php index 02469e46..0a67f4f0 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryResult.php +++ b/tests/Type/Doctrine/data/QueryResult/queryResult.php @@ -144,11 +144,11 @@ public function testReturnTypeOfQueryMethodsWithExplicitObjectHydrationMode(Enti } /** - * Test that we properly infer the return type of Query methods with explicit hydration mode that is not HYDRATE_OBJECT + * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_ARRAY * - * We are never able to infer the return type here + * We can infer the return type by changing every object by an array */ - public function testReturnTypeOfQueryMethodsWithExplicitNonObjectHydrationMode(EntityManagerInterface $em): void + public function testReturnTypeOfQueryMethodsWithExplicitArrayHydrationMode(EntityManagerInterface $em): void { $query = $em->createQuery(' SELECT m @@ -156,35 +156,415 @@ public function testReturnTypeOfQueryMethodsWithExplicitNonObjectHydrationMode(E '); assertType( - 'mixed', + 'list', $query->getResult(AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'iterable', + 'list', + $query->getArrayResult() + ); + assertType( + 'iterable', $query->toIterable([], AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'mixed', + 'list', $query->execute(null, AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'mixed', + 'list', $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'mixed', + 'list', $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'mixed', + 'array', $query->getSingleResult(AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'mixed', + 'array|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY) + ); + + $query = $em->createQuery(' + SELECT m.intColumn, m.stringNullColumn, m.datetimeColumn + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_ARRAY) + ); + assertType( + 'list', + $query->getArrayResult() + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_ARRAY) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_ARRAY) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_ARRAY) + ); + assertType( + 'array{intColumn: int, stringNullColumn: string|null, datetimeColumn: DateTime}', + $query->getSingleResult(AbstractQuery::HYDRATE_ARRAY) + ); + assertType( + 'array{intColumn: int, stringNullColumn: string|null, datetimeColumn: DateTime}|null', $query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY) ); } + /** + * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR + */ + public function testReturnTypeOfQueryMethodsWithExplicitScalarHydrationMode(EntityManagerInterface $em): void + { + $query = $em->createQuery(' + SELECT m + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->getScalarResult() + ); + assertType( + 'iterable', + $query->toIterable([], AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'array', + $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'array|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR) + ); + + $query = $em->createQuery(' + SELECT m.intColumn, m.stringNullColumn + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->getScalarResult() + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'array{intColumn: int, stringNullColumn: string|null}', + $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'array{intColumn: int, stringNullColumn: string|null}|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR) + ); + } + + /** + * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR + */ + public function testReturnTypeOfQueryMethodsWithExplicitSingleScalarHydrationMode(EntityManagerInterface $em): void + { + $query = $em->createQuery(' + SELECT m + FROM QueryResult\Entities\Many m + '); + + assertType( + 'mixed', + $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->getSingleScalarResult() + ); + assertType( + 'iterable', + $query->toIterable([], AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + + $query = $em->createQuery(' + SELECT m.intColumn + FROM QueryResult\Entities\Many m + '); + + assertType( + 'int', + $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int', + $query->getSingleScalarResult() + ); + assertType( + 'int', + $query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int', + $query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + + $query = $em->createQuery(' + SELECT COUNT(m.id) + FROM QueryResult\Entities\Many m + '); + + assertType( + 'int<0, max>|numeric-string', + $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int<0, max>|numeric-string', + $query->getSingleScalarResult() + ); + assertType( + 'int<0, max>|numeric-string', + $query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int<0, max>|numeric-string', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int<0, max>|numeric-string', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int<0, max>|numeric-string', + $query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int<0, max>|numeric-string|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + } + + /** + * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SIMPLEOBJECT + * + * We are never able to infer the return type here + */ + public function testReturnTypeOfQueryMethodsWithExplicitSimpleObjectHydrationMode(EntityManagerInterface $em): void + { + $query = $em->createQuery(' + SELECT m + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'iterable', + $query->toIterable([], AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'QueryResult\Entities\Many', + $query->getSingleResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'QueryResult\Entities\Many|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + + $query = $em->createQuery(' + SELECT m.intColumn, m.stringNullColumn + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'mixed', + $query->getSingleResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'mixed', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + } + + /** + * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR_COLUMN + * + * We are never able to infer the return type here + */ + public function testReturnTypeOfQueryMethodsWithExplicitScalarColumnHydrationMode(EntityManagerInterface $em): void + { + $query = $em->createQuery(' + SELECT m + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->getSingleColumnResult() + ); + assertType( + 'iterable', + $query->toIterable([], AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'mixed', + $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'mixed', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + + $query = $em->createQuery(' + SELECT m.intColumn + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->getSingleColumnResult() + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'int', + $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'int|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + } + /** * Test that we properly infer the return type of Query methods with explicit hydration mode that is not a constant value * From 88928ed210f990281434976d23c6b39dd162a2bd Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 25 Feb 2023 16:22:01 +0100 Subject: [PATCH 02/15] Solve phpstan deprecation --- .../QueryResultDynamicReturnTypeExtension.php | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index a46f96be..e7357b35 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -10,7 +10,6 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Doctrine\ObjectMetadataResolver; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -209,19 +208,22 @@ static function (Type $type, callable $traverse) use ($objectManager): Type { private function getScalarHydratedReturnType(Type $queryResultType): Type { - if (!$queryResultType instanceof ArrayType) { + if (!$queryResultType->isArray()->yes()) { return new ArrayType(new MixedType(), new MixedType()); } - $itemType = $queryResultType->getItemType(); - $hasNoObject = (new ObjectWithoutClassType())->isSuperTypeOf($itemType)->no(); - $hasNoArray = $itemType->isArray()->no(); + foreach ($queryResultType->getArrays() as $arrayType) { + $itemType = $arrayType->getItemType(); - if ($hasNoArray && $hasNoObject) { - return $queryResultType; + if ( + !(new ObjectWithoutClassType())->isSuperTypeOf($itemType)->no() + || !$itemType->isArray()->no() + ) { + return new ArrayType(new MixedType(), new MixedType()); + } } - return new ArrayType(new MixedType(), new MixedType()); + return $queryResultType; } private function getSimpleObjectHydratedReturnType(Type $queryResultType): Type @@ -236,31 +238,41 @@ private function getSimpleObjectHydratedReturnType(Type $queryResultType): Type private function getSingleScalarHydratedReturnType(Type $queryResultType): Type { $queryResultType = $this->getScalarHydratedReturnType($queryResultType); - if (!$queryResultType instanceof ConstantArrayType) { + if (!$queryResultType->isConstantArray()->yes()) { return new MixedType(); } - $values = $queryResultType->getValueTypes(); - if (count($values) !== 1) { - return new MixedType(); + $types = []; + foreach ($queryResultType->getConstantArrays() as $constantArrayType) { + $values = $constantArrayType->getValueTypes(); + if (count($values) !== 1) { + return new MixedType(); + } + + $types[] = $constantArrayType->getFirstIterableValueType(); } - return $queryResultType->getFirstIterableValueType(); + return TypeCombinator::union(...$types); } private function getScalarColumnHydratedReturnType(Type $queryResultType): Type { $queryResultType = $this->getScalarHydratedReturnType($queryResultType); - if (!$queryResultType instanceof ConstantArrayType) { + if (!$queryResultType->isConstantArray()->yes()) { return new MixedType(); } - $values = $queryResultType->getValueTypes(); - if (count($values) !== 1) { - return new MixedType(); + $types = []; + foreach ($queryResultType->getConstantArrays() as $constantArrayType) { + $values = $constantArrayType->getValueTypes(); + if (count($values) !== 1) { + return new MixedType(); + } + + $types[] = $constantArrayType->getFirstIterableValueType(); } - return $queryResultType->getFirstIterableValueType(); + return TypeCombinator::union(...$types); } private function originalReturnType(MethodReflection $methodReflection): Type From 11e5333f21f50be80cef9d6f0bde1d205d059668 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 5 Apr 2023 12:14:27 +0200 Subject: [PATCH 03/15] Avoid false positive about array result --- .../QueryResultDynamicReturnTypeExtension.php | 11 ++++++++--- .../Doctrine/data/QueryResult/queryResult.php | 16 ++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index e7357b35..7d047768 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -199,9 +199,14 @@ static function (Type $type, callable $traverse) use ($objectManager): Type { return new MixedType(); } - return $objectManager->getMetadataFactory()->hasMetadataFor($type->getClassName()) - ? new ArrayType(new MixedType(), new MixedType()) - : $traverse($type); + if (!$objectManager->getMetadataFactory()->hasMetadataFor($type->getClassName())) { + return $traverse($type); + } + + // We could return `new ArrayTyp(new MixedType(), new MixedType())` + // but the lack of precision in the array keys/values would give false positive + // @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934 + return new MixedType(); } ); } diff --git a/tests/Type/Doctrine/data/QueryResult/queryResult.php b/tests/Type/Doctrine/data/QueryResult/queryResult.php index 0a67f4f0..272744df 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryResult.php +++ b/tests/Type/Doctrine/data/QueryResult/queryResult.php @@ -156,35 +156,35 @@ public function testReturnTypeOfQueryMethodsWithExplicitArrayHydrationMode(Entit '); assertType( - 'list', + 'list', $query->getResult(AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'list', + 'list', $query->getArrayResult() ); assertType( - 'iterable', + 'iterable', $query->toIterable([], AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'list', + 'list', $query->execute(null, AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'list', + 'list', $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'list', + 'list', $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'array', + 'mixed', $query->getSingleResult(AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'array|null', + 'mixed', $query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY) ); From 3ce312a4a2579804229d6c2ef0a4a4e5449c3a65 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 5 Apr 2023 19:58:58 +0200 Subject: [PATCH 04/15] Use benevolent union for scalar in queries --- .../Doctrine/Query/QueryResultTypeWalker.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 14519922..dc375af8 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -797,7 +797,7 @@ public function walkSelectExpression($selectExpression): string $type = $this->resolveDoctrineType($typeName, $enumType, $nullable); - $this->typeBuilder->addScalar($resultAlias, $type); + $this->addScalar($resultAlias, $type); return ''; } @@ -857,7 +857,7 @@ public function walkSelectExpression($selectExpression): string }); } - $this->typeBuilder->addScalar($resultAlias, $type); + $this->addScalar($resultAlias, $type); return ''; } @@ -1298,6 +1298,18 @@ public function walkResultVariable($resultVariable): string return $this->marshalType(new MixedType()); } + /** + * @param array-key $alias + */ + private function addScalar($alias, Type $type): void + { + if ($type instanceof UnionType) { + $type = TypeUtils::toBenevolentUnion($type); + } + + $this->typeBuilder->addScalar($alias, $type); + } + private function unmarshalType(string $marshalledType): Type { $type = unserialize($marshalledType); From 2b3fe65f248584ccaa0d32348bb9fc219e627078 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 5 Apr 2023 21:04:39 +0200 Subject: [PATCH 05/15] Only use benevolent when where clause is used --- src/Type/Doctrine/Query/QueryResultTypeWalker.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index dc375af8..33d99f7a 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -108,6 +108,9 @@ class QueryResultTypeWalker extends SqlWalker /** @var bool */ private $hasGroupByClause; + /** @var bool */ + private $hasCondition; + /** * @param Query $query */ @@ -136,6 +139,7 @@ public function __construct($query, $parserResult, array $queryComponents) $this->nullableQueryComponents = []; $this->hasAggregateFunction = false; $this->hasGroupByClause = false; + $this->hasCondition = false; // The object is instantiated by Doctrine\ORM\Query\Parser, so receiving // dependencies through the constructor is not an option. Instead, we @@ -592,6 +596,8 @@ public function walkOrderByItem($orderByItem): string */ public function walkHavingClause($havingClause): string { + $this->hasCondition = true; + return $this->marshalType(new MixedType()); } @@ -1016,6 +1022,8 @@ public function walkWhereClause($whereClause): string */ public function walkConditionalExpression($condExpr): string { + $this->hasCondition = true; + return $this->marshalType(new MixedType()); } @@ -1303,7 +1311,10 @@ public function walkResultVariable($resultVariable): string */ private function addScalar($alias, Type $type): void { - if ($type instanceof UnionType) { + // Since we don't check the condition inside the WHERE or HAVING + // conditions, we cannot be sure all the union types are correct. + // For exemple, a condition `WHERE foo.bar IS NOT NULL` could be added. + if ($this->hasCondition && $type instanceof UnionType) { $type = TypeUtils::toBenevolentUnion($type); } From 10480a66903075ab356b4603acee7d2a49357424 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 5 Apr 2023 21:09:29 +0200 Subject: [PATCH 06/15] Use benevolent union when we cast values --- src/Type/Doctrine/Query/QueryResultTypeWalker.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 33d99f7a..1bbe5f2b 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -849,18 +849,27 @@ public function walkSelectExpression($selectExpression): string // the driver and PHP version. // Here we assume that the value may or may not be casted to // string by the driver. - $type = TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + $casted = false; + $type = TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$casted): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } if ($type instanceof IntegerType || $type instanceof FloatType) { + $casted = true; return TypeCombinator::union($type->toString(), $type); } if ($type instanceof BooleanType) { + $casted = true; return TypeCombinator::union($type->toInteger()->toString(), $type); } return $traverse($type); }); + + // Since we made supposition about possibly casted values, + // we can only provide a benevolent union. + if ($casted && $type instanceof UnionType) { + $type = TypeUtils::toBenevolentUnion($type); + } } $this->addScalar($resultAlias, $type); From 651c3d92b4629127bac5183810084f3637c10ff5 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 6 Apr 2023 09:29:32 +0200 Subject: [PATCH 07/15] Fix tests --- .../Query/QueryResultTypeWalkerTest.php | 186 ++++++++++-------- 1 file changed, 100 insertions(+), 86 deletions(-) diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index ea1e0aea..245d7178 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -13,6 +13,7 @@ use Doctrine\ORM\Tools\SchemaTool; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -28,6 +29,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use QueryResult\Entities\Embedded; @@ -551,12 +553,12 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(2), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(1), new ConstantIntegerType(2), new ConstantStringType('1'), new ConstantStringType('2') - ), + )), ], [ new ConstantIntegerType(3), @@ -603,7 +605,7 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(1), - TypeCombinator::addNull($this->intStringified()), + $this->intStringified(true), ], [ new ConstantIntegerType(2), @@ -617,7 +619,7 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(4), - TypeCombinator::addNull($this->intStringified()), + $this->intStringified(true), ], [ new ConstantIntegerType(5), @@ -627,7 +629,7 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(6), - TypeCombinator::addNull($this->intStringified()), + $this->intStringified(true), ], [ new ConstantIntegerType(7), @@ -653,7 +655,7 @@ public function getTestData(): iterable ], [ new ConstantStringType('max'), - TypeCombinator::addNull($this->intStringified()), + $this->intStringified(true), ], [ new ConstantStringType('arithmetic'), @@ -761,10 +763,10 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantStringType('1'), new ConstantIntegerType(1) - ), + )), ], [new ConstantIntegerType(2), new ConstantStringType('hello')], ]), @@ -778,11 +780,11 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(1), new ConstantStringType('1'), new NullType() - ), + )), ], ]), ' @@ -795,10 +797,10 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new StringType(), new IntegerType() - ), + )), ], [ new ConstantIntegerType(2), @@ -824,10 +826,10 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new StringType(), new ConstantIntegerType(0) - ), + )), ], ]), ' @@ -844,10 +846,10 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new StringType(), new ConstantIntegerType(0) - ), + )), ], ]), ' @@ -864,12 +866,12 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(0), new ConstantIntegerType(1), new ConstantStringType('0'), new ConstantStringType('1') - ), + )), ], ]), ' @@ -885,12 +887,12 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(0), new ConstantIntegerType(1), new ConstantStringType('0'), new ConstantStringType('1') - ), + )), ], ]), ' @@ -906,31 +908,31 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(1), new ConstantStringType('1') - ), + )), ], [ new ConstantIntegerType(2), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(0), new ConstantStringType('0') - ), + )), ], [ new ConstantIntegerType(3), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(1), new ConstantStringType('1') - ), + )), ], [ new ConstantIntegerType(4), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(0), new ConstantStringType('0') - ), + )), ], ]), ' @@ -1102,10 +1104,10 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(2), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(1), new ConstantStringType('1') - ), + )), ], [ new ConstantStringType('intColumn'), @@ -1149,7 +1151,7 @@ public function getTestData(): iterable yield 'new arguments affect scalar counter' => [ $this->constantArray([ - [new ConstantIntegerType(5), TypeCombinator::addNull($this->intStringified())], + [new ConstantIntegerType(5), $this->intStringified(true)], [new ConstantIntegerType(0), new ObjectType(ManyId::class)], [new ConstantIntegerType(1), new ObjectType(OneId::class)], ]), @@ -1166,7 +1168,7 @@ public function getTestData(): iterable [new ConstantStringType('intColumn'), new IntegerType()], [new ConstantIntegerType(1), $this->intStringified()], [new ConstantIntegerType(2), $this->intStringified()], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->intStringified())], + [new ConstantIntegerType(3), $this->intStringified(true)], [new ConstantIntegerType(4), $this->intStringified()], [new ConstantIntegerType(5), $this->intStringified()], [new ConstantIntegerType(6), $this->numericStringified()], @@ -1188,9 +1190,9 @@ public function getTestData(): iterable yield 'abs function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->unumericStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->unumericStringified())], + [new ConstantIntegerType(2), $this->unumericStringified(true)], [new ConstantIntegerType(3), $this->unumericStringified()], - [new ConstantIntegerType(4), TypeCombinator::union($this->unumericStringified())], + [new ConstantIntegerType(4), $this->unumericStringified()], ]), ' SELECT ABS(m.intColumn), @@ -1204,7 +1206,7 @@ public function getTestData(): iterable yield 'bit_and function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], + [new ConstantIntegerType(2), $this->uintStringified(true)], [new ConstantIntegerType(3), $this->uintStringified()], ]), ' @@ -1218,7 +1220,7 @@ public function getTestData(): iterable yield 'bit_or function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], + [new ConstantIntegerType(2), $this->uintStringified(true)], [new ConstantIntegerType(3), $this->uintStringified()], ]), ' @@ -1296,8 +1298,8 @@ public function getTestData(): iterable yield 'date_diff function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->numericStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->numericStringified())], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->numericStringified())], + [new ConstantIntegerType(2), $this->numericStringified(true)], + [new ConstantIntegerType(3), $this->numericStringified(true)], [new ConstantIntegerType(4), $this->numericStringified()], ]), ' @@ -1339,11 +1341,9 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(2), - TypeCombinator::addNull( - $this->hasTypedExpressions() - ? $this->uint() - : $this->uintStringified() - ), + $this->hasTypedExpressions() + ? $this->uint(true) + : $this->uintStringified(true) ], [ new ConstantIntegerType(3), @@ -1364,8 +1364,8 @@ public function getTestData(): iterable yield 'locate function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->uintStringified())], + [new ConstantIntegerType(2), $this->uintStringified(true)], + [new ConstantIntegerType(3), $this->uintStringified(true)], [new ConstantIntegerType(4), $this->uintStringified()], ]), ' @@ -1407,8 +1407,8 @@ public function getTestData(): iterable yield 'mod function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->uintStringified())], + [new ConstantIntegerType(2), $this->uintStringified(true)], + [new ConstantIntegerType(3), $this->uintStringified(true)], [new ConstantIntegerType(4), $this->uintStringified()], ]), ' @@ -1422,7 +1422,7 @@ public function getTestData(): iterable yield 'mod function error' => [ $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->uintStringified())], + [new ConstantIntegerType(1), $this->uintStringified(true)], ]), ' SELECT MOD(10, NULLIF(m.intColumn, m.intColumn)) @@ -1481,15 +1481,15 @@ public function getTestData(): iterable yield 'identity function' => [ $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], - [new ConstantIntegerType(2), $this->numericStringOrInt()], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(1), $this->intStringified(true)], + [new ConstantIntegerType(2), $this->intStringified()], + [new ConstantIntegerType(3), $this->intStringified(true)], [new ConstantIntegerType(4), TypeCombinator::addNull(new StringType())], [new ConstantIntegerType(5), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(6), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(6), $this->intStringified(true)], [new ConstantIntegerType(7), TypeCombinator::addNull(new MixedType())], - [new ConstantIntegerType(8), TypeCombinator::addNull($this->numericStringOrInt())], - [new ConstantIntegerType(9), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(8), $this->intStringified(true)], + [new ConstantIntegerType(9), $this->intStringified(true)], ]), ' SELECT IDENTITY(m.oneNull), @@ -1508,7 +1508,7 @@ public function getTestData(): iterable yield 'select nullable association' => [ $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(1), $this->intStringified(true)], ]), ' SELECT DISTINCT(m.oneNull) @@ -1518,7 +1518,7 @@ public function getTestData(): iterable yield 'select non null association' => [ $this->constantArray([ - [new ConstantIntegerType(1), $this->numericStringOrInt()], + [new ConstantIntegerType(1), $this->intStringified()], ]), ' SELECT DISTINCT(m.one) @@ -1528,7 +1528,7 @@ public function getTestData(): iterable yield 'select default nullability association' => [ $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(1), $this->intStringified(true)], ]), ' SELECT DISTINCT(m.oneDefaultNullability) @@ -1538,7 +1538,7 @@ public function getTestData(): iterable yield 'select non null association in aggregated query' => [ $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(1), $this->intStringified(true)], [ new ConstantIntegerType(2), $this->hasTypedExpressions() @@ -1608,17 +1608,6 @@ private function constantArray(array $elements): Type 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([ @@ -1627,42 +1616,67 @@ private function numericString(): Type ]); } - private function uint(): Type + private function uint(bool $nullable = false): Type { - return IntegerRangeType::fromInterval(0, null); + $type = IntegerRangeType::fromInterval(0, null); + if ($nullable) { + TypeCombinator::addNull($type); + } + + return $type; } - private function intStringified(): Type + private function intStringified(bool $nullable = false): Type { - return TypeCombinator::union( + $types = [ new IntegerType(), - $this->numericString() - ); + $this->numericString(), + ]; + if ($nullable) { + $types[] = new NullType(); + } + + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); } - private function uintStringified(): Type + private function uintStringified(bool $nullable = false): Type { - return TypeCombinator::union( + $types = [ $this->uint(), - $this->numericString() - ); + $this->numericString(), + ]; + if ($nullable) { + $types[] = new NullType(); + } + + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); } - private function numericStringified(): Type + private function numericStringified(bool $nullable = false): Type { - return TypeCombinator::union( + $types = [ new FloatType(), new IntegerType(), - $this->numericString() - ); + $this->numericString(), + ]; + if ($nullable) { + $types[] = new NullType(); + } + + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); } - private function unumericStringified(): Type + private function unumericStringified(bool $nullable = false): Type { - return TypeCombinator::union( + $types = [ new FloatType(), IntegerRangeType::fromInterval(0, null), $this->numericString() - ); + ]; + if ($nullable) { + $types[] = new NullType(); + } + + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); } private function hasTypedExpressions(): bool From 2516fe5039da778bd52b8ad617f35f60be82e8cf Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 6 Apr 2023 09:43:46 +0200 Subject: [PATCH 08/15] Avoid useless benevolent unions --- src/Type/Doctrine/Query/QueryResultTypeWalker.php | 4 +++- .../Doctrine/Query/QueryResultTypeWalkerTest.php | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 1bbe5f2b..397c5792 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -850,6 +850,8 @@ public function walkSelectExpression($selectExpression): string // Here we assume that the value may or may not be casted to // string by the driver. $casted = false; + $originalType = $type; + $type = TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$casted): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); @@ -867,7 +869,7 @@ public function walkSelectExpression($selectExpression): string // Since we made supposition about possibly casted values, // we can only provide a benevolent union. - if ($casted && $type instanceof UnionType) { + if ($casted && $type instanceof UnionType && !$originalType->equals($type)) { $type = TypeUtils::toBenevolentUnion($type); } } diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 245d7178..ee9f8c6d 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -797,10 +797,10 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeUtils::toBenevolentUnion(TypeCombinator::union( + TypeCombinator::union( new StringType(), new IntegerType() - )), + ), ], [ new ConstantIntegerType(2), @@ -826,10 +826,10 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeUtils::toBenevolentUnion(TypeCombinator::union( + TypeCombinator::union( new StringType(), new ConstantIntegerType(0) - )), + ), ], ]), ' @@ -846,10 +846,10 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeUtils::toBenevolentUnion(TypeCombinator::union( + TypeCombinator::union( new StringType(), new ConstantIntegerType(0) - )), + ), ], ]), ' From 93b6bbc0796c80097633d4f57627bb46e54946c6 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 6 Apr 2023 09:51:03 +0200 Subject: [PATCH 09/15] Fix more tests --- .../QueryResultDynamicReturnTypeExtension.php | 9 ++++++++- .../Doctrine/Query/QueryResultTypeWalkerTest.php | 6 ++---- .../Type/Doctrine/data/QueryResult/queryResult.php | 14 +++++++------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index 7d047768..96b3f7bc 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -10,6 +10,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Doctrine\ObjectMetadataResolver; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -21,6 +22,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; +use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VoidType; use function count; @@ -156,7 +158,12 @@ private function getMethodReturnTypeForHydrationMode( case 'getSingleResult': return $queryResultType; case 'getOneOrNullResult': - return TypeCombinator::addNull($queryResultType); + $nullableQueryResultType = TypeCombinator::addNull($queryResultType); + if ($queryResultType instanceof BenevolentUnionType) { + $nullableQueryResultType = TypeUtils::toBenevolentUnion($nullableQueryResultType); + } + + return $nullableQueryResultType; case 'toIterable': return new IterableType( $queryKeyType->isNull()->yes() ? new IntegerType() : $queryKeyType, diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index ee9f8c6d..e789e312 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -13,7 +13,6 @@ use Doctrine\ORM\Tools\SchemaTool; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Accessory\AccessoryNumericStringType; -use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -30,7 +29,6 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; -use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use QueryResult\Entities\Embedded; use QueryResult\Entities\JoinedChild; @@ -1343,7 +1341,7 @@ public function getTestData(): iterable new ConstantIntegerType(2), $this->hasTypedExpressions() ? $this->uint(true) - : $this->uintStringified(true) + : $this->uintStringified(true), ], [ new ConstantIntegerType(3), @@ -1670,7 +1668,7 @@ private function unumericStringified(bool $nullable = false): Type $types = [ new FloatType(), IntegerRangeType::fromInterval(0, null), - $this->numericString() + $this->numericString(), ]; if ($nullable) { $types[] = new NullType(); diff --git a/tests/Type/Doctrine/data/QueryResult/queryResult.php b/tests/Type/Doctrine/data/QueryResult/queryResult.php index 272744df..d6972943 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryResult.php +++ b/tests/Type/Doctrine/data/QueryResult/queryResult.php @@ -384,31 +384,31 @@ public function testReturnTypeOfQueryMethodsWithExplicitSingleScalarHydrationMod '); assertType( - 'int<0, max>|numeric-string', + '(int<0, max>|numeric-string)', $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) ); assertType( - 'int<0, max>|numeric-string', + '(int<0, max>|numeric-string)', $query->getSingleScalarResult() ); assertType( - 'int<0, max>|numeric-string', + '(int<0, max>|numeric-string)', $query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) ); assertType( - 'int<0, max>|numeric-string', + '(int<0, max>|numeric-string)', $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) ); assertType( - 'int<0, max>|numeric-string', + '(int<0, max>|numeric-string)', $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) ); assertType( - 'int<0, max>|numeric-string', + '(int<0, max>|numeric-string)', $query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) ); assertType( - 'int<0, max>|numeric-string|null', + '(int<0, max>|numeric-string|null)', $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) ); } From 601591e178c4c923785e061236be7eefce5cf238 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 6 Apr 2023 10:27:06 +0200 Subject: [PATCH 10/15] Fix check for where condition --- .../Doctrine/Query/QueryResultTypeWalker.php | 13 +++++-------- .../Query/QueryResultTypeWalkerTest.php | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 397c5792..04b5c1e6 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -109,7 +109,7 @@ class QueryResultTypeWalker extends SqlWalker private $hasGroupByClause; /** @var bool */ - private $hasCondition; + private $hasWhereClause; /** * @param Query $query @@ -139,7 +139,7 @@ public function __construct($query, $parserResult, array $queryComponents) $this->nullableQueryComponents = []; $this->hasAggregateFunction = false; $this->hasGroupByClause = false; - $this->hasCondition = false; + $this->hasWhereClause = false; // The object is instantiated by Doctrine\ORM\Query\Parser, so receiving // dependencies through the constructor is not an option. Instead, we @@ -179,6 +179,7 @@ public function walkSelectStatement(AST\SelectStatement $AST): string $this->typeBuilder->setSelectQuery(); $this->hasAggregateFunction = $this->hasAggregateFunction($AST); $this->hasGroupByClause = $AST->groupByClause !== null; + $this->hasWhereClause = $AST->whereClause !== null; $this->walkFromClause($AST->fromClause); @@ -596,8 +597,6 @@ public function walkOrderByItem($orderByItem): string */ public function walkHavingClause($havingClause): string { - $this->hasCondition = true; - return $this->marshalType(new MixedType()); } @@ -1033,8 +1032,6 @@ public function walkWhereClause($whereClause): string */ public function walkConditionalExpression($condExpr): string { - $this->hasCondition = true; - return $this->marshalType(new MixedType()); } @@ -1322,10 +1319,10 @@ public function walkResultVariable($resultVariable): string */ private function addScalar($alias, Type $type): void { - // Since we don't check the condition inside the WHERE or HAVING + // Since we don't check the condition inside the WHERE // conditions, we cannot be sure all the union types are correct. // For exemple, a condition `WHERE foo.bar IS NOT NULL` could be added. - if ($this->hasCondition && $type instanceof UnionType) { + if ($this->hasWhereClause && $type instanceof UnionType) { $type = TypeUtils::toBenevolentUnion($type); } diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index e789e312..408e9c98 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -449,6 +449,25 @@ public function getTestData(): iterable ', ]; + yield 'scalar with where condition' => [ + $this->constantArray([ + [new ConstantStringType('intColumn'), new IntegerType()], + [new ConstantStringType('stringColumn'), new StringType()], + [ + new ConstantStringType('stringNullColumn'), + TypeUtils::toBenevolentUnion(TypeCombinator::addNull(new StringType())) + ], + [new ConstantStringType('datetimeColumn'), new ObjectType(DateTime::class)], + [new ConstantStringType('datetimeImmutableColumn'), new ObjectType(DateTimeImmutable::class)], + ]), + ' + SELECT m.intColumn, m.stringColumn, m.stringNullColumn, + m.datetimeColumn, m.datetimeImmutableColumn + FROM QueryResult\Entities\Many m + WHERE m.stringNullColumn IS NOT NULL + ', + ]; + yield 'scalar with alias' => [ $this->constantArray([ [new ConstantStringType('i'), new IntegerType()], From 032df01ff664fbd346594e3c1d54382cbceb74a2 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 6 Apr 2023 10:37:39 +0200 Subject: [PATCH 11/15] Fix cs --- tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 408e9c98..64bbb291 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -455,7 +455,7 @@ public function getTestData(): iterable [new ConstantStringType('stringColumn'), new StringType()], [ new ConstantStringType('stringNullColumn'), - TypeUtils::toBenevolentUnion(TypeCombinator::addNull(new StringType())) + TypeUtils::toBenevolentUnion(TypeCombinator::addNull(new StringType())), ], [new ConstantStringType('datetimeColumn'), new ObjectType(DateTime::class)], [new ConstantStringType('datetimeImmutableColumn'), new ObjectType(DateTimeImmutable::class)], From 6e89ae215868d139b1fe53a90081628d7a0ce532 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 17 Jan 2024 11:13:22 +0100 Subject: [PATCH 12/15] Rely on default type when it cannot be precisely inferred --- .../QueryResultDynamicReturnTypeExtension.php | 63 +++++++++++++------ .../Doctrine/data/QueryResult/queryResult.php | 52 +++++++-------- 2 files changed, 70 insertions(+), 45 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index 96b3f7bc..d4f4aa2d 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -154,6 +154,10 @@ private function getMethodReturnTypeForHydrationMode( return $this->originalReturnType($methodReflection); } + if (null === $queryResultType) { + return $this->originalReturnType($methodReflection); + } + switch ($methodReflection->getName()) { case 'getSingleResult': return $queryResultType; @@ -187,13 +191,21 @@ private function getMethodReturnTypeForHydrationMode( } } - private function getArrayHydratedReturnType(Type $queryResultType): Type + /** + * When we're array-hydrating object, we're not sure of the shape of the array. + * We could return `new ArrayTyp(new MixedType(), new MixedType())` + * but the lack of precision in the array keys/values would give false positive. + * + * @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934 + */ + private function getArrayHydratedReturnType(Type $queryResultType): ?Type { $objectManager = $this->objectMetadataResolver->getObjectManager(); - return TypeTraverser::map( + $mixedFound = false; + $queryResultType = TypeTraverser::map( $queryResultType, - static function (Type $type, callable $traverse) use ($objectManager): Type { + static function (Type $type, callable $traverse) use ($objectManager, &$mixedFound): Type { $isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type); if ($isObject->no()) { return $traverse($type); @@ -203,6 +215,8 @@ static function (Type $type, callable $traverse) use ($objectManager): Type { || !$type instanceof TypeWithClassName || $objectManager === null ) { + $mixedFound = true; + return new MixedType(); } @@ -210,18 +224,26 @@ static function (Type $type, callable $traverse) use ($objectManager): Type { return $traverse($type); } - // We could return `new ArrayTyp(new MixedType(), new MixedType())` - // but the lack of precision in the array keys/values would give false positive - // @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934 + $mixedFound = true; + return new MixedType(); } ); + + return $mixedFound ? null : $queryResultType; } - private function getScalarHydratedReturnType(Type $queryResultType): Type + /** + * When we're scalar-hydrating object, we're not sure of the shape of the array. + * We could return `new ArrayTyp(new MixedType(), new MixedType())` + * but the lack of precision in the array keys/values would give false positive. + * + * @see https://github.com/phpstan/phpstan-doctrine/pull/453#issuecomment-1895415544 + */ + private function getScalarHydratedReturnType(Type $queryResultType): ?Type { if (!$queryResultType->isArray()->yes()) { - return new ArrayType(new MixedType(), new MixedType()); + return null; } foreach ($queryResultType->getArrays() as $arrayType) { @@ -231,34 +253,37 @@ private function getScalarHydratedReturnType(Type $queryResultType): Type !(new ObjectWithoutClassType())->isSuperTypeOf($itemType)->no() || !$itemType->isArray()->no() ) { - return new ArrayType(new MixedType(), new MixedType()); + // We could return `new ArrayTyp(new MixedType(), new MixedType())` + // but the lack of precision in the array keys/values would give false positive + // @see https://github.com/phpstan/phpstan-doctrine/pull/453#issuecomment-1895415544 + return null; } } return $queryResultType; } - private function getSimpleObjectHydratedReturnType(Type $queryResultType): Type + private function getSimpleObjectHydratedReturnType(Type $queryResultType): ?Type { if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) { return $queryResultType; } - return new MixedType(); + return null; } - private function getSingleScalarHydratedReturnType(Type $queryResultType): Type + private function getSingleScalarHydratedReturnType(Type $queryResultType): ?Type { $queryResultType = $this->getScalarHydratedReturnType($queryResultType); - if (!$queryResultType->isConstantArray()->yes()) { - return new MixedType(); + if (null === $queryResultType || !$queryResultType->isConstantArray()->yes()) { + return null; } $types = []; foreach ($queryResultType->getConstantArrays() as $constantArrayType) { $values = $constantArrayType->getValueTypes(); if (count($values) !== 1) { - return new MixedType(); + return null; } $types[] = $constantArrayType->getFirstIterableValueType(); @@ -267,18 +292,18 @@ private function getSingleScalarHydratedReturnType(Type $queryResultType): Type return TypeCombinator::union(...$types); } - private function getScalarColumnHydratedReturnType(Type $queryResultType): Type + private function getScalarColumnHydratedReturnType(Type $queryResultType): ?Type { $queryResultType = $this->getScalarHydratedReturnType($queryResultType); - if (!$queryResultType->isConstantArray()->yes()) { - return new MixedType(); + if (null === $queryResultType || !$queryResultType->isConstantArray()->yes()) { + return null; } $types = []; foreach ($queryResultType->getConstantArrays() as $constantArrayType) { $values = $constantArrayType->getValueTypes(); if (count($values) !== 1) { - return new MixedType(); + return null; } $types[] = $constantArrayType->getFirstIterableValueType(); diff --git a/tests/Type/Doctrine/data/QueryResult/queryResult.php b/tests/Type/Doctrine/data/QueryResult/queryResult.php index d6972943..da6ff38e 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryResult.php +++ b/tests/Type/Doctrine/data/QueryResult/queryResult.php @@ -156,27 +156,27 @@ public function testReturnTypeOfQueryMethodsWithExplicitArrayHydrationMode(Entit '); assertType( - 'list', + 'mixed', $query->getResult(AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'list', + 'array', $query->getArrayResult() ); assertType( - 'iterable', + 'iterable', $query->toIterable([], AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'list', + 'mixed', $query->execute(null, AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'list', + 'mixed', $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'list', + 'mixed', $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_ARRAY) ); assertType( @@ -234,35 +234,35 @@ public function testReturnTypeOfQueryMethodsWithExplicitScalarHydrationMode(Enti '); assertType( - 'list', + 'mixed', $query->getResult(AbstractQuery::HYDRATE_SCALAR) ); assertType( - 'list', + 'array', $query->getScalarResult() ); assertType( - 'iterable', + 'iterable', $query->toIterable([], AbstractQuery::HYDRATE_SCALAR) ); assertType( - 'list', + 'mixed', $query->execute(null, AbstractQuery::HYDRATE_SCALAR) ); assertType( - 'list', + 'mixed', $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR) ); assertType( - 'list', + 'mixed', $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR) ); assertType( - 'array', + 'mixed', $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR) ); assertType( - 'array|null', + 'mixed', $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR) ); @@ -316,11 +316,11 @@ public function testReturnTypeOfQueryMethodsWithExplicitSingleScalarHydrationMod $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) ); assertType( - 'mixed', + 'bool|float|int|string|null', $query->getSingleScalarResult() ); assertType( - 'iterable', + 'iterable', $query->toIterable([], AbstractQuery::HYDRATE_SINGLE_SCALAR) ); assertType( @@ -460,19 +460,19 @@ public function testReturnTypeOfQueryMethodsWithExplicitSimpleObjectHydrationMod '); assertType( - 'list', + 'mixed', $query->getResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) ); assertType( - 'list', + 'mixed', $query->execute(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) ); assertType( - 'list', + 'mixed', $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) ); assertType( - 'list', + 'mixed', $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) ); assertType( @@ -498,27 +498,27 @@ public function testReturnTypeOfQueryMethodsWithExplicitScalarColumnHydrationMod '); assertType( - 'list', + 'mixed', $query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) ); assertType( - 'list', + 'array', $query->getSingleColumnResult() ); assertType( - 'iterable', + 'iterable', $query->toIterable([], AbstractQuery::HYDRATE_SCALAR_COLUMN) ); assertType( - 'list', + 'mixed', $query->execute(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) ); assertType( - 'list', + 'mixed', $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) ); assertType( - 'list', + 'mixed', $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) ); assertType( From 0b882d955f1440d3d9623b14331d9006d78f03df Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 17 Jan 2024 11:24:12 +0100 Subject: [PATCH 13/15] Fix cs --- .../Query/QueryResultDynamicReturnTypeExtension.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index d4f4aa2d..174e94a7 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -154,7 +154,7 @@ private function getMethodReturnTypeForHydrationMode( return $this->originalReturnType($methodReflection); } - if (null === $queryResultType) { + if ($queryResultType === null) { return $this->originalReturnType($methodReflection); } @@ -275,7 +275,7 @@ private function getSimpleObjectHydratedReturnType(Type $queryResultType): ?Type private function getSingleScalarHydratedReturnType(Type $queryResultType): ?Type { $queryResultType = $this->getScalarHydratedReturnType($queryResultType); - if (null === $queryResultType || !$queryResultType->isConstantArray()->yes()) { + if ($queryResultType === null || !$queryResultType->isConstantArray()->yes()) { return null; } @@ -295,7 +295,7 @@ private function getSingleScalarHydratedReturnType(Type $queryResultType): ?Type private function getScalarColumnHydratedReturnType(Type $queryResultType): ?Type { $queryResultType = $this->getScalarHydratedReturnType($queryResultType); - if (null === $queryResultType || !$queryResultType->isConstantArray()->yes()) { + if ($queryResultType === null || !$queryResultType->isConstantArray()->yes()) { return null; } From 7ec8ad2c5187b75124a005646e550f4cc5a501db Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 18 Apr 2024 14:42:40 +0200 Subject: [PATCH 14/15] Fix --- .../Query/QueryResultTypeWalkerTest.php | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 64bbb291..3c91ce8c 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -704,63 +704,63 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantStringType('1'), new ConstantIntegerType(1), new NullType() - ), + )), ], [ new ConstantIntegerType(2), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantStringType('0'), new ConstantIntegerType(0), new ConstantStringType('1'), new ConstantIntegerType(1), new NullType() - ), + )), ], [ new ConstantIntegerType(3), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantStringType('1'), new ConstantIntegerType(1), new NullType() - ), + )), ], [ new ConstantIntegerType(4), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantStringType('0'), new ConstantIntegerType(0), new ConstantStringType('1'), new ConstantIntegerType(1), new NullType() - ), + )), ], [ new ConstantIntegerType(5), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( $this->intStringified(), new FloatType(), new NullType() - ), + )), ], [ new ConstantIntegerType(6), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( $this->intStringified(), new FloatType(), new NullType() - ), + )), ], [ new ConstantIntegerType(7), - TypeCombinator::addNull($this->intStringified()), + TypeUtils::toBenevolentUnion(TypeCombinator::addNull($this->intStringified())), ], [ new ConstantIntegerType(8), - TypeCombinator::addNull($this->intStringified()), + TypeUtils::toBenevolentUnion(TypeCombinator::addNull($this->intStringified())), ], ]), ' From aded0cf799795f978cd3117d91ebe56d4553aee3 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 18 Apr 2024 14:48:39 +0200 Subject: [PATCH 15/15] Fix --- .../Doctrine/Query/QueryResultDynamicReturnTypeExtension.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index 174e94a7..fdeb0ea3 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -220,7 +220,9 @@ static function (Type $type, callable $traverse) use ($objectManager, &$mixedFou return new MixedType(); } - if (!$objectManager->getMetadataFactory()->hasMetadataFor($type->getClassName())) { + /** @var class-string $className */ + $className = $type->getClassName(); + if (!$objectManager->getMetadataFactory()->hasMetadataFor($className)) { return $traverse($type); }