Skip to content

Commit

Permalink
Revert "Handle all hydration mode in QueryResultDynamicReturnTypeExte…
Browse files Browse the repository at this point in the history
…nsion"

This reverts commit 19dd2dd.
  • Loading branch information
ondrejmirtes committed Jan 17, 2024
1 parent 19dd2dd commit 24adb0e
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 707 deletions.
194 changes: 22 additions & 172 deletions src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,14 @@
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;
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;
use function count;

final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
Expand All @@ -40,32 +32,14 @@ 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;
}

public function isMethodSupported(MethodReflection $methodReflection): bool
{
return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()])
|| isset(self::METHOD_HYDRATION_MODE[$methodReflection->getName()]);
return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]);
}

public function getTypeFromMethodCall(
Expand All @@ -76,23 +50,21 @@ public function getTypeFromMethodCall(
{
$methodName = $methodReflection->getName();

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(self::METHOD_HYDRATION_MODE_ARG[$methodName])) {
throw new ShouldNotHappenException();
}

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();
}
$argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName];
$args = $methodCall->getArgs();

if (isset($args[$argIndex])) {
$hydrationMode = $scope->getType($args[$argIndex]->value);
} else {
throw new ShouldNotHappenException();
$parametersAcceptor = ParametersAcceptorSelector::selectSingle(
$methodReflection->getVariants()
);
$parameter = $parametersAcceptor->getParameters()[$argIndex];
$hydrationMode = $parameter->getDefaultValue() ?? new NullType();
}

$queryType = $scope->getType($methodCall->var);
Expand Down Expand Up @@ -126,54 +98,23 @@ private function getMethodReturnTypeForHydrationMode(
return $this->originalReturnType($methodReflection);
}

if (!$hydrationMode instanceof ConstantIntegerType) {
if (!$this->isObjectHydrationMode($hydrationMode)) {
// We support only HYDRATE_OBJECT. For other hydration modes, we
// return the declared return type of the method.
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;
case 'getOneOrNullResult':
$nullableQueryResultType = TypeCombinator::addNull($queryResultType);
if ($queryResultType instanceof BenevolentUnionType) {
$nullableQueryResultType = TypeUtils::toBenevolentUnion($nullableQueryResultType);
}

return $nullableQueryResultType;
return TypeCombinator::addNull($queryResultType);
case 'toIterable':
return new IterableType(
$queryKeyType->isNull()->yes() ? new IntegerType() : $queryKeyType,
$queryResultType
);
default:
if ($singleResult) {
return $queryResultType;
}

if ($queryKeyType->isNull()->yes()) {
return AccessoryArrayListType::intersectWith(new ArrayType(
new IntegerType(),
Expand All @@ -187,104 +128,13 @@ private function getMethodReturnTypeForHydrationMode(
}
}

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();
}

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();
}
);
}

private function getScalarHydratedReturnType(Type $queryResultType): Type
{
if (!$queryResultType->isArray()->yes()) {
return new ArrayType(new MixedType(), new MixedType());
}

foreach ($queryResultType->getArrays() as $arrayType) {
$itemType = $arrayType->getItemType();

if (
!(new ObjectWithoutClassType())->isSuperTypeOf($itemType)->no()
|| !$itemType->isArray()->no()
) {
return new ArrayType(new MixedType(), new MixedType());
}
}

return $queryResultType;
}

private function getSimpleObjectHydratedReturnType(Type $queryResultType): Type
{
if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) {
return $queryResultType;
}

return new MixedType();
}

private function getSingleScalarHydratedReturnType(Type $queryResultType): Type
private function isObjectHydrationMode(Type $type): bool
{
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
if (!$queryResultType->isConstantArray()->yes()) {
return new MixedType();
}

$types = [];
foreach ($queryResultType->getConstantArrays() as $constantArrayType) {
$values = $constantArrayType->getValueTypes();
if (count($values) !== 1) {
return new MixedType();
}

$types[] = $constantArrayType->getFirstIterableValueType();
}

return TypeCombinator::union(...$types);
}

private function getScalarColumnHydratedReturnType(Type $queryResultType): Type
{
$queryResultType = $this->getScalarHydratedReturnType($queryResultType);
if (!$queryResultType->isConstantArray()->yes()) {
return new MixedType();
}

$types = [];
foreach ($queryResultType->getConstantArrays() as $constantArrayType) {
$values = $constantArrayType->getValueTypes();
if (count($values) !== 1) {
return new MixedType();
}

$types[] = $constantArrayType->getFirstIterableValueType();
if (!$type instanceof ConstantIntegerType) {
return false;
}

return TypeCombinator::union(...$types);
return $type->getValue() === AbstractQuery::HYDRATE_OBJECT;
}

private function originalReturnType(MethodReflection $methodReflection): Type
Expand Down
37 changes: 3 additions & 34 deletions src/Type/Doctrine/Query/QueryResultTypeWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,6 @@ class QueryResultTypeWalker extends SqlWalker
/** @var bool */
private $hasGroupByClause;

/** @var bool */
private $hasWhereClause;

/**
* @param Query<mixed> $query
*/
Expand Down Expand Up @@ -138,7 +135,6 @@ public function __construct($query, $parserResult, array $queryComponents)
$this->nullableQueryComponents = [];
$this->hasAggregateFunction = false;
$this->hasGroupByClause = 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
Expand Down Expand Up @@ -181,7 +177,6 @@ public function walkSelectStatement(AST\SelectStatement $AST)
$this->typeBuilder->setSelectQuery();
$this->hasAggregateFunction = $this->hasAggregateFunction($AST);
$this->hasGroupByClause = $AST->groupByClause !== null;
$this->hasWhereClause = $AST->whereClause !== null;

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

Expand Down Expand Up @@ -800,7 +795,7 @@ public function walkSelectExpression($selectExpression)

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

$this->addScalar($resultAlias, $type);
$this->typeBuilder->addScalar($resultAlias, $type);

return '';
}
Expand Down Expand Up @@ -846,32 +841,21 @@ public function walkSelectExpression($selectExpression)
// the driver and PHP version.
// 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 {
$type = TypeTraverser::map($type, static function (Type $type, callable $traverse): 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 && !$originalType->equals($type)) {
$type = TypeUtils::toBenevolentUnion($type);
}
}

$this->addScalar($resultAlias, $type);
$this->typeBuilder->addScalar($resultAlias, $type);

return '';
}
Expand Down Expand Up @@ -1292,21 +1276,6 @@ public function walkResultVariable($resultVariable)
return $this->marshalType(new MixedType());
}

/**
* @param array-key $alias
*/
private function addScalar($alias, Type $type): void
{
// 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->hasWhereClause && $type instanceof UnionType) {
$type = TypeUtils::toBenevolentUnion($type);
}

$this->typeBuilder->addScalar($alias, $type);
}

private function unmarshalType(string $marshalledType): Type
{
$type = unserialize($marshalledType);
Expand Down
Loading

0 comments on commit 24adb0e

Please sign in to comment.