From 66c248d09cde902ff83a6a2d53e7be92f5426e98 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 1 Sep 2024 14:15:29 +0200 Subject: [PATCH] Precise return type for `Result::rowCount()` based on detected driver --- extension.neon | 12 ++ ...wCountMethodDynamicReturnTypeExtension.php | 118 ++++++++++++++++++ .../MysqliResultRowCountReturnTypeTest.php | 35 ++++++ .../DBAL/PDOResultRowCountReturnTypeTest.php | 35 ++++++ .../DBAL/data/mysqli-result-row-count.php | 15 +++ .../DBAL/data/pdo-result-row-count.php | 15 +++ tests/Type/Doctrine/DBAL/mysqli.neon | 6 + tests/Type/Doctrine/DBAL/mysqli.php | 25 ++++ tests/Type/Doctrine/DBAL/pdo.neon | 6 + tests/Type/Doctrine/DBAL/pdo.php | 25 ++++ 10 files changed, 292 insertions(+) create mode 100644 src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php create mode 100644 tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php create mode 100644 tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php create mode 100644 tests/Type/Doctrine/DBAL/data/mysqli-result-row-count.php create mode 100644 tests/Type/Doctrine/DBAL/data/pdo-result-row-count.php create mode 100644 tests/Type/Doctrine/DBAL/mysqli.neon create mode 100644 tests/Type/Doctrine/DBAL/mysqli.php create mode 100644 tests/Type/Doctrine/DBAL/pdo.neon create mode 100644 tests/Type/Doctrine/DBAL/pdo.php diff --git a/extension.neon b/extension.neon index a4f36b15..63365d37 100644 --- a/extension.neon +++ b/extension.neon @@ -313,6 +313,18 @@ services: class: PHPStan\Type\Doctrine\DBAL\QueryBuilder\QueryBuilderExecuteMethodExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Type\Doctrine\DBAL\RowCountMethodDynamicReturnTypeExtension + arguments: + class: Doctrine\DBAL\Result + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Type\Doctrine\DBAL\RowCountMethodDynamicReturnTypeExtension + arguments: + class: Doctrine\DBAL\Driver\Result + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension # Type descriptors - diff --git a/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php b/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php new file mode 100644 index 00000000..bb4f626b --- /dev/null +++ b/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php @@ -0,0 +1,118 @@ +class = $class; + $this->objectMetadataResolver = $objectMetadataResolver; + $this->driverDetector = $driverDetector; + $this->reflectionProvider = $reflectionProvider; + } + + public function getClass(): string + { + return $this->class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'rowCount'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + $objectManager = $this->objectMetadataResolver->getObjectManager(); + if (!$objectManager instanceof EntityManagerInterface) { + return null; + } + + $connection = $objectManager->getConnection(); + $driver = $this->driverDetector->detect($connection); + if ($driver === null) { + return null; + } + + $resultClass = $this->getResultClass($driver); + if ($resultClass === null) { + return null; + } + + if (!$this->reflectionProvider->hasClass($resultClass)) { + return null; + } + + $resultReflection = $this->reflectionProvider->getClass($resultClass); + if (!$resultReflection->hasNativeMethod('rowCount')) { + return null; + } + + $rowCountMethod = $resultReflection->getNativeMethod('rowCount'); + $variant = ParametersAcceptorSelector::selectSingle($rowCountMethod->getVariants()); + + return $variant->getReturnType(); + } + + /** + * @param DriverDetector::* $driver + * @return class-string|null + */ + private function getResultClass(string $driver): ?string + { + switch ($driver) { + case DriverDetector::IBM_DB2: + return 'Doctrine\DBAL\Driver\IBMDB2\Result'; + case DriverDetector::MYSQLI: + return 'Doctrine\DBAL\Driver\Mysqli\Result'; + case DriverDetector::OCI8: + return 'Doctrine\DBAL\Driver\OCI8\Result'; + case DriverDetector::PDO_MYSQL: + case DriverDetector::PDO_OCI: + case DriverDetector::PDO_PGSQL: + case DriverDetector::PDO_SQLITE: + case DriverDetector::PDO_SQLSRV: + return 'Doctrine\DBAL\Driver\PDO\Result'; + case DriverDetector::PGSQL: + return 'Doctrine\DBAL\Driver\PgSQL\Result'; + case DriverDetector::SQLITE3: + return 'Doctrine\DBAL\Driver\SQLite3\Result'; + case DriverDetector::SQLSRV: + return 'Doctrine\DBAL\Driver\SQLSrv\Result'; + } + + return null; + } + +} diff --git a/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php b/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php new file mode 100644 index 00000000..5a4841f5 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php @@ -0,0 +1,35 @@ + */ + public function dataFileAsserts(): iterable + { + yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-row-count.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + /** @return string[] */ + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/mysqli.neon']; + } + +} diff --git a/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php b/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php new file mode 100644 index 00000000..0b6aa6bc --- /dev/null +++ b/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php @@ -0,0 +1,35 @@ + */ + public function dataFileAsserts(): iterable + { + yield from $this->gatherAssertTypes(__DIR__ . '/data/pdo-result-row-count.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + /** @return string[] */ + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/pdo.neon']; + } + +} diff --git a/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count.php b/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count.php new file mode 100644 index 00000000..84f69543 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count.php @@ -0,0 +1,15 @@ +rowCount()); +}; + +function (DriverResult $r): void { + assertType('int|numeric-string', $r->rowCount()); +}; diff --git a/tests/Type/Doctrine/DBAL/data/pdo-result-row-count.php b/tests/Type/Doctrine/DBAL/data/pdo-result-row-count.php new file mode 100644 index 00000000..ec73a00c --- /dev/null +++ b/tests/Type/Doctrine/DBAL/data/pdo-result-row-count.php @@ -0,0 +1,15 @@ +rowCount()); +}; + +function (DriverResult $r): void { + assertType('int', $r->rowCount()); +}; diff --git a/tests/Type/Doctrine/DBAL/mysqli.neon b/tests/Type/Doctrine/DBAL/mysqli.neon new file mode 100644 index 00000000..e287719f --- /dev/null +++ b/tests/Type/Doctrine/DBAL/mysqli.neon @@ -0,0 +1,6 @@ +includes: + - ../../../../extension.neon + +parameters: + doctrine: + objectManagerLoader: mysqli.php diff --git a/tests/Type/Doctrine/DBAL/mysqli.php b/tests/Type/Doctrine/DBAL/mysqli.php new file mode 100644 index 00000000..2bc11294 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/mysqli.php @@ -0,0 +1,25 @@ +setProxyDir(__DIR__); +$config->setProxyNamespace('App\GeneratedProxy'); +$config->setMetadataCache(new ArrayCachePool()); +$config->setMetadataDriverImpl(new AnnotationDriver( + new AnnotationReader(), + [__DIR__ . '/data'] +)); + +return new EntityManager( + DriverManager::getConnection([ + 'driver' => 'mysqli', + 'memory' => true, + ]), + $config +); diff --git a/tests/Type/Doctrine/DBAL/pdo.neon b/tests/Type/Doctrine/DBAL/pdo.neon new file mode 100644 index 00000000..ee4897c8 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/pdo.neon @@ -0,0 +1,6 @@ +includes: + - ../../../../extension.neon + +parameters: + doctrine: + objectManagerLoader: pdo.php diff --git a/tests/Type/Doctrine/DBAL/pdo.php b/tests/Type/Doctrine/DBAL/pdo.php new file mode 100644 index 00000000..c7e48751 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/pdo.php @@ -0,0 +1,25 @@ +setProxyDir(__DIR__); +$config->setProxyNamespace('App\GeneratedProxy'); +$config->setMetadataCache(new ArrayCachePool()); +$config->setMetadataDriverImpl(new AnnotationDriver( + new AnnotationReader(), + [__DIR__ . '/data'] +)); + +return new EntityManager( + DriverManager::getConnection([ + 'driver' => 'pdo_pgsql', + 'memory' => true, + ]), + $config +);