From 9d2f535404ebf0fae0a22774d109e17bb3c315b1 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 28 Jun 2024 11:05:33 +0200 Subject: [PATCH 1/4] Test hydration modes --- extension.neon | 2 + .../HydrationModeReturnTypeResolver.php | 143 +++++++++ .../QueryResultDynamicReturnTypeExtension.php | 138 +-------- ...QueryResultTypeWalkerHydrationModeTest.php | 278 ++++++++++++++++++ .../data/QueryResult/Entities/Simple.php | 71 +++++ 5 files changed, 506 insertions(+), 126 deletions(-) create mode 100644 src/Type/Doctrine/HydrationModeReturnTypeResolver.php create mode 100644 tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php create mode 100644 tests/Type/Doctrine/data/QueryResult/Entities/Simple.php diff --git a/extension.neon b/extension.neon index fe686a66..81469b91 100644 --- a/extension.neon +++ b/extension.neon @@ -91,6 +91,8 @@ services: - class: PHPStan\Doctrine\Driver\DriverDetector + - + class: PHPStan\Type\Doctrine\HydrationModeReturnTypeResolver - class: PHPStan\Reflection\Doctrine\DoctrineSelectableClassReflectionExtension - diff --git a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php new file mode 100644 index 00000000..798856c7 --- /dev/null +++ b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php @@ -0,0 +1,143 @@ +isSuperTypeOf($queryResultType); + + if ($isVoidType->yes()) { + // A void query result type indicates an UPDATE or DELETE query. + // In this case all methods return the number of affected rows. + return new IntegerType(); + } + + if ($isVoidType->maybe()) { + // We can't be sure what the query type is, so we return the + // declared return type of the method. + return null; + } + + switch ($hydrationMode) { + case AbstractQuery::HYDRATE_OBJECT: + break; + case AbstractQuery::HYDRATE_ARRAY: + $queryResultType = $this->getArrayHydratedReturnType($queryResultType); + break; + case AbstractQuery::HYDRATE_SIMPLEOBJECT: + $queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType); + break; + default: + return null; + } + + if ($queryResultType === null) { + return null; + } + + switch ($methodName) { + case 'getSingleResult': + return $queryResultType; + case 'getOneOrNullResult': + $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, + $queryResultType + ); + default: + if ($queryKeyType->isNull()->yes()) { + return AccessoryArrayListType::intersectWith(new ArrayType( + new IntegerType(), + $queryResultType + )); + } + return new ArrayType( + $queryKeyType, + $queryResultType + ); + } + } + + /** + * 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(); + + $mixedFound = false; + $queryResultType = TypeTraverser::map( + $queryResultType, + static function (Type $type, callable $traverse) use ($objectManager, &$mixedFound): Type { + $isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type); + if ($isObject->no()) { + return $traverse($type); + } + if ( + $isObject->maybe() + || !$type instanceof TypeWithClassName + || $objectManager === null + ) { + $mixedFound = true; + + return new MixedType(); + } + + /** @var class-string $className */ + $className = $type->getClassName(); + if (!$objectManager->getMetadataFactory()->hasMetadataFor($className)) { + return $traverse($type); + } + + $mixedFound = true; + + return new MixedType(); + } + ); + + return $mixedFound ? null : $queryResultType; + } + + private function getSimpleObjectHydratedReturnType(Type $queryResultType): ?Type + { + if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) { + return $queryResultType; + } + + return null; + } + +} diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index 54345e7f..dc33c43d 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -12,6 +12,7 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Doctrine\HydrationModeReturnTypeResolver; use PHPStan\Type\Doctrine\ObjectMetadataResolver; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\IntegerType; @@ -46,11 +47,16 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn /** @var ObjectMetadataResolver */ private $objectMetadataResolver; + /** @var HydrationModeReturnTypeResolver */ + private $hydrationModeReturnTypeResolver; + public function __construct( - ObjectMetadataResolver $objectMetadataResolver + ObjectMetadataResolver $objectMetadataResolver, + HydrationModeReturnTypeResolver $hydrationModeReturnTypeResolver ) { $this->objectMetadataResolver = $objectMetadataResolver; + $this->hydrationModeReturnTypeResolver = $hydrationModeReturnTypeResolver; } public function getClass(): string @@ -93,136 +99,16 @@ public function getTypeFromMethodCall( $queryType = $scope->getType($methodCall->var); - return $this->getMethodReturnTypeForHydrationMode( - $methodReflection, - $hydrationMode, - $queryType->getTemplateType(AbstractQuery::class, 'TKey'), - $queryType->getTemplateType(AbstractQuery::class, 'TResult') - ); - } - - private function getMethodReturnTypeForHydrationMode( - MethodReflection $methodReflection, - Type $hydrationMode, - Type $queryKeyType, - Type $queryResultType - ): ?Type - { - $isVoidType = (new VoidType())->isSuperTypeOf($queryResultType); - - if ($isVoidType->yes()) { - // A void query result type indicates an UPDATE or DELETE query. - // In this case all methods return the number of affected rows. - return new IntegerType(); - } - - if ($isVoidType->maybe()) { - // We can't be sure what the query type is, so we return the - // declared return type of the method. - return null; - } - if (!$hydrationMode instanceof ConstantIntegerType) { return null; } - switch ($hydrationMode->getValue()) { - case AbstractQuery::HYDRATE_OBJECT: - break; - case AbstractQuery::HYDRATE_ARRAY: - $queryResultType = $this->getArrayHydratedReturnType($queryResultType); - break; - case AbstractQuery::HYDRATE_SIMPLEOBJECT: - $queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType); - break; - default: - return null; - } - - if ($queryResultType === null) { - return null; - } - - switch ($methodReflection->getName()) { - case 'getSingleResult': - return $queryResultType; - case 'getOneOrNullResult': - $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, - $queryResultType - ); - default: - if ($queryKeyType->isNull()->yes()) { - return AccessoryArrayListType::intersectWith(new ArrayType( - new IntegerType(), - $queryResultType - )); - } - return new ArrayType( - $queryKeyType, - $queryResultType - ); - } - } - - /** - * 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(); - - $mixedFound = false; - $queryResultType = TypeTraverser::map( - $queryResultType, - static function (Type $type, callable $traverse) use ($objectManager, &$mixedFound): Type { - $isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type); - if ($isObject->no()) { - return $traverse($type); - } - if ( - $isObject->maybe() - || !$type instanceof TypeWithClassName - || $objectManager === null - ) { - $mixedFound = true; - - return new MixedType(); - } - - /** @var class-string $className */ - $className = $type->getClassName(); - if (!$objectManager->getMetadataFactory()->hasMetadataFor($className)) { - return $traverse($type); - } - - $mixedFound = true; - - return new MixedType(); - } + return $this->hydrationModeReturnTypeResolver->getMethodReturnTypeForHydrationMode( + $methodReflection->getName(), + $hydrationMode->getValue(), + $queryType->getTemplateType(AbstractQuery::class, 'TKey'), + $queryType->getTemplateType(AbstractQuery::class, 'TResult') ); - - return $mixedFound ? null : $queryResultType; - } - - private function getSimpleObjectHydratedReturnType(Type $queryResultType): ?Type - { - if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) { - return $queryResultType; - } - - return null; } } diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php new file mode 100644 index 00000000..0f7215df --- /dev/null +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php @@ -0,0 +1,278 @@ +getMetadataFactory()->getAllMetadata(); + $schemaTool->dropSchema($classes); + $schemaTool->createSchema($classes); + + $simple = new Simple(); + $simple->id = '1'; + $simple->intColumn = 1; + $simple->floatColumn = 0.1; + $simple->decimalColumn = '1.1'; + $simple->stringColumn = 'foobar'; + $simple->stringNullColumn = null; + + $entityManager->persist($simple); + $entityManager->flush(); + + $query = $entityManager->createQuery($dql); + + $typeBuilder = new QueryResultTypeBuilder(); + + QueryResultTypeWalker::walk( + $query, + $typeBuilder, + self::getContainer()->getByType(DescriptorRegistry::class), + self::getContainer()->getByType(PhpVersion::class), + self::getContainer()->getByType(DriverDetector::class) + ); + + $resolver = self::getContainer()->getByType(HydrationModeReturnTypeResolver::class); + + $type = $resolver->getMethodReturnTypeForHydrationMode( + $methodName, + $hydrationMode, + $typeBuilder->getIndexType(), + $typeBuilder->getResultType(), + $entityManager + ) ?? new MixedType(); + + self::assertSame( + $expectedType->describe(VerbosityLevel::precise()), + $type->describe(VerbosityLevel::precise()) + ); + + $query = $entityManager->createQuery($dql); + $result = $query->$methodName($hydrationMode); // TODO should be improved + self::assertGreaterThan(0, count($result)); + + $resultType = ConstantTypeHelper::getTypeFromValue($result); + self::assertTrue( + $type->accepts($resultType, true)->yes(), + sprintf( + "The inferred type\n%s\nshould accept actual type\n%s", + $type->describe(VerbosityLevel::precise()), + $resultType->describe(VerbosityLevel::precise()) + ) + ); + } + + /** + * @return iterable + */ + public static function getTestData(): iterable + { + AccessoryArrayListType::setListTypeEnabled(true); + + yield 'getResult(object), full entity' => [ + self::list(new ObjectType(Simple::class)), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_OBJECT, + ]; + + yield 'getResult(array), full entity' => [ + new MixedType(), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_ARRAY, + ]; + + yield 'getArray(), full entity' => [ + new MixedType(), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'getArrayResult', + Query::HYDRATE_ARRAY, + ]; + + yield 'getResult(simple_object), full entity' => [ + self::list(new ObjectType(Simple::class)), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_SIMPLEOBJECT, + ]; + + yield 'getResult(single_scalar), full entity' => [ + new MixedType(), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'getScalarResult', + Query::HYDRATE_SINGLE_SCALAR, + ]; + + yield 'getResult(scalar), full entity' => [ + new MixedType(), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_SCALAR, + ]; + + yield 'getResult(scalar), bigint field' => [ + self::list(self::constantArray([ + [new ConstantStringType('id'), self::numericString()], + ])), + ' + SELECT s.id + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_SCALAR, + ]; + + yield 'getScalarResult(), bigint field' => [ + self::list(self::constantArray([ + [new ConstantStringType('id'), self::numericString()], + ])), + ' + SELECT s.id + FROM QueryResult\Entities\Simple s + ', + 'getScalarResult', + Query::HYDRATE_SCALAR, + ]; + + yield 'getScalarResult(), decimal field' => [ + self::list(self::constantArray([ + [new ConstantStringType('decimalColumn'), self::numericString()], + ])), + ' + SELECT s.decimalColumn + FROM QueryResult\Entities\Simple s + ', + 'getScalarResult', + Query::HYDRATE_SCALAR, + ]; + + yield 'getScalarResult(), bigint expression' => [ + self::list(self::constantArray([ + [new ConstantStringType('col'), new IntegerType()], + ])), + ' + SELECT -s.id as col + FROM QueryResult\Entities\Simple s + ', + 'getScalarResult', + Query::HYDRATE_SCALAR, + ]; + + yield 'getScalarResult(), decimal expression' => [ + self::list(self::constantArray([ + [new ConstantStringType('col'), TypeCombinator::union(new IntegerType(), new FloatType())], + ])), + ' + SELECT -s.decimalColumn as col + FROM QueryResult\Entities\Simple s + ', + 'getScalarResult', + Query::HYDRATE_SCALAR, + ]; + } + + /** + * @param array $elements + */ + private static 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 static function list(Type $values): Type + { + return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $values)); + } + + private static function numericString(): Type + { + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]); + } + +} diff --git a/tests/Type/Doctrine/data/QueryResult/Entities/Simple.php b/tests/Type/Doctrine/data/QueryResult/Entities/Simple.php new file mode 100644 index 00000000..5169a9e5 --- /dev/null +++ b/tests/Type/Doctrine/data/QueryResult/Entities/Simple.php @@ -0,0 +1,71 @@ + Date: Fri, 28 Jun 2024 12:51:33 +0200 Subject: [PATCH 2/4] Fix NS --- .../Query/QueryResultTypeWalkerHydrationModeTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php index 0f7215df..d58c666e 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php @@ -1,6 +1,6 @@ createQuery($dql); - $result = $query->$methodName($hydrationMode); // TODO should be improved + $result = $query->$methodName($hydrationMode); // @phpstan-ignore-line TODO should be improved self::assertGreaterThan(0, count($result)); $resultType = ConstantTypeHelper::getTypeFromValue($result); From a2487c9e3ec3d7d1fe8a88ef3d902a2aa57da95b Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 28 Jun 2024 15:10:37 +0200 Subject: [PATCH 3/4] More testcases, less magic, allow all php --- .../HydrationModeReturnTypeResolver.php | 10 +- .../QueryResultDynamicReturnTypeExtension.php | 15 +- ...QueryResultTypeWalkerHydrationModeTest.php | 300 +++++++++++++++--- 3 files changed, 261 insertions(+), 64 deletions(-) diff --git a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php index 798856c7..301731f5 100644 --- a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php +++ b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Doctrine; use Doctrine\ORM\AbstractQuery; +use Doctrine\Persistence\ObjectManager; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; @@ -24,7 +25,8 @@ public function getMethodReturnTypeForHydrationMode( string $methodName, int $hydrationMode, Type $queryKeyType, - Type $queryResultType + Type $queryResultType, + ?ObjectManager $objectManager ): ?Type { $isVoidType = (new VoidType())->isSuperTypeOf($queryResultType); @@ -45,7 +47,7 @@ public function getMethodReturnTypeForHydrationMode( case AbstractQuery::HYDRATE_OBJECT: break; case AbstractQuery::HYDRATE_ARRAY: - $queryResultType = $this->getArrayHydratedReturnType($queryResultType); + $queryResultType = $this->getArrayHydratedReturnType($queryResultType, $objectManager); break; case AbstractQuery::HYDRATE_SIMPLEOBJECT: $queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType); @@ -94,10 +96,8 @@ public function getMethodReturnTypeForHydrationMode( * * @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934 */ - private function getArrayHydratedReturnType(Type $queryResultType): ?Type + private function getArrayHydratedReturnType(Type $queryResultType, ?ObjectManager $objectManager): ?Type { - $objectManager = $this->objectMetadataResolver->getObjectManager(); - $mixedFound = false; $queryResultType = TypeTraverser::map( $queryResultType, diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index dc33c43d..818c3869 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -8,24 +8,12 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; 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\HydrationModeReturnTypeResolver; 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\TypeUtils; -use PHPStan\Type\TypeWithClassName; -use PHPStan\Type\VoidType; final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -107,7 +95,8 @@ public function getTypeFromMethodCall( $methodReflection->getName(), $hydrationMode->getValue(), $queryType->getTemplateType(AbstractQuery::class, 'TKey'), - $queryType->getTemplateType(AbstractQuery::class, 'TResult') + $queryType->getTemplateType(AbstractQuery::class, 'TResult'), + $this->objectMetadataResolver->getObjectManager() ); } diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php index d58c666e..88225b23 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php @@ -6,6 +6,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query; use Doctrine\ORM\Tools\SchemaTool; +use LogicException; use PHPStan\Doctrine\Driver\DriverDetector; use PHPStan\Php\PhpVersion; use PHPStan\Testing\PHPStanTestCase; @@ -21,6 +22,7 @@ use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\IterableType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; @@ -29,7 +31,6 @@ use PHPStan\Type\VerbosityLevel; use QueryResult\Entities\Simple; use Type\Doctrine\data\QueryResult\CustomIntType; -use function count; use function sprintf; use const PHP_VERSION_ID; @@ -44,12 +45,8 @@ public static function getAdditionalConfigFiles(): array } /** @dataProvider getTestData */ - public function test(Type $expectedType, string $dql, string $methodName, int $hydrationMode): void + public function test(Type $expectedType, string $dql, string $methodName, ?int $hydrationMode = null): void { - if (PHP_VERSION_ID < 80100) { - self::markTestSkipped('Tests only non-stringified types so far.'); // TODO can be eliminated - } - /** @var EntityManagerInterface $entityManager */ $entityManager = require __DIR__ . '/../data/QueryResult/entity-manager.php'; @@ -89,7 +86,7 @@ public function test(Type $expectedType, string $dql, string $methodName, int $h $type = $resolver->getMethodReturnTypeForHydrationMode( $methodName, - $hydrationMode, + $this->getRealHydrationMode($methodName, $hydrationMode), $typeBuilder->getIndexType(), $typeBuilder->getResultType(), $entityManager @@ -101,8 +98,7 @@ public function test(Type $expectedType, string $dql, string $methodName, int $h ); $query = $entityManager->createQuery($dql); - $result = $query->$methodName($hydrationMode); // @phpstan-ignore-line TODO should be improved - self::assertGreaterThan(0, count($result)); + $result = $this->getQueryResult($query, $methodName, $hydrationMode); $resultType = ConstantTypeHelper::getTypeFromValue($result); self::assertTrue( @@ -132,6 +128,16 @@ public static function getTestData(): iterable Query::HYDRATE_OBJECT, ]; + yield 'getResult(simple_object), full entity' => [ + self::list(new ObjectType(Simple::class)), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_SIMPLEOBJECT, + ]; + yield 'getResult(array), full entity' => [ new MixedType(), ' @@ -142,104 +148,211 @@ public static function getTestData(): iterable Query::HYDRATE_ARRAY, ]; - yield 'getArray(), full entity' => [ - new MixedType(), + yield 'getResult(array), fields' => [ + self::list(self::constantArray([ + [new ConstantStringType('decimalColumn'), self::numericString()], + [new ConstantStringType('floatColumn'), new FloatType()], + ])), ' - SELECT s + SELECT s.decimalColumn, s.floatColumn FROM QueryResult\Entities\Simple s ', - 'getArrayResult', + 'getResult', Query::HYDRATE_ARRAY, ]; - yield 'getResult(simple_object), full entity' => [ - self::list(new ObjectType(Simple::class)), + yield 'getResult(array), expressions' => [ + self::list(self::constantArray([ + [new ConstantStringType('decimalColumn'), self::floatOrIntOrStringified()], + [new ConstantStringType('floatColumn'), self::floatOrStringified()], + ])), ' - SELECT s + SELECT -s.decimalColumn as decimalColumn, -s.floatColumn as floatColumn + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_ARRAY, + ]; + + yield 'getResult(object), fields' => [ + self::list(self::constantArray([ + [new ConstantStringType('decimalColumn'), self::numericString()], + [new ConstantStringType('floatColumn'), new FloatType()], + ])), + ' + SELECT s.decimalColumn, s.floatColumn FROM QueryResult\Entities\Simple s ', 'getResult', + Query::HYDRATE_OBJECT, + ]; + + yield 'getResult(object), expressions' => [ + self::list(self::constantArray([ + [new ConstantStringType('decimalColumn'), self::floatOrIntOrStringified()], + [new ConstantStringType('floatColumn'), self::floatOrStringified()], + ])), + ' + SELECT -s.decimalColumn as decimalColumn, -s.floatColumn as floatColumn + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_OBJECT, + ]; + + yield 'toIterable(object), full entity' => [ + new IterableType(new IntegerType(), new ObjectType(Simple::class)), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'toIterable', + Query::HYDRATE_OBJECT, + ]; + + yield 'toIterable(object), fields' => [ + new IterableType(new IntegerType(), self::constantArray([ + [new ConstantStringType('decimalColumn'), self::numericString()], + [new ConstantStringType('floatColumn'), new FloatType()], + ])), + ' + SELECT s.decimalColumn, s.floatColumn + FROM QueryResult\Entities\Simple s + ', + 'toIterable', + Query::HYDRATE_OBJECT, + ]; + + yield 'toIterable(object), expressions' => [ + new IterableType(new IntegerType(), self::constantArray([ + [new ConstantStringType('decimalColumn'), self::floatOrIntOrStringified()], + [new ConstantStringType('floatColumn'), self::floatOrStringified()], + ])), + ' + SELECT -s.decimalColumn as decimalColumn, -s.floatColumn as floatColumn + FROM QueryResult\Entities\Simple s + ', + 'toIterable', + Query::HYDRATE_OBJECT, + ]; + + yield 'toIterable(simple_object), full entity' => [ + new IterableType(new IntegerType(), new ObjectType(Simple::class)), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'toIterable', Query::HYDRATE_SIMPLEOBJECT, ]; - yield 'getResult(single_scalar), full entity' => [ + yield 'toIterable(array), full entity' => [ new MixedType(), ' SELECT s FROM QueryResult\Entities\Simple s ', - 'getScalarResult', - Query::HYDRATE_SINGLE_SCALAR, + 'toIterable', + Query::HYDRATE_ARRAY, ]; - yield 'getResult(scalar), full entity' => [ + yield 'getArrayResult(), full entity' => [ new MixedType(), ' SELECT s FROM QueryResult\Entities\Simple s ', - 'getResult', - Query::HYDRATE_SCALAR, + 'getArrayResult', ]; - yield 'getResult(scalar), bigint field' => [ + yield 'getArrayResult(), fields' => [ self::list(self::constantArray([ - [new ConstantStringType('id'), self::numericString()], + [new ConstantStringType('decimalColumn'), self::numericString()], + [new ConstantStringType('floatColumn'), new FloatType()], ])), ' - SELECT s.id + SELECT s.decimalColumn, s.floatColumn FROM QueryResult\Entities\Simple s ', - 'getResult', - Query::HYDRATE_SCALAR, + 'getArrayResult', ]; - yield 'getScalarResult(), bigint field' => [ + yield 'getArrayResult(), expressions' => [ self::list(self::constantArray([ - [new ConstantStringType('id'), self::numericString()], + [new ConstantStringType('decimalColumn'), self::floatOrIntOrStringified()], + [new ConstantStringType('floatColumn'), self::floatOrStringified()], ])), ' - SELECT s.id + SELECT -s.decimalColumn as decimalColumn, -s.floatColumn as floatColumn FROM QueryResult\Entities\Simple s ', - 'getScalarResult', + 'getArrayResult', + ]; + + yield 'getResult(single_scalar), decimal field' => [ + new MixedType(), + ' + SELECT s.decimalColumn + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_SINGLE_SCALAR, + ]; + + yield 'getResult(scalar), full entity' => [ + new MixedType(), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'getResult', Query::HYDRATE_SCALAR, ]; - yield 'getScalarResult(), decimal field' => [ - self::list(self::constantArray([ - [new ConstantStringType('decimalColumn'), self::numericString()], - ])), + yield 'getResult(scalar), decimal field' => [ + new MixedType(), ' SELECT s.decimalColumn FROM QueryResult\Entities\Simple s ', - 'getScalarResult', + 'getResult', Query::HYDRATE_SCALAR, ]; - yield 'getScalarResult(), bigint expression' => [ - self::list(self::constantArray([ - [new ConstantStringType('col'), new IntegerType()], - ])), + yield 'getScalarResult, full entity' => [ + new MixedType(), ' - SELECT -s.id as col + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'getScalarResult', + ]; + + yield 'getScalarResult(), decimal field' => [ + new MixedType(), + ' + SELECT s.decimalColumn FROM QueryResult\Entities\Simple s ', 'getScalarResult', - Query::HYDRATE_SCALAR, ]; yield 'getScalarResult(), decimal expression' => [ - self::list(self::constantArray([ - [new ConstantStringType('col'), TypeCombinator::union(new IntegerType(), new FloatType())], - ])), + new MixedType(), ' SELECT -s.decimalColumn as col FROM QueryResult\Entities\Simple s ', 'getScalarResult', - Query::HYDRATE_SCALAR, + ]; + + yield 'getSingleScalarResult(), decimal field' => [ + new MixedType(), + ' + SELECT s.decimalColumn + FROM QueryResult\Entities\Simple s + ', + 'getSingleScalarResult', ]; } @@ -273,4 +386,99 @@ private static function numericString(): Type ]); } + /** + * @param Query $query + * @return mixed + */ + private function getQueryResult(Query $query, string $methodName, ?int $hydrationMode) + { + if ($methodName === 'getResult') { + if ($hydrationMode === null) { + throw new LogicException('Hydration mode must be set for getResult() method.'); + } + return $query->getResult($hydrationMode); // @phpstan-ignore-line dynamic arg + } + + if ($methodName === 'getArrayResult') { + if ($hydrationMode !== null) { + throw new LogicException('Hydration mode must NOT be set for getArrayResult() method.'); + } + return $query->getArrayResult(); + } + + if ($methodName === 'getScalarResult') { + if ($hydrationMode !== null) { + throw new LogicException('Hydration mode must NOT be set for getScalarResult() method.'); + } + return $query->getScalarResult(); + } + + if ($methodName === 'getSingleResult') { + if ($hydrationMode === null) { + throw new LogicException('Hydration mode must be set for getSingleResult() method.'); + } + + return $query->getSingleResult($hydrationMode); // @phpstan-ignore-line dynamic arg + } + + if ($methodName === 'getSingleScalarResult') { + if ($hydrationMode !== null) { + throw new LogicException('Hydration mode must NOT be set for getSingleScalarResult() method.'); + } + + return $query->getSingleScalarResult(); + } + + if ($methodName === 'toIterable') { + if ($hydrationMode === null) { + throw new LogicException('Hydration mode must be set for toIterable() method.'); + } + + return $query->toIterable([], $hydrationMode); // @phpstan-ignore-line dynamic arg + } + + throw new LogicException(sprintf('Unsupported method %s.', $methodName)); + } + + private function getRealHydrationMode(string $methodName, ?int $hydrationMode): int + { + if ($hydrationMode !== null) { + return $hydrationMode; + } + + if ($methodName === 'getArrayResult') { + return Query::HYDRATE_ARRAY; + } + + if ($methodName === 'getScalarResult') { + return Query::HYDRATE_SCALAR; + } + + if ($methodName === 'getSingleScalarResult') { + return Query::HYDRATE_SCALAR; + } + + throw new LogicException("Using $methodName without hydration mode is not supported."); + } + + + private static function stringifies(): bool + { + return PHP_VERSION_ID < 80100; + } + + private static function floatOrStringified(): Type + { + return self::stringifies() + ? self::numericString() + : new FloatType(); + } + + private static function floatOrIntOrStringified(): Type + { + return self::stringifies() + ? self::numericString() + : TypeCombinator::union(new FloatType(), new IntegerType()); + } + } From 55dc335ba1915540056180972ccb1e5d9d0f6e81 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 28 Jun 2024 15:15:41 +0200 Subject: [PATCH 4/4] Fix cs --- .../Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php index 88225b23..de3bf93b 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php @@ -458,7 +458,7 @@ private function getRealHydrationMode(string $methodName, ?int $hydrationMode): return Query::HYDRATE_SCALAR; } - throw new LogicException("Using $methodName without hydration mode is not supported."); + throw new LogicException(sprintf('Using %s without hydration mode is not supported.', $methodName)); }