Skip to content

Commit 66c248d

Browse files
committed
Precise return type for Result::rowCount() based on detected driver
1 parent ba9563e commit 66c248d

10 files changed

+292
-0
lines changed

extension.neon

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,18 @@ services:
313313
class: PHPStan\Type\Doctrine\DBAL\QueryBuilder\QueryBuilderExecuteMethodExtension
314314
tags:
315315
- phpstan.broker.dynamicMethodReturnTypeExtension
316+
-
317+
class: PHPStan\Type\Doctrine\DBAL\RowCountMethodDynamicReturnTypeExtension
318+
arguments:
319+
class: Doctrine\DBAL\Result
320+
tags:
321+
- phpstan.broker.dynamicMethodReturnTypeExtension
322+
-
323+
class: PHPStan\Type\Doctrine\DBAL\RowCountMethodDynamicReturnTypeExtension
324+
arguments:
325+
class: Doctrine\DBAL\Driver\Result
326+
tags:
327+
- phpstan.broker.dynamicMethodReturnTypeExtension
316328

317329
# Type descriptors
318330
-
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\DBAL;
4+
5+
use Doctrine\DBAL\Driver\Result as DriverResult;
6+
use Doctrine\ORM\EntityManagerInterface;
7+
use PhpParser\Node\Expr\MethodCall;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Doctrine\Driver\DriverDetector;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Reflection\ParametersAcceptorSelector;
12+
use PHPStan\Reflection\ReflectionProvider;
13+
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
14+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
15+
use PHPStan\Type\Type;
16+
17+
class RowCountMethodDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
18+
{
19+
20+
/** @var string */
21+
private $class;
22+
23+
/** @var ObjectMetadataResolver */
24+
private $objectMetadataResolver;
25+
26+
/** @var DriverDetector */
27+
private $driverDetector;
28+
29+
/** @var ReflectionProvider */
30+
private $reflectionProvider;
31+
32+
public function __construct(
33+
string $class,
34+
ObjectMetadataResolver $objectMetadataResolver,
35+
DriverDetector $driverDetector,
36+
ReflectionProvider $reflectionProvider
37+
)
38+
{
39+
$this->class = $class;
40+
$this->objectMetadataResolver = $objectMetadataResolver;
41+
$this->driverDetector = $driverDetector;
42+
$this->reflectionProvider = $reflectionProvider;
43+
}
44+
45+
public function getClass(): string
46+
{
47+
return $this->class;
48+
}
49+
50+
public function isMethodSupported(MethodReflection $methodReflection): bool
51+
{
52+
return $methodReflection->getName() === 'rowCount';
53+
}
54+
55+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
56+
{
57+
$objectManager = $this->objectMetadataResolver->getObjectManager();
58+
if (!$objectManager instanceof EntityManagerInterface) {
59+
return null;
60+
}
61+
62+
$connection = $objectManager->getConnection();
63+
$driver = $this->driverDetector->detect($connection);
64+
if ($driver === null) {
65+
return null;
66+
}
67+
68+
$resultClass = $this->getResultClass($driver);
69+
if ($resultClass === null) {
70+
return null;
71+
}
72+
73+
if (!$this->reflectionProvider->hasClass($resultClass)) {
74+
return null;
75+
}
76+
77+
$resultReflection = $this->reflectionProvider->getClass($resultClass);
78+
if (!$resultReflection->hasNativeMethod('rowCount')) {
79+
return null;
80+
}
81+
82+
$rowCountMethod = $resultReflection->getNativeMethod('rowCount');
83+
$variant = ParametersAcceptorSelector::selectSingle($rowCountMethod->getVariants());
84+
85+
return $variant->getReturnType();
86+
}
87+
88+
/**
89+
* @param DriverDetector::* $driver
90+
* @return class-string<DriverResult>|null
91+
*/
92+
private function getResultClass(string $driver): ?string
93+
{
94+
switch ($driver) {
95+
case DriverDetector::IBM_DB2:
96+
return 'Doctrine\DBAL\Driver\IBMDB2\Result';
97+
case DriverDetector::MYSQLI:
98+
return 'Doctrine\DBAL\Driver\Mysqli\Result';
99+
case DriverDetector::OCI8:
100+
return 'Doctrine\DBAL\Driver\OCI8\Result';
101+
case DriverDetector::PDO_MYSQL:
102+
case DriverDetector::PDO_OCI:
103+
case DriverDetector::PDO_PGSQL:
104+
case DriverDetector::PDO_SQLITE:
105+
case DriverDetector::PDO_SQLSRV:
106+
return 'Doctrine\DBAL\Driver\PDO\Result';
107+
case DriverDetector::PGSQL:
108+
return 'Doctrine\DBAL\Driver\PgSQL\Result';
109+
case DriverDetector::SQLITE3:
110+
return 'Doctrine\DBAL\Driver\SQLite3\Result';
111+
case DriverDetector::SQLSRV:
112+
return 'Doctrine\DBAL\Driver\SQLSrv\Result';
113+
}
114+
115+
return null;
116+
}
117+
118+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\DBAL;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
7+
class MysqliResultRowCountReturnTypeTest extends TypeInferenceTestCase
8+
{
9+
10+
/** @return iterable<mixed> */
11+
public function dataFileAsserts(): iterable
12+
{
13+
yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-row-count.php');
14+
}
15+
16+
/**
17+
* @dataProvider dataFileAsserts
18+
* @param mixed ...$args
19+
*/
20+
public function testFileAsserts(
21+
string $assertType,
22+
string $file,
23+
...$args
24+
): void
25+
{
26+
$this->assertFileAsserts($assertType, $file, ...$args);
27+
}
28+
29+
/** @return string[] */
30+
public static function getAdditionalConfigFiles(): array
31+
{
32+
return [__DIR__ . '/mysqli.neon'];
33+
}
34+
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\DBAL;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
7+
class PDOResultRowCountReturnTypeTest extends TypeInferenceTestCase
8+
{
9+
10+
/** @return iterable<mixed> */
11+
public function dataFileAsserts(): iterable
12+
{
13+
yield from $this->gatherAssertTypes(__DIR__ . '/data/pdo-result-row-count.php');
14+
}
15+
16+
/**
17+
* @dataProvider dataFileAsserts
18+
* @param mixed ...$args
19+
*/
20+
public function testFileAsserts(
21+
string $assertType,
22+
string $file,
23+
...$args
24+
): void
25+
{
26+
$this->assertFileAsserts($assertType, $file, ...$args);
27+
}
28+
29+
/** @return string[] */
30+
public static function getAdditionalConfigFiles(): array
31+
{
32+
return [__DIR__ . '/pdo.neon'];
33+
}
34+
35+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace MysqliResultRowCount;
4+
5+
use Doctrine\DBAL\Result;
6+
use Doctrine\DBAL\Driver\Result as DriverResult;
7+
use function PHPStan\Testing\assertType;
8+
9+
function (Result $r): void {
10+
assertType('int|numeric-string', $r->rowCount());
11+
};
12+
13+
function (DriverResult $r): void {
14+
assertType('int|numeric-string', $r->rowCount());
15+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace PDOResultRowCount;
4+
5+
use Doctrine\DBAL\Result;
6+
use Doctrine\DBAL\Driver\Result as DriverResult;
7+
use function PHPStan\Testing\assertType;
8+
9+
function (Result $r): void {
10+
assertType('int', $r->rowCount());
11+
};
12+
13+
function (DriverResult $r): void {
14+
assertType('int', $r->rowCount());
15+
};

tests/Type/Doctrine/DBAL/mysqli.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
includes:
2+
- ../../../../extension.neon
3+
4+
parameters:
5+
doctrine:
6+
objectManagerLoader: mysqli.php

tests/Type/Doctrine/DBAL/mysqli.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php declare(strict_types = 1);
2+
3+
use Cache\Adapter\PHPArray\ArrayCachePool;
4+
use Doctrine\Common\Annotations\AnnotationReader;
5+
use Doctrine\DBAL\DriverManager;
6+
use Doctrine\ORM\Configuration;
7+
use Doctrine\ORM\EntityManager;
8+
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
9+
10+
$config = new Configuration();
11+
$config->setProxyDir(__DIR__);
12+
$config->setProxyNamespace('App\GeneratedProxy');
13+
$config->setMetadataCache(new ArrayCachePool());
14+
$config->setMetadataDriverImpl(new AnnotationDriver(
15+
new AnnotationReader(),
16+
[__DIR__ . '/data']
17+
));
18+
19+
return new EntityManager(
20+
DriverManager::getConnection([
21+
'driver' => 'mysqli',
22+
'memory' => true,
23+
]),
24+
$config
25+
);

tests/Type/Doctrine/DBAL/pdo.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
includes:
2+
- ../../../../extension.neon
3+
4+
parameters:
5+
doctrine:
6+
objectManagerLoader: pdo.php

tests/Type/Doctrine/DBAL/pdo.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php declare(strict_types = 1);
2+
3+
use Cache\Adapter\PHPArray\ArrayCachePool;
4+
use Doctrine\Common\Annotations\AnnotationReader;
5+
use Doctrine\DBAL\DriverManager;
6+
use Doctrine\ORM\Configuration;
7+
use Doctrine\ORM\EntityManager;
8+
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
9+
10+
$config = new Configuration();
11+
$config->setProxyDir(__DIR__);
12+
$config->setProxyNamespace('App\GeneratedProxy');
13+
$config->setMetadataCache(new ArrayCachePool());
14+
$config->setMetadataDriverImpl(new AnnotationDriver(
15+
new AnnotationReader(),
16+
[__DIR__ . '/data']
17+
));
18+
19+
return new EntityManager(
20+
DriverManager::getConnection([
21+
'driver' => 'pdo_pgsql',
22+
'memory' => true,
23+
]),
24+
$config
25+
);

0 commit comments

Comments
 (0)