Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
],
"require": {
"php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.8.11"
"phpstan/phpstan": "^1.9.0"
},
"conflict": {
"doctrine/collections": "<1.0",
Expand Down
12 changes: 6 additions & 6 deletions src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
use PHPStan\Type\UnionType;

/**
* Infers TResult in Query<TResult> on EntityManagerInterface::createQuery()
* Infers TResult and TKey in Query<TKey,TResult> on EntityManagerInterface::createQuery()
*/
final class CreateQueryDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
Expand Down Expand Up @@ -63,7 +63,7 @@ public function getTypeFromMethodCall(
if (!isset($args[$queryStringArgIndex])) {
return new GenericObjectType(
Query::class,
[new MixedType()]
[new MixedType(), new MixedType()]
);
}

Expand All @@ -78,7 +78,7 @@ public function getTypeFromMethodCall(

$em = $this->objectMetadataResolver->getObjectManager();
if (!$em instanceof EntityManagerInterface) {
return new QueryType($queryString, null);
return new QueryType($queryString, null, null);
}

$typeBuilder = new QueryResultTypeBuilder();
Expand All @@ -87,14 +87,14 @@ public function getTypeFromMethodCall(
$query = $em->createQuery($queryString);
QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry);
} catch (ORMException | DBALException | NewDBALException | CommonException $e) {
return new QueryType($queryString, null);
return new QueryType($queryString, null, null);
}

return new QueryType($queryString, $typeBuilder->getResultType());
return new QueryType($queryString, $typeBuilder->getIndexType(), $typeBuilder->getResultType());
}
return new GenericObjectType(
Query::class,
[new MixedType()]
[new MixedType(), new MixedType()]
);
});
}
Expand Down
38 changes: 31 additions & 7 deletions src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Accessory\AccessoryArrayListType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Doctrine\Type\ListIndexMarkerType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\IntegerType;
Expand All @@ -18,6 +20,7 @@
use PHPStan\Type\NullType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
use PHPStan\Type\VoidType;

final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
Expand Down Expand Up @@ -70,15 +73,17 @@ public function getTypeFromMethodCall(

$queryType = $scope->getType($methodCall->var);
$queryResultType = $this->getQueryResultType($queryType);
$queryIndexType = $this->getQueryIndexType($queryType);

return $this->getMethodReturnTypeForHydrationMode(
$methodReflection,
$hydrationMode,
$queryResultType
$queryResultType,
$queryIndexType
);
}

private function getQueryResultType(Type $queryType): Type
private function getQueryIndexType(Type $queryType): Type
{
if (!$queryType instanceof GenericObjectType) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should rather be PHPStan\Type\Doctrine\Query\QueryType, but this was used below.

return new MixedType();
Expand All @@ -89,10 +94,22 @@ private function getQueryResultType(Type $queryType): Type
return $types[0] ?? new MixedType();
}

private function getQueryResultType(Type $queryType): Type
{
if (!$queryType instanceof GenericObjectType) {
return new MixedType();
}

$types = $queryType->getTypes();

return $types[1] ?? new MixedType();
}

private function getMethodReturnTypeForHydrationMode(
MethodReflection $methodReflection,
Type $hydrationMode,
Type $queryResultType
Type $queryResultType,
Type $queryIndexType
): Type
{
$isVoidType = (new VoidType())->isSuperTypeOf($queryResultType);
Expand Down Expand Up @@ -126,10 +143,17 @@ private function getMethodReturnTypeForHydrationMode(
$queryResultType
);
default:
return new ArrayType(
new MixedType(),
$queryResultType
);
return $queryIndexType instanceof ListIndexMarkerType
? AccessoryArrayListType::intersectWith(
TypeCombinator::intersect(
new ArrayType(new IntegerType(), $queryResultType),
...TypeUtils::getAccessoryTypes($queryResultType)
)
)
: new ArrayType(
new MixedType(),
$queryResultType
);
}
}

Expand Down
23 changes: 23 additions & 0 deletions src/Type/Doctrine/Query/QueryResultTypeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Doctrine\Type\ListIndexMarkerType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\VoidType;
Expand Down Expand Up @@ -62,6 +63,14 @@ final class QueryResultTypeBuilder
*/
private $newObjects = [];

/** @var Type */
private $indexedBy;

public function __construct()
{
$this->indexedBy = new ListIndexMarkerType();
}

public function setSelectQuery(): void
{
$this->selectQuery = true;
Expand Down Expand Up @@ -230,4 +239,18 @@ private function resolveOffsetType($alias): Type
return new ConstantStringType($alias);
}

public function setIndexedBy(Type $type): void
{
$this->indexedBy = $type;
}

public function getIndexType(): Type
{
if (!$this->selectQuery) {
return new VoidType();
}

return $this->indexedBy;
}

}
12 changes: 9 additions & 3 deletions src/Type/Doctrine/Query/QueryResultTypeWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class QueryResultTypeWalker extends SqlWalker
*/
private $newObjectCounter = 0;

/** @var Query<mixed> */
/** @var Query<mixed, mixed> */
private $query;

/** @var EntityManagerInterface */
Expand Down Expand Up @@ -107,7 +107,7 @@ class QueryResultTypeWalker extends SqlWalker
private $hasGroupByClause;

/**
* @param Query<mixed> $query
* @param Query<mixed, mixed> $query
*/
public static function walk(Query $query, QueryResultTypeBuilder $typeBuilder, DescriptorRegistry $descriptorRegistry): void
{
Expand All @@ -122,7 +122,7 @@ public static function walk(Query $query, QueryResultTypeBuilder $typeBuilder, D
/**
* {@inheritDoc}
*
* @param Query<mixed> $query
* @param Query<mixed, mixed> $query
* @param ParserResult $parserResult
* @param array<QueryComponent> $queryComponents
*/
Expand Down Expand Up @@ -312,6 +312,10 @@ public function walkFromClause($fromClause)
*/
public function walkIdentificationVariableDeclaration($identificationVariableDecl)
{
if ($identificationVariableDecl->indexBy !== null) {
$identificationVariableDecl->indexBy->dispatch($this);
}

foreach ($identificationVariableDecl->joins as $join) {
assert($join instanceof AST\Node);

Expand All @@ -326,6 +330,8 @@ public function walkIdentificationVariableDeclaration($identificationVariableDec
*/
public function walkIndexBy($indexBy): void
{
$type = $this->unmarshalType($indexBy->singleValuedPathExpression->dispatch($this));
$this->typeBuilder->setIndexedBy($type);
}

/**
Expand Down
12 changes: 9 additions & 3 deletions src/Type/Doctrine/Query/QueryType.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ class QueryType extends GenericObjectType
/** @var string */
private $dql;

public function __construct(string $dql, ?Type $resultType = null)
public function __construct(
string $dql,
?Type $indexType,
?Type $resultType
)
{
$resultType = $resultType ?? new MixedType();
parent::__construct('Doctrine\ORM\Query', [$resultType]);
parent::__construct('Doctrine\ORM\Query', [
$indexType ?? new MixedType(),
$resultType ?? new MixedType(),
]);
$this->dql = $dql;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ private function getQueryType(string $dql): Type
{
$em = $this->objectMetadataResolver->getObjectManager();
if (!$em instanceof EntityManagerInterface) {
return new QueryType($dql, null);
return new QueryType($dql, null, null);
}

$typeBuilder = new QueryResultTypeBuilder();
Expand All @@ -162,10 +162,10 @@ private function getQueryType(string $dql): Type
$query = $em->createQuery($dql);
QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry);
} catch (ORMException | DBALException | CommonException $e) {
return new QueryType($dql, null);
return new QueryType($dql, null, null);
}

return new QueryType($dql, $typeBuilder->getResultType());
return new QueryType($dql, $typeBuilder->getIndexType(), $typeBuilder->getResultType());
}

}
10 changes: 10 additions & 0 deletions src/Type/Doctrine/Type/ListIndexMarkerType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Doctrine\Type;

use PHPStan\Type\IntegerType;

class ListIndexMarkerType extends IntegerType
{

}
1 change: 1 addition & 0 deletions stubs/ORM/AbstractQuery.stub
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace Doctrine\ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
* @template TKey The type of column used in indexBy
* @template TResult The type of results returned by this query in HYDRATE_OBJECT mode
*/
abstract class AbstractQuery
Expand Down
3 changes: 2 additions & 1 deletion stubs/ORM/Query.stub
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
namespace Doctrine\ORM;

/**
* @template TKey The type of column used in indexBy
* @template TResult The type of results returned by this query in HYDRATE_OBJECT mode
*
* @extends AbstractQuery<TResult>
* @extends AbstractQuery<TKey, TResult>
*/
final class Query extends AbstractQuery
{
Expand Down
2 changes: 1 addition & 1 deletion stubs/ORM/QueryBuilder.stub
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class QueryBuilder
}

/**
* @return Query<mixed>
* @return Query<mixed, mixed>
*/
public function getQuery()
{
Expand Down
4 changes: 2 additions & 2 deletions stubs/bleedingEdge/ORM/QueryBuilder.stub
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class QueryBuilder
{

/**
* @return Query<mixed>
* @return Query<mixed, mixed>
*/
public function getQuery()
{
Expand Down Expand Up @@ -143,7 +143,7 @@ class QueryBuilder
{

}

/**
* @param literal-string|object|array<mixed> $predicates
* @return $this
Expand Down
2 changes: 2 additions & 0 deletions tests/Type/Doctrine/data/QueryResult/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ includes:
parameters:
doctrine:
objectManagerLoader: entity-manager.php
featureToggles:
listType: true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I can keep this here if we would merge this.

8 changes: 4 additions & 4 deletions tests/Type/Doctrine/data/QueryResult/createQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,28 @@ public function testQueryTypeParametersAreInfered(EntityManagerInterface $em): v
FROM QueryResult\Entities\Many m
');

assertType('Doctrine\ORM\Query<QueryResult\Entities\Many>', $query);
assertType('Doctrine\ORM\Query<int, QueryResult\Entities\Many>', $query);

$query = $em->createQuery('
SELECT m.intColumn, m.stringNullColumn
FROM QueryResult\Entities\Many m
');

assertType('Doctrine\ORM\Query<array{intColumn: int, stringNullColumn: string|null}>', $query);
assertType('Doctrine\ORM\Query<int, array{intColumn: int, stringNullColumn: string|null}>', $query);
}

public function testQueryResultTypeIsMixedWhenDQLIsNotKnown(EntityManagerInterface $em, string $dql): void
{
$query = $em->createQuery($dql);

assertType('Doctrine\ORM\Query<mixed>', $query);
assertType('Doctrine\ORM\Query<mixed, mixed>', $query);
}

public function testQueryResultTypeIsMixedWhenDQLIsInvalid(EntityManagerInterface $em, string $dql): void
{
$query = $em->createQuery('invalid');

assertType('Doctrine\ORM\Query<mixed>', $query);
assertType('Doctrine\ORM\Query<mixed, mixed>', $query);
}

}
Loading