diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2955f0fe..4e53f520 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,27 +27,32 @@ jobs: # dependencies: "lowest" - php-version: "7.4" + dbal-version: "^3" db-image: 'mysql:8.0' reflector: "pdo-mysql" mode: "recording" dependencies: "highest" - php-version: "8.0" + dbal-version: "^3" db-image: 'mysql:8.0' reflector: "pdo-mysql" mode: "recording" dependencies: "highest" - php-version: "8.0" + dbal-version: "^3" db-image: 'mysql:8.0' reflector: "mysqli" mode: "recording" dependencies: "highest" - php-version: "8.1" + dbal-version: "^3" db-image: 'mysql:8.0' reflector: "mysqli" mode: "recording" dependencies: "highest" - php-version: '8.1' + dbal-version: "^3" db-image: 'mariadb:latest' platform: 'mariadb' reflector: "mysqli" @@ -55,29 +60,48 @@ jobs: dependencies: "highest" - php-version: "8.2" + dbal-version: "^3" db-image: 'mysql:8.0' reflector: "mysqli" mode: "recording" dependencies: "highest" - php-version: '8.2' + dbal-version: "^3" db-image: 'mariadb:latest' platform: 'mariadb' reflector: "mysqli" mode: "recording" dependencies: "highest" + - php-version: "8.3" + dbal-version: "^4" + db-image: 'mysql:8.0' + reflector: "mysqli" + mode: "recording" + dependencies: "highest" + + - php-version: "8.4" + dbal-version: "^4" + db-image: 'mysql:8.0' + reflector: "mysqli" + mode: "recording" + dependencies: "highest" + - php-version: "8.1" + dbal-version: "^3" db-image: 'mysql:8.0' reflector: "pdo-mysql" mode: "replay-and-recording" dependencies: "highest" - php-version: "8.1" + dbal-version: "^3" db-image: 'mysql:8.0' reflector: "pdo-mysql" mode: "empty-recording" dependencies: "highest" - php-version: "8.1" + dbal-version: "^3" db-image: 'mysql:8.0' reflector: "pdo-mysql" mode: "empty-replay-and-recording" @@ -120,7 +144,7 @@ jobs: run: composer require sqlftw/sqlftw --ignore-platform-req=php+ - name: Install doctrine/dbal (optional dependency) - run: composer require doctrine/dbal:^3 --ignore-platform-req=php+ + run: "composer require doctrine/dbal:${{ matrix.dbal-version }} --ignore-platform-req=php+" - name: Setup Problem Matchers for PHPUnit run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" diff --git a/composer.json b/composer.json index 04489a83..b7f22838 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "php": "^7.4 || ^8.0", "composer-runtime-api": "^2.0", "composer/semver": "^3.2", - "doctrine/dbal": "3.*", + "doctrine/dbal": "3.*|4.*", "phpstan/phpstan": "^2.0" }, "require-dev": { diff --git a/config/rules.neon b/config/rules.neon index b401c2ba..c91aefad 100644 --- a/config/rules.neon +++ b/config/rules.neon @@ -46,6 +46,8 @@ services: - 'PDO::prepare#0' - 'mysqli::query#0' - 'mysqli::execute_query#0' + - 'Doctrine\DBAL\Connection::executeQuery#0' + - 'Doctrine\DBAL\Connection::executeStatement#0' - 'Doctrine\DBAL\Connection::query#0' # deprecated in doctrine - 'Doctrine\DBAL\Connection::exec#0' # deprecated in doctrine diff --git a/src/Extensions/DoctrineConnectionExecuteQueryDynamicReturnTypeExtension.php b/src/Extensions/DoctrineConnectionExecuteQueryDynamicReturnTypeExtension.php index 48413a1c..bbc43815 100644 --- a/src/Extensions/DoctrineConnectionExecuteQueryDynamicReturnTypeExtension.php +++ b/src/Extensions/DoctrineConnectionExecuteQueryDynamicReturnTypeExtension.php @@ -44,7 +44,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } // make sure we don't report wrong types in doctrine 2.x - if (! InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '3.*')) { + if (! InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '3.*|4.*')) { return null; } diff --git a/src/Extensions/DoctrineConnectionFetchDynamicReturnTypeExtension.php b/src/Extensions/DoctrineConnectionFetchDynamicReturnTypeExtension.php index db972151..57d866ac 100644 --- a/src/Extensions/DoctrineConnectionFetchDynamicReturnTypeExtension.php +++ b/src/Extensions/DoctrineConnectionFetchDynamicReturnTypeExtension.php @@ -57,7 +57,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } // make sure we don't report wrong types in doctrine 2.x - if (! InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '3.*')) { + if (! InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '3.*|4.*')) { return null; } diff --git a/src/Extensions/DoctrineConnectionPrepareDynamicReturnTypeExtension.php b/src/Extensions/DoctrineConnectionPrepareDynamicReturnTypeExtension.php index 1b6a5eba..2f6987a8 100644 --- a/src/Extensions/DoctrineConnectionPrepareDynamicReturnTypeExtension.php +++ b/src/Extensions/DoctrineConnectionPrepareDynamicReturnTypeExtension.php @@ -44,7 +44,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } // make sure we don't report wrong types in doctrine 2.x - if (! InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '3.*')) { + if (! InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '3.*|4.*')) { return null; } diff --git a/src/Extensions/DoctrineConnectionQueryDynamicReturnTypeExtension.php b/src/Extensions/DoctrineConnectionQueryDynamicReturnTypeExtension.php index 7f69e7c3..8ff4436c 100644 --- a/src/Extensions/DoctrineConnectionQueryDynamicReturnTypeExtension.php +++ b/src/Extensions/DoctrineConnectionQueryDynamicReturnTypeExtension.php @@ -44,7 +44,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } // make sure we don't report wrong types in doctrine 2.x - if (! InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '3.*')) { + if (! InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '3.*|4.*')) { return null; } diff --git a/src/Extensions/DoctrineResultDynamicReturnTypeExtension.php b/src/Extensions/DoctrineResultDynamicReturnTypeExtension.php index 8454f533..fb6a6aa4 100644 --- a/src/Extensions/DoctrineResultDynamicReturnTypeExtension.php +++ b/src/Extensions/DoctrineResultDynamicReturnTypeExtension.php @@ -48,7 +48,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { // make sure we don't report wrong types in doctrine 2.x - if (! InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '3.*')) { + if (! InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '3.*|4.*')) { return null; } diff --git a/src/Extensions/DoctrineStatementExecuteDynamicReturnTypeExtension.php b/src/Extensions/DoctrineStatementExecuteDynamicReturnTypeExtension.php index 2469ccc8..9642d453 100644 --- a/src/Extensions/DoctrineStatementExecuteDynamicReturnTypeExtension.php +++ b/src/Extensions/DoctrineStatementExecuteDynamicReturnTypeExtension.php @@ -40,7 +40,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } // make sure we don't report wrong types in doctrine 2.x - if (! InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '3.*')) { + if (! InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '3.*|4.*')) { return null; } diff --git a/tests/default/DbaInferenceTest.php b/tests/default/DbaInferenceTest.php index 41854f63..f104a8d3 100644 --- a/tests/default/DbaInferenceTest.php +++ b/tests/default/DbaInferenceTest.php @@ -18,6 +18,9 @@ public function dataFileAsserts(): iterable } yield from $this->gatherAssertTypes(__DIR__ . '/data/doctrine-dbal-union-result.php'); + if (InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '3.*')) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/doctrine-dbal3.php'); + } yield from $this->gatherAssertTypes(__DIR__ . '/data/doctrine-dbal.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/inference-placeholder.php'); diff --git a/tests/default/data/doctrine-dbal-union-result.php b/tests/default/data/doctrine-dbal-union-result.php index 4d86be6c..69fdab12 100644 --- a/tests/default/data/doctrine-dbal-union-result.php +++ b/tests/default/data/doctrine-dbal-union-result.php @@ -26,14 +26,4 @@ public function doFoo(Connection $conn) assertType('array{adaid: int<-32768, 32767>}|array{email: string}|false', $fetch); } } - - public function doBar(Connection $conn) - { - $queries = ['SELECT adaid FROM ada', 'SELECT email FROM ada']; - - foreach ($queries as $query) { - $result = $conn->query($query); - assertType('array{adaid: int<-32768, 32767>}|array{email: string}|false', $result->fetchAssociative()); - } - } } diff --git a/tests/default/data/doctrine-dbal.php b/tests/default/data/doctrine-dbal.php index 4fc0f957..a614406f 100644 --- a/tests/default/data/doctrine-dbal.php +++ b/tests/default/data/doctrine-dbal.php @@ -10,9 +10,9 @@ class Foo { - public function foo(Connection $conn) + public function resultFetching(Connection $conn) { - $result = $conn->query('SELECT email, adaid FROM ada'); + $result = $conn->executeQuery('SELECT email, adaid FROM ada'); $columnCount = $result->columnCount(); assertType('2', $columnCount); diff --git a/tests/default/data/doctrine-dbal3.php b/tests/default/data/doctrine-dbal3.php new file mode 100644 index 00000000..0536643d --- /dev/null +++ b/tests/default/data/doctrine-dbal3.php @@ -0,0 +1,60 @@ +query('SELECT email, adaid FROM ada'); + + $columnCount = $result->columnCount(); + assertType('2', $columnCount); + + $fetch = $result->fetchOne(); + assertType('string|false', $fetch); + + $fetch = $result->fetchNumeric(); + assertType('array{string, int<-32768, 32767>}|false', $fetch); + + $fetch = $result->fetchFirstColumn(); + assertType('list', $fetch); + + $fetch = $result->fetchAssociative(); + assertType('array{email: string, adaid: int<-32768, 32767>}|false', $fetch); + + $fetch = $result->fetchAllNumeric(); + assertType('list}>', $fetch); + + $fetch = $result->fetchAllAssociative(); + assertType('list}>', $fetch); + + $fetch = $result->fetchAllKeyValue(); + assertType('array>', $fetch); + + $fetch = $result->iterateNumeric(); + assertType('Traversable}>', $fetch); + + $fetch = $result->iterateAssociative(); + assertType('Traversable}>', $fetch); + + $fetch = $result->iterateColumn(); + assertType('Traversable', $fetch); + + $fetch = $result->iterateKeyValue(); + assertType('Traversable>', $fetch); + } + + public function unionQueriesResult(Connection $conn) + { + $queries = ['SELECT adaid FROM ada', 'SELECT email FROM ada']; + + foreach ($queries as $query) { + $result = $conn->query($query); + assertType('array{adaid: int<-32768, 32767>}|array{email: string}|false', $result->fetchAssociative()); + } + } +} \ No newline at end of file diff --git a/tests/rules/SyntaxErrorInQueryMethodRuleTest.php b/tests/rules/SyntaxErrorInQueryMethodRuleTest.php index c135ec94..d2a4eb1d 100644 --- a/tests/rules/SyntaxErrorInQueryMethodRuleTest.php +++ b/tests/rules/SyntaxErrorInQueryMethodRuleTest.php @@ -4,6 +4,8 @@ namespace staabm\PHPStanDba\Tests; +use Composer\InstalledVersions; +use Composer\Semver\VersionParser; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use staabm\PHPStanDba\QueryReflection\MysqliQueryReflector; @@ -302,6 +304,46 @@ public function testSyntaxErrorInQueryRule(): void $this->analyse([__DIR__ . '/data/syntax-error-in-query-method.php'], $expected); } + public function testSyntaxErrorInQueryRuleForDoctrineDbal3(): void + { + if (! InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '3.*')) { + self::markTestSkipped('Doctrine DBAL 3.x test only.'); + } + + if (MysqliQueryReflector::NAME === getenv('DBA_REFLECTOR')) { + $expected = [ + [ + "Query error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL/MariaDB server version for the right syntax to use near 'freigabe1u1 FROM ada LIMIT 0' at line 1 (1064).", + 12, + ], + ]; + } elseif (PdoMysqlQueryReflector::NAME === getenv('DBA_REFLECTOR')) { + if ('mariadb' === $_ENV['DBA_PLATFORM']) { + self::markTestSkipped("We don't test all variants of expectations for all drivers"); + } + + $expected = [ + [ + "Query error: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL/MariaDB server version for the right syntax to use near 'freigabe1u1 FROM ada LIMIT 0' at line 1 (42000).", + 12, + ], + ]; + } elseif (PdoPgSqlQueryReflector::NAME === getenv('DBA_REFLECTOR')) { + $expected = [ + [ + 'Query error: SQLSTATE[42601]: Syntax error: 7 ERROR: syntax error at or near "freigabe1u1" +LINE 1: SELECT email adaid WHERE gesperrt freigabe1u1 FROM ada LIMIT... + ^ (42601).', + 12, + ], + ]; + } else { + throw new \RuntimeException('Unsupported DBA_REFLECTOR ' . getenv('DBA_REFLECTOR')); + } + + $this->analyse([__DIR__ . '/data/syntax-error-in-query-method-dbal3.php'], $expected); + } + public function testMysqliExecuteQuery(): void { if (\PHP_VERSION_ID < 80200) { diff --git a/tests/rules/data/syntax-error-in-query-method-dbal3.php b/tests/rules/data/syntax-error-in-query-method-dbal3.php new file mode 100644 index 00000000..9747c115 --- /dev/null +++ b/tests/rules/data/syntax-error-in-query-method-dbal3.php @@ -0,0 +1,20 @@ +query($sql); + } + + public function noErrorOnQueriesContainingPlaceholders(\Doctrine\DBAL\Connection $conn) + { + // errors in this scenario are reported by SyntaxErrorInPreparedStatementMethodRule only + $conn->query('SELECT email, adaid, gesperrt, freigabe1u1 FROM ada WHERE adaid=?'); + } +} \ No newline at end of file diff --git a/tests/rules/data/syntax-error-in-query-method.php b/tests/rules/data/syntax-error-in-query-method.php index 89a5a27f..74f93c4f 100644 --- a/tests/rules/data/syntax-error-in-query-method.php +++ b/tests/rules/data/syntax-error-in-query-method.php @@ -79,13 +79,13 @@ public function syntaxErrorPdoPrepare(PDO $pdo) public function syntaxErrorDoctrineDbal(\Doctrine\DBAL\Connection $conn) { $sql = 'SELECT email adaid WHERE gesperrt freigabe1u1 FROM ada'; - $conn->query($sql); + $conn->executeQuery($sql); } public function noErrorOnQueriesContainingPlaceholders(\Doctrine\DBAL\Connection $conn) { // errors in this scenario are reported by SyntaxErrorInPreparedStatementMethodRule only - $conn->query('SELECT email, adaid, gesperrt, freigabe1u1 FROM ada WHERE adaid=?'); + $conn->executeQuery('SELECT email, adaid, gesperrt, freigabe1u1 FROM ada WHERE adaid=?'); } public function conditionalSyntaxError(PDO $pdo)