Skip to content

Commit

Permalink
Introduce a DynamicMethodReturnTypeExtension for the DBAL QueryBuilde…
Browse files Browse the repository at this point in the history
…r execute method

fixes phpstan#117
  • Loading branch information
mitelg authored and ondrejmirtes committed Jan 18, 2021
1 parent 723ed14 commit ebaa9eb
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 0 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"doctrine/annotations": "^1.11.0",
"doctrine/collections": "^1.0",
"doctrine/common": "^2.7 || ^3.0",
"doctrine/dbal": "^2.11.0",
"doctrine/mongodb-odm": "^1.3 || ^2.1",
"doctrine/orm": "^2.5",
"doctrine/persistence": "^1.1 || ^2.0",
Expand Down
4 changes: 4 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ services:
arguments:
class: Doctrine\ORM\Query\Expr
argumentsProcessor: @doctrineQueryBuilderArgumentsProcessor
-
class: PHPStan\Type\Doctrine\DBAL\QueryBuilder\QueryBuilderExecuteMethodExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension

# Type descriptors
-
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Doctrine\DBAL\QueryBuilder;

use Doctrine\DBAL\Driver\ResultStatement;
use Doctrine\DBAL\Query\QueryBuilder;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;

class QueryBuilderExecuteMethodExtension implements DynamicMethodReturnTypeExtension
{

public function getClass(): string
{
return QueryBuilder::class;
}

public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'execute';
}

public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
$defaultReturnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();

$queryBuilderType = new ObjectType(QueryBuilder::class);
$var = $methodCall->var;
while ($var instanceof MethodCall) {
$varType = $scope->getType($var->var);
if (!$queryBuilderType->isSuperTypeOf($varType)->yes()) {
return $defaultReturnType;
}

$nameObject = $var->name;
if (!($nameObject instanceof Identifier)) {
return $defaultReturnType;
}

$name = $nameObject->toString();
if ($name === 'select' || $name === 'addSelect') {
return new ObjectType(ResultStatement::class);
}

$var = $var->var;
}

return $defaultReturnType;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public function dataTopics(): array
['entityManagerMergeReturn'],
['customRepositoryUsage'],
['queryBuilder'],
['dbalQueryBuilderExecuteDynamicReturn'],
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"message": "Cannot call method fetchAll() on Doctrine\\DBAL\\Driver\\ResultStatement|int.",
"line": 43,
"ignorable": true
},
{
"message": "Cannot call method fetchAll() on Doctrine\\DBAL\\Driver\\ResultStatement|int.",
"line": 60,
"ignorable": true
},
{
"message": "Cannot call method fetchAll() on Doctrine\\DBAL\\Driver\\ResultStatement|int.",
"line": 76,
"ignorable": true
},
{
"message": "Cannot call method fetchAll() on Doctrine\\DBAL\\Driver\\ResultStatement|int.",
"line": 87,
"ignorable": true
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php declare(strict_types=1);

namespace PHPStan\DoctrineIntegration\ORM\DbalQueryBuilderExecuteDynamicReturn;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Query\QueryBuilder;

class Example
{
/** @var Connection */
private $connection;

public function __construct(Connection $connection)
{
$this->connection = $connection;
}

/**
* @return mixed[]
*/
public function testCaseOne(int $userId): array
{
return $this->connection->createQueryBuilder()
->select('*')
->from('user')
->where('user.id = :id')
->setParameter('id', $userId)
->execute()
->fetchAll();
}

/**
* @return mixed[]
*/
public function testCaseTwo(int $userId): array
{
$qb = $this->connection->createQueryBuilder();
$qb->select('*');
$qb->from('user');
$qb->where('user.id = :id');
$qb->setParameter('id', $userId);

return $qb->execute()->fetchAll();
}

/**
* @return mixed[]
*/
public function testCaseThree(?int $userId = null): array
{
$qb = $this->connection->createQueryBuilder();
$qb->select('*');
$qb->from('user');

if ($userId !== null) {
$qb->where('user.id = :id');
$qb->setParameter('id', $userId);
}

return $qb->execute()->fetchAll();
}

/**
* @return mixed[]
*/
public function testCaseFourA(?int $userId = null): array
{
$qb = $this->connection->createQueryBuilder();
$qb->select('*');
$qb->from('user');

if ($userId !== null) {
return $this->testCaseFourB($qb, $userId);
}

return $qb->execute()->fetchAll();
}

/**
* @return mixed[]
*/
private function testCaseFourB(QueryBuilder $qb, int $userId): array
{
$qb->where('user.id = :id');
$qb->setParameter('id', $userId);

return $qb->execute()->fetchAll();
}
}

0 comments on commit ebaa9eb

Please sign in to comment.