Skip to content

Commit 24adb0e

Browse files
committed
Revert "Handle all hydration mode in QueryResultDynamicReturnTypeExtension"
This reverts commit 19dd2dd.
1 parent 19dd2dd commit 24adb0e

File tree

4 files changed

+115
-707
lines changed

4 files changed

+115
-707
lines changed

src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php

+22-172
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,14 @@
1010
use PHPStan\ShouldNotHappenException;
1111
use PHPStan\Type\Accessory\AccessoryArrayListType;
1212
use PHPStan\Type\ArrayType;
13-
use PHPStan\Type\BenevolentUnionType;
1413
use PHPStan\Type\Constant\ConstantIntegerType;
15-
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
1614
use PHPStan\Type\DynamicMethodReturnTypeExtension;
1715
use PHPStan\Type\IntegerType;
1816
use PHPStan\Type\IterableType;
19-
use PHPStan\Type\MixedType;
2017
use PHPStan\Type\NullType;
21-
use PHPStan\Type\ObjectWithoutClassType;
2218
use PHPStan\Type\Type;
2319
use PHPStan\Type\TypeCombinator;
24-
use PHPStan\Type\TypeTraverser;
25-
use PHPStan\Type\TypeUtils;
26-
use PHPStan\Type\TypeWithClassName;
2720
use PHPStan\Type\VoidType;
28-
use function count;
2921

3022
final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
3123
{
@@ -40,32 +32,14 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn
4032
'getSingleResult' => 0,
4133
];
4234

43-
private const METHOD_HYDRATION_MODE = [
44-
'getArrayResult' => AbstractQuery::HYDRATE_ARRAY,
45-
'getScalarResult' => AbstractQuery::HYDRATE_SCALAR,
46-
'getSingleColumnResult' => AbstractQuery::HYDRATE_SCALAR_COLUMN,
47-
'getSingleScalarResult' => AbstractQuery::HYDRATE_SINGLE_SCALAR,
48-
];
49-
50-
/** @var ObjectMetadataResolver */
51-
private $objectMetadataResolver;
52-
53-
public function __construct(
54-
ObjectMetadataResolver $objectMetadataResolver
55-
)
56-
{
57-
$this->objectMetadataResolver = $objectMetadataResolver;
58-
}
59-
6035
public function getClass(): string
6136
{
6237
return AbstractQuery::class;
6338
}
6439

6540
public function isMethodSupported(MethodReflection $methodReflection): bool
6641
{
67-
return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()])
68-
|| isset(self::METHOD_HYDRATION_MODE[$methodReflection->getName()]);
42+
return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]);
6943
}
7044

7145
public function getTypeFromMethodCall(
@@ -76,23 +50,21 @@ public function getTypeFromMethodCall(
7650
{
7751
$methodName = $methodReflection->getName();
7852

79-
if (isset(self::METHOD_HYDRATION_MODE[$methodName])) {
80-
$hydrationMode = new ConstantIntegerType(self::METHOD_HYDRATION_MODE[$methodName]);
81-
} elseif (isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) {
82-
$argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName];
83-
$args = $methodCall->getArgs();
53+
if (!isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) {
54+
throw new ShouldNotHappenException();
55+
}
8456

85-
if (isset($args[$argIndex])) {
86-
$hydrationMode = $scope->getType($args[$argIndex]->value);
87-
} else {
88-
$parametersAcceptor = ParametersAcceptorSelector::selectSingle(
89-
$methodReflection->getVariants()
90-
);
91-
$parameter = $parametersAcceptor->getParameters()[$argIndex];
92-
$hydrationMode = $parameter->getDefaultValue() ?? new NullType();
93-
}
57+
$argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName];
58+
$args = $methodCall->getArgs();
59+
60+
if (isset($args[$argIndex])) {
61+
$hydrationMode = $scope->getType($args[$argIndex]->value);
9462
} else {
95-
throw new ShouldNotHappenException();
63+
$parametersAcceptor = ParametersAcceptorSelector::selectSingle(
64+
$methodReflection->getVariants()
65+
);
66+
$parameter = $parametersAcceptor->getParameters()[$argIndex];
67+
$hydrationMode = $parameter->getDefaultValue() ?? new NullType();
9668
}
9769

9870
$queryType = $scope->getType($methodCall->var);
@@ -126,54 +98,23 @@ private function getMethodReturnTypeForHydrationMode(
12698
return $this->originalReturnType($methodReflection);
12799
}
128100

129-
if (!$hydrationMode instanceof ConstantIntegerType) {
101+
if (!$this->isObjectHydrationMode($hydrationMode)) {
102+
// We support only HYDRATE_OBJECT. For other hydration modes, we
103+
// return the declared return type of the method.
130104
return $this->originalReturnType($methodReflection);
131105
}
132106

133-
$singleResult = false;
134-
switch ($hydrationMode->getValue()) {
135-
case AbstractQuery::HYDRATE_OBJECT:
136-
break;
137-
case AbstractQuery::HYDRATE_ARRAY:
138-
$queryResultType = $this->getArrayHydratedReturnType($queryResultType);
139-
break;
140-
case AbstractQuery::HYDRATE_SCALAR:
141-
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
142-
break;
143-
case AbstractQuery::HYDRATE_SINGLE_SCALAR:
144-
$singleResult = true;
145-
$queryResultType = $this->getSingleScalarHydratedReturnType($queryResultType);
146-
break;
147-
case AbstractQuery::HYDRATE_SIMPLEOBJECT:
148-
$queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType);
149-
break;
150-
case AbstractQuery::HYDRATE_SCALAR_COLUMN:
151-
$queryResultType = $this->getScalarColumnHydratedReturnType($queryResultType);
152-
break;
153-
default:
154-
return $this->originalReturnType($methodReflection);
155-
}
156-
157107
switch ($methodReflection->getName()) {
158108
case 'getSingleResult':
159109
return $queryResultType;
160110
case 'getOneOrNullResult':
161-
$nullableQueryResultType = TypeCombinator::addNull($queryResultType);
162-
if ($queryResultType instanceof BenevolentUnionType) {
163-
$nullableQueryResultType = TypeUtils::toBenevolentUnion($nullableQueryResultType);
164-
}
165-
166-
return $nullableQueryResultType;
111+
return TypeCombinator::addNull($queryResultType);
167112
case 'toIterable':
168113
return new IterableType(
169114
$queryKeyType->isNull()->yes() ? new IntegerType() : $queryKeyType,
170115
$queryResultType
171116
);
172117
default:
173-
if ($singleResult) {
174-
return $queryResultType;
175-
}
176-
177118
if ($queryKeyType->isNull()->yes()) {
178119
return AccessoryArrayListType::intersectWith(new ArrayType(
179120
new IntegerType(),
@@ -187,104 +128,13 @@ private function getMethodReturnTypeForHydrationMode(
187128
}
188129
}
189130

190-
private function getArrayHydratedReturnType(Type $queryResultType): Type
191-
{
192-
$objectManager = $this->objectMetadataResolver->getObjectManager();
193-
194-
return TypeTraverser::map(
195-
$queryResultType,
196-
static function (Type $type, callable $traverse) use ($objectManager): Type {
197-
$isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type);
198-
if ($isObject->no()) {
199-
return $traverse($type);
200-
}
201-
if (
202-
$isObject->maybe()
203-
|| !$type instanceof TypeWithClassName
204-
|| $objectManager === null
205-
) {
206-
return new MixedType();
207-
}
208-
209-
if (!$objectManager->getMetadataFactory()->hasMetadataFor($type->getClassName())) {
210-
return $traverse($type);
211-
}
212-
213-
// We could return `new ArrayTyp(new MixedType(), new MixedType())`
214-
// but the lack of precision in the array keys/values would give false positive
215-
// @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934
216-
return new MixedType();
217-
}
218-
);
219-
}
220-
221-
private function getScalarHydratedReturnType(Type $queryResultType): Type
222-
{
223-
if (!$queryResultType->isArray()->yes()) {
224-
return new ArrayType(new MixedType(), new MixedType());
225-
}
226-
227-
foreach ($queryResultType->getArrays() as $arrayType) {
228-
$itemType = $arrayType->getItemType();
229-
230-
if (
231-
!(new ObjectWithoutClassType())->isSuperTypeOf($itemType)->no()
232-
|| !$itemType->isArray()->no()
233-
) {
234-
return new ArrayType(new MixedType(), new MixedType());
235-
}
236-
}
237-
238-
return $queryResultType;
239-
}
240-
241-
private function getSimpleObjectHydratedReturnType(Type $queryResultType): Type
242-
{
243-
if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) {
244-
return $queryResultType;
245-
}
246-
247-
return new MixedType();
248-
}
249-
250-
private function getSingleScalarHydratedReturnType(Type $queryResultType): Type
131+
private function isObjectHydrationMode(Type $type): bool
251132
{
252-
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
253-
if (!$queryResultType->isConstantArray()->yes()) {
254-
return new MixedType();
255-
}
256-
257-
$types = [];
258-
foreach ($queryResultType->getConstantArrays() as $constantArrayType) {
259-
$values = $constantArrayType->getValueTypes();
260-
if (count($values) !== 1) {
261-
return new MixedType();
262-
}
263-
264-
$types[] = $constantArrayType->getFirstIterableValueType();
265-
}
266-
267-
return TypeCombinator::union(...$types);
268-
}
269-
270-
private function getScalarColumnHydratedReturnType(Type $queryResultType): Type
271-
{
272-
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
273-
if (!$queryResultType->isConstantArray()->yes()) {
274-
return new MixedType();
275-
}
276-
277-
$types = [];
278-
foreach ($queryResultType->getConstantArrays() as $constantArrayType) {
279-
$values = $constantArrayType->getValueTypes();
280-
if (count($values) !== 1) {
281-
return new MixedType();
282-
}
283-
284-
$types[] = $constantArrayType->getFirstIterableValueType();
133+
if (!$type instanceof ConstantIntegerType) {
134+
return false;
285135
}
286136

287-
return TypeCombinator::union(...$types);
137+
return $type->getValue() === AbstractQuery::HYDRATE_OBJECT;
288138
}
289139

290140
private function originalReturnType(MethodReflection $methodReflection): Type

src/Type/Doctrine/Query/QueryResultTypeWalker.php

+3-34
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,6 @@ class QueryResultTypeWalker extends SqlWalker
107107
/** @var bool */
108108
private $hasGroupByClause;
109109

110-
/** @var bool */
111-
private $hasWhereClause;
112-
113110
/**
114111
* @param Query<mixed> $query
115112
*/
@@ -138,7 +135,6 @@ public function __construct($query, $parserResult, array $queryComponents)
138135
$this->nullableQueryComponents = [];
139136
$this->hasAggregateFunction = false;
140137
$this->hasGroupByClause = false;
141-
$this->hasWhereClause = false;
142138

143139
// The object is instantiated by Doctrine\ORM\Query\Parser, so receiving
144140
// dependencies through the constructor is not an option. Instead, we
@@ -181,7 +177,6 @@ public function walkSelectStatement(AST\SelectStatement $AST)
181177
$this->typeBuilder->setSelectQuery();
182178
$this->hasAggregateFunction = $this->hasAggregateFunction($AST);
183179
$this->hasGroupByClause = $AST->groupByClause !== null;
184-
$this->hasWhereClause = $AST->whereClause !== null;
185180

186181
$this->walkFromClause($AST->fromClause);
187182

@@ -800,7 +795,7 @@ public function walkSelectExpression($selectExpression)
800795

801796
$type = $this->resolveDoctrineType($typeName, $enumType, $nullable);
802797

803-
$this->addScalar($resultAlias, $type);
798+
$this->typeBuilder->addScalar($resultAlias, $type);
804799

805800
return '';
806801
}
@@ -846,32 +841,21 @@ public function walkSelectExpression($selectExpression)
846841
// the driver and PHP version.
847842
// Here we assume that the value may or may not be casted to
848843
// string by the driver.
849-
$casted = false;
850-
$originalType = $type;
851-
852-
$type = TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$casted): Type {
844+
$type = TypeTraverser::map($type, static function (Type $type, callable $traverse): Type {
853845
if ($type instanceof UnionType || $type instanceof IntersectionType) {
854846
return $traverse($type);
855847
}
856848
if ($type instanceof IntegerType || $type instanceof FloatType) {
857-
$casted = true;
858849
return TypeCombinator::union($type->toString(), $type);
859850
}
860851
if ($type instanceof BooleanType) {
861-
$casted = true;
862852
return TypeCombinator::union($type->toInteger()->toString(), $type);
863853
}
864854
return $traverse($type);
865855
});
866-
867-
// Since we made supposition about possibly casted values,
868-
// we can only provide a benevolent union.
869-
if ($casted && $type instanceof UnionType && !$originalType->equals($type)) {
870-
$type = TypeUtils::toBenevolentUnion($type);
871-
}
872856
}
873857

874-
$this->addScalar($resultAlias, $type);
858+
$this->typeBuilder->addScalar($resultAlias, $type);
875859

876860
return '';
877861
}
@@ -1292,21 +1276,6 @@ public function walkResultVariable($resultVariable)
12921276
return $this->marshalType(new MixedType());
12931277
}
12941278

1295-
/**
1296-
* @param array-key $alias
1297-
*/
1298-
private function addScalar($alias, Type $type): void
1299-
{
1300-
// Since we don't check the condition inside the WHERE
1301-
// conditions, we cannot be sure all the union types are correct.
1302-
// For exemple, a condition `WHERE foo.bar IS NOT NULL` could be added.
1303-
if ($this->hasWhereClause && $type instanceof UnionType) {
1304-
$type = TypeUtils::toBenevolentUnion($type);
1305-
}
1306-
1307-
$this->typeBuilder->addScalar($alias, $type);
1308-
}
1309-
13101279
private function unmarshalType(string $marshalledType): Type
13111280
{
13121281
$type = unserialize($marshalledType);

0 commit comments

Comments
 (0)