Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\Type\IntegerType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function in_array;

class EntityQueryDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
Expand Down Expand Up @@ -66,6 +67,14 @@ public function getTypeFromMethodCall(
? IntegerRangeType::createAllGreaterThanOrEqualTo(0)
: new EntityQueryExecuteWithoutAccessCheckCountType();
}
if ($varType->isAggregate()) {
// An aggregate query returns a list of result rows, each keyed
// by the aggregate results and the groupBy columns.
$rowType = new ArrayType(new StringType(), TypeCombinator::addNull(new StringType()));
return $varType->hasAccessCheck()
? new ArrayType(new IntegerType(), $rowType)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

nit: QueryAggregate::result() builds the rows with $return[] =, so this is really a list. The existing branches use a plain ArrayType too, so it's fine to leave this as is.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I left this as is.

: new EntityQueryExecuteWithoutAccessCheckType(new IntegerType(), $rowType);
}
if ($varType instanceof ConfigEntityQueryType) {
return $varType->hasAccessCheck()
? new ArrayType(new StringType(), new StringType())
Expand Down
26 changes: 25 additions & 1 deletion src/Type/EntityQuery/EntityQueryType.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class EntityQueryType extends ObjectType

private bool $isCount = false;

private bool $isAggregate = false;

public function hasAccessCheck(): bool
{
return $this->hasAccessCheck;
Expand All @@ -23,6 +25,11 @@ public function isCount(): bool
return $this->isCount;
}

public function isAggregate(): bool
{
return $this->isAggregate;
}

public function withAccessCheck(): self
{
// The constructor of ObjectType is under backward compatibility promise.
Expand All @@ -35,6 +42,7 @@ public function withAccessCheck(): self
);
$type->hasAccessCheck = true;
$type->isCount = $this->isCount;
$type->isAggregate = $this->isAggregate;
return $type;
}

Expand All @@ -48,14 +56,30 @@ public function asCount(): self
);
$type->hasAccessCheck = $this->hasAccessCheck;
$type->isCount = true;
$type->isAggregate = $this->isAggregate;
return $type;
}

public function asAggregate(): self
{
// @phpstan-ignore new.static
$type = new static(
$this->getClassName(),
$this->getSubtractedType(),
$this->getClassReflection()
);
$type->hasAccessCheck = $this->hasAccessCheck;
$type->isCount = $this->isCount;
$type->isAggregate = true;
return $type;
}

protected function describeAdditionalCacheKey(): string
{
$parts = [
$this->hasAccessCheck ? 'with-access-check' : 'without-access-check',
$this->isCount ? 'count' : ''
$this->isCount ? 'count' : '',
$this->isAggregate ? 'aggregate' : '',
];
return implode('-', $parts);
}
Expand Down
11 changes: 9 additions & 2 deletions src/Type/EntityStorage/GetQueryReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,19 @@ public function getTypeFromMethodCall(
return $returnType;
}

$isAggregate = $methodReflection->getName() === 'getAggregateQuery';

if ((new ObjectType(ContentEntityStorageInterface::class))->isSuperTypeOf($callerType)->yes()) {
return new ContentEntityQueryType($className);
$queryType = new ContentEntityQueryType($className);
return $isAggregate ? $queryType->asAggregate() : $queryType;
}
if ((new ObjectType(ConfigEntityStorageInterface::class))->isSuperTypeOf($callerType)->yes()) {
// Core throws QueryException for an aggregate query over config
// entities, so there is no aggregate query type to produce here.
// @see \Drupal\Core\Config\Entity\Query\QueryFactory::getAggregate()
return new ConfigEntityQueryType($className);
}
return new EntityQueryType($className);
$queryType = new EntityQueryType($className);
return $isAggregate ? $queryType->asAggregate() : $queryType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ final class EntityQueryDynamicReturnTypeExtensionTest extends TypeInferenceTestC
public static function dataFileAsserts(): iterable
{
yield from self::gatherAssertTypes(__DIR__ . '/../data/entity-query-execute.php');
yield from self::gatherAssertTypes(__DIR__ . '/../data/aggregate-query-execute.php');
yield from self::gatherAssertTypes(__DIR__ . '/../data/bug-355-entity-query.php');
yield from self::gatherAssertTypes(__DIR__ . '/../data/bug-522.php');
yield from self::gatherAssertTypes(__DIR__ . '/../data/bug-909.php');
Expand Down
34 changes: 34 additions & 0 deletions tests/src/Type/data/aggregate-query-execute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace DrupalAggregateQuery;

use function PHPStan\Testing\assertType;

// An aggregate query grouped by a field returns a list of result rows, where
// each row is an array keyed by the aggregate results and the groupBy columns.
// @see \Drupal\Core\Entity\Query\QueryAggregateInterface::execute()
$rows = \Drupal::entityTypeManager()->getStorage('node')->getAggregateQuery()
->accessCheck(FALSE)
->condition('status', TRUE)
->groupBy('type')
->execute();
assertType('array<int, array<string, string|null>>', $rows);

// array_column() over those rows pulls a single groupBy column back out, which
// is the pattern used to collect the grouped IDs for a follow-up query.
$ids = array_column(
\Drupal::entityTypeManager()->getStorage('node')->getAggregateQuery()
->accessCheck(FALSE)
->condition('status', TRUE)
->groupBy('type')
->execute(),
'type'
);
assertType('list<string|null>', $ids);

// Because the column can be present, the result is not statically empty, so a
// truthiness check on it is meaningful rather than "always false".
$filtered = $ids ? $ids : [];
assertType('list<string|null>', $filtered);
Loading