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
26 changes: 26 additions & 0 deletions src/Type/Doctrine/ArgumentsProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@

namespace PHPStan\Type\Doctrine;

use Composer\InstalledVersions;
use PhpParser\Node\Arg;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Doctrine\ORM\DynamicQueryBuilderArgumentException;
use PHPStan\Type\Doctrine\QueryBuilder\Expr\ExprType;
use function class_exists;
use function constant;
use function count;
use function in_array;
use function sprintf;
use function strpos;
use function version_compare;

/** @api */
class ArgumentsProcessor
Expand Down Expand Up @@ -72,6 +77,16 @@ public function processArgs(
}
}

$enumCases = $value->getEnumCases();
if (
count($enumCases) === 1
&& $enumCases[0]->getClassName() === 'SortDirection'
&& $this->isSortDirectionSupported()
) {
$args[] = constant(sprintf('%s::%s', $enumCases[0]->getClassName(), $enumCases[0]->getEnumCaseName()));
continue;
}

if (count($value->getConstantScalarValues()) !== 1) {
throw new DynamicQueryBuilderArgumentException();
}
Expand All @@ -82,4 +97,15 @@ public function processArgs(
return $args;
}

private function isSortDirectionSupported(): bool
{
if (!class_exists(InstalledVersions::class)) {
return false;
}

$ormVersion = InstalledVersions::getVersion('doctrine/orm');

return $ormVersion !== null && version_compare($ormVersion, '3.7.0', '>=');
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use Throwable;
use function class_exists;

class NewExprDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
Expand Down Expand Up @@ -65,17 +66,17 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection,
}

try {
$exprObject = new $className(
...$this->argumentsProcessor->processArgs(
$scope,
$methodReflection->getName(),
$methodCall->getArgs(),
),
);
$args = $this->argumentsProcessor->processArgs($scope, $methodReflection->getName(), $methodCall->getArgs());
} catch (DynamicQueryBuilderArgumentException $e) {
return new ObjectType($this->reflectionProvider->getClassName($className));
}

try {
$exprObject = new $className(...$args);
} catch (Throwable $e) {
return new ObjectType($this->reflectionProvider->getClassName($className));
}

return new ExprType($className, $exprObject);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,13 @@ public function getTypeFromMethodCall(
}
}

$resultTypes[] = $this->getQueryType($queryBuilder->getDQL());
try {
$dql = $queryBuilder->getDQL();
} catch (Throwable $e) {
return null;
}

$resultTypes[] = $this->getQueryType($dql);
}

return TypeCombinator::union(...$resultTypes);
Expand Down
44 changes: 44 additions & 0 deletions tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

namespace PHPStan\Rules\Doctrine\ORM;

use Composer\InstalledVersions;
use Composer\Semver\VersionParser;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
use SortDirection;
use function class_exists;

/**
* @extends RuleTestCase<QueryBuilderDqlRule>
Expand Down Expand Up @@ -168,6 +172,46 @@ public function testDynamicWhere(): void
]);
}

public function testSortDirection(): void
{
if (!class_exists(SortDirection::class)) {
self::markTestSkipped('Test requires SortDirection enum.');
}

if (!InstalledVersions::satisfies(new VersionParser(), 'doctrine/orm', '>=3.7')) {
// ORM below 3.7 does not accept SortDirection so it stays a dynamic argument.
$this->analyse([__DIR__ . '/data/query-builder-dql-sort-direction.php'], [
['Could not analyse QueryBuilder with dynamic arguments.', 51],
['Could not analyse QueryBuilder with dynamic arguments.', 60],
['Could not analyse QueryBuilder with dynamic arguments.', 78],
]);
return;
}

$this->analyse([__DIR__ . '/data/query-builder-dql-sort-direction.php'], [
[
'QueryBuilder: [Semantical Error] line 0, col 63 near \'name ASC\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named name',
23,
],
[
'QueryBuilder: [Semantical Error] line 0, col 76 near \'name DESC\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named name',
41,
],
[
'QueryBuilder: [Semantical Error] line 0, col 63 near \'name DESC\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named name',
51,
],
[
'Could not analyse QueryBuilder with dynamic arguments.',
69,
],
[
'Could not analyse QueryBuilder with dynamic arguments.',
78,
],
]);
}

public static function getAdditionalConfigFiles(): array
{
return [
Expand Down
84 changes: 84 additions & 0 deletions tests/Rules/Doctrine/ORM/data/query-builder-dql-sort-direction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Doctrine\ORM;

use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Query\Expr\Comparison;
use Doctrine\ORM\Query\Expr\OrderBy;
use SortDirection;

class TestQueryBuilderSortDirectionRepository
{

/** @var EntityManager */
private $entityManager;

public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}

public function orderByUnknownField(): void
{
$this->entityManager->createQueryBuilder()
->select('e')
->from(MyEntity::class, 'e')
->orderBy('e.name', SortDirection::Ascending)
->getQuery();
}

public function orderByCorrect(): void
{
$this->entityManager->createQueryBuilder()
->select('e')
->from(MyEntity::class, 'e')
->orderBy('e.title', SortDirection::Descending)
->getQuery();
}

public function addOrderByUnknownField(): void
{
$this->entityManager->createQueryBuilder()
->select('e')
->from(MyEntity::class, 'e')
->orderBy('e.title', SortDirection::Ascending)
->addOrderBy('e.name', SortDirection::Descending)
->getQuery();
}

public function newExprUnknownField(): void
{
$this->entityManager->createQueryBuilder()
->select('e')
->from(MyEntity::class, 'e')
->add('orderBy', new OrderBy('e.name', SortDirection::Descending))
->getQuery();
}

public function newExprCorrect(): void
{
$this->entityManager->createQueryBuilder()
->select('e')
->from(MyEntity::class, 'e')
->add('orderBy', new OrderBy('e.title', SortDirection::Ascending))
->getQuery();
}

public function enumNotConvertibleToDql(): void
{
$this->entityManager->createQueryBuilder()
->select('e')
->from(MyEntity::class, 'e')
->where(new Comparison('e.id', '=', SortDirection::Ascending))
->getQuery();
}

public function enumNotAcceptedByConstructor(): void
{
$this->entityManager->createQueryBuilder()
->select('e')
->add('from', new \Doctrine\ORM\Query\Expr\From(SortDirection::Ascending, 'e'))
->getQuery();
}

}
Loading