diff --git a/composer.json b/composer.json index 330329a9bd8..200790b4498 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ "require-dev": { "doctrine/coding-standard": "9.0.0", "jetbrains/phpstorm-stubs": "2021.1", - "phpstan/phpstan": "1.3.0", + "phpstan/phpstan": "1.4.0", "phpstan/phpstan-phpunit": "1.0.0", "phpstan/phpstan-strict-rules": "1.1.0", "phpunit/phpunit": "9.5.11", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 06fd2bbb968..ac0eeaa35bf 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -8,16 +8,6 @@ parameters: checkMissingIterableValueType: false checkGenericClassInNonGenericObjectType: false ignoreErrors: - # Requires a release of https://github.com/JetBrains/phpstorm-stubs/pull/553 - - - message: '~^Call to function assert\(\) with true will always evaluate to true\.$~' - path: src/Driver/PDO/Connection.php - - # Requires a release of https://github.com/JetBrains/phpstorm-stubs/pull/923 - - - message: '~^Instanceof between PDOStatement and PDOStatement will always evaluate to true\.$~' - path: src/Driver/PDO/Connection.php - # Requires a release of https://github.com/JetBrains/phpstorm-stubs/pull/1158 - message: '~^Strict comparison using === between string and null will always evaluate to false\.$~' @@ -50,26 +40,8 @@ parameters: - message: '~^Variable method call on .*~' paths: - - src/Schema/AbstractSchemaManager.php - src/Schema/Column.php - # Temporaily suppressed during up-merging an upgrade to PHPStan 0.12.33 - - - message: '~^Parameter #1 \$expected of static method PHPUnit\\Framework\\Assert::assertInstanceOf\(\) expects class-string, class-string&class-string given\.$~' - path: %currentWorkingDirectory%/tests/DriverManagerTest.php - - # Unlike Psalm, PHPStan doesn't understand the shape of the parse_str() return value - - - message: '~^Parameter #1 \$scheme of static method Doctrine\\DBAL\\DriverManager::parseDatabaseUrlScheme\(\) expects string\|null, int\|string\|null given\.$~' - paths: - - src/DriverManager.php - - # https://github.com/phpstan/phpstan-src/pull/692 - - - message: '~^Parameter #2 \$count of function array_fill expects int<0, max>, int given\.~' - paths: - - %currentWorkingDirectory%/src/Driver/Mysqli/Statement.php - # TODO: remove this once the support for PHP 7 is dropped - message: '~^Strict comparison using !== between int and false will always evaluate to true\.$~' @@ -83,7 +55,7 @@ parameters: - message: '~Template type T of method Doctrine\\DBAL\\DriverManager::getConnection\(\) is not referenced in a parameter\.~' paths: - - src/DriverManager.ph + - src/DriverManager.php - message: '~Method Doctrine\\DBAL\\DriverManager::createDriver\(\) should return Doctrine\\DBAL\\Driver but returns object\.~' @@ -101,35 +73,29 @@ parameters: paths: - src/Driver/Mysqli/Result.php - - - message: '~Method Doctrine\\DBAL\\Driver\\Mysqli\\Connection::exec\(\) should return int but returns int\|string\.~' - paths: - - src/Driver/Mysqli/Connection.php - - # Requires a release of https://github.com/phpstan/phpstan-src/pull/719 - - - message: '~Strict comparison using === between non-empty-string and false will always evaluate to false\.~' - path: src/Driver/PDO/Connection.php - # The schema manager isn't initialized if the test is skipped - message: '~Strict comparison using === between Doctrine\\DBAL\\Schema\\AbstractSchemaManager and null will always evaluate to false\.~' path: tests/Functional/Schema/SchemaManagerFunctionalTestCase.php + # Removing the (int) cast will make Psalm unhappy. - - message: '~^Parameter #1 \$message of class Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\ConnectionFailed constructor expects string, string\|null given\.$~' + message: '~^Casting to int something that''s already int\.$~' paths: + - src/Driver/Mysqli/Exception/ConnectionError.php - src/Driver/Mysqli/Exception/ConnectionFailed.php + - src/Driver/Mysqli/Exception/InvalidCharset.php + - src/Driver/Mysqli/Exception/StatementError.php - - message: '~^Cannot use array destructuring on array\|null\.$~' + message: '~^Unable to resolve the template type T in call to method Doctrine\\DBAL\\Portability\\Converter\:\:compose\(\)$~' paths: - - src/Driver/PDO/Connection.php + - src/Portability/Converter.php - # These properties are accessed via var_dump() + # We're testing with invalid input. - - message: '~Property Doctrine\\DBAL\\Tests\\Tools\\TestAsset\\.*::\$.* is never read, only written\.~' - path: tests/Tools/TestAsset/*.php + message: '~^Parameter #2 \$lockMode of method Doctrine\\DBAL\\Platforms\\AbstractPlatform\:\:appendLockHint\(\) expects 0\|1\|2\|4, 128 given\.$~' + path: tests/Platforms/AbstractPlatformTestCase.php includes: - vendor/phpstan/phpstan-phpunit/extension.neon diff --git a/psalm.xml.dist b/psalm.xml.dist index 6b1be01aa0b..92834ceefdf 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -107,6 +107,12 @@ + + + + + + @@ -196,12 +202,6 @@ - - - - - - diff --git a/src/Connection.php b/src/Connection.php index 9f4527b0e6e..0e1828a5be4 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -46,6 +46,7 @@ * configuration, emulated transaction nesting, lazy connecting and more. * * @psalm-import-type Params from DriverManager + * @psalm-consistent-constructor */ class Connection implements ServerVersionProvider { @@ -59,6 +60,11 @@ class Connection implements ServerVersionProvider */ public const PARAM_STR_ARRAY = ParameterType::STRING + self::ARRAY_PARAM_OFFSET; + /** + * Represents an array of ascii strings to be expanded by Doctrine SQL parsing. + */ + public const PARAM_ASCII_STR_ARRAY = ParameterType::ASCII + self::ARRAY_PARAM_OFFSET; + /** * Offset by which PARAM_* constants are detected as arrays of the param type. */ @@ -1453,7 +1459,11 @@ private function needsArrayParameterConversion(array $params, array $types): boo } foreach ($types as $type) { - if ($type === self::PARAM_INT_ARRAY || $type === self::PARAM_STR_ARRAY) { + if ( + $type === self::PARAM_INT_ARRAY + || $type === self::PARAM_STR_ARRAY + || $type === self::PARAM_ASCII_STR_ARRAY + ) { return true; } } diff --git a/src/Driver/Mysqli/Connection.php b/src/Driver/Mysqli/Connection.php index 885b08098a3..d20b81a3f42 100644 --- a/src/Driver/Mysqli/Connection.php +++ b/src/Driver/Mysqli/Connection.php @@ -10,9 +10,6 @@ use mysqli; use mysqli_sql_exception; -use function floor; -use function stripos; - final class Connection implements ConnectionInterface { /** @@ -30,26 +27,9 @@ public function __construct(mysqli $connection) $this->connection = $connection; } - /** - * {@inheritdoc} - * - * The server version detection includes a special case for MariaDB - * to support '5.5.5-' prefixed versions introduced in Maria 10+ - * - * @link https://jira.mariadb.org/browse/MDEV-4088 - */ public function getServerVersion(): string { - $serverInfos = $this->connection->get_server_info(); - if (stripos($serverInfos, 'mariadb') !== false) { - return $serverInfos; - } - - $majorVersion = floor($this->connection->server_version / 10000); - $minorVersion = floor(($this->connection->server_version - $majorVersion * 10000) / 100); - $patchVersion = floor($this->connection->server_version - $majorVersion * 10000 - $minorVersion * 100); - - return $majorVersion . '.' . $minorVersion . '.' . $patchVersion; + return $this->connection->get_server_info(); } public function prepare(string $sql): Statement diff --git a/src/Driver/PDO/PgSQL/Driver.php b/src/Driver/PDO/PgSQL/Driver.php index a83c4211ecd..60767dc1c92 100644 --- a/src/Driver/PDO/PgSQL/Driver.php +++ b/src/Driver/PDO/PgSQL/Driver.php @@ -28,7 +28,7 @@ public function connect(array $params): Connection $this->constructPdoDsn($params), $params['user'] ?? '', $params['password'] ?? '', - $driverOptions, + $driverOptions ); } catch (PDOException $exception) { throw Exception::new($exception); diff --git a/src/ExpandArrayParameters.php b/src/ExpandArrayParameters.php index 791b9810076..915e1819b30 100644 --- a/src/ExpandArrayParameters.php +++ b/src/ExpandArrayParameters.php @@ -97,7 +97,11 @@ private function acceptParameter(int|string $key, mixed $value): void $type = $this->originalTypes[$key]; - if ($type !== Connection::PARAM_INT_ARRAY && $type !== Connection::PARAM_STR_ARRAY) { + if ( + $type !== Connection::PARAM_INT_ARRAY + && $type !== Connection::PARAM_STR_ARRAY + && $type !== Connection::PARAM_ASCII_STR_ARRAY + ) { $this->appendTypedParameter([$value], $type); return; diff --git a/src/Platforms/AbstractPlatform.php b/src/Platforms/AbstractPlatform.php index 1c8f615b583..949407b559d 100644 --- a/src/Platforms/AbstractPlatform.php +++ b/src/Platforms/AbstractPlatform.php @@ -776,6 +776,7 @@ public function getForUpdateSQL(): string * * @param string $fromClause The FROM clause to append the hint for the given lock mode to * @param int $lockMode One of the Doctrine\DBAL\LockMode::* constants + * @psalm-param LockMode::* $lockMode */ public function appendLockHint(string $fromClause, int $lockMode): string { diff --git a/src/Platforms/Keywords/KeywordList.php b/src/Platforms/Keywords/KeywordList.php index 2db13dd004f..8c70152d707 100644 --- a/src/Platforms/Keywords/KeywordList.php +++ b/src/Platforms/Keywords/KeywordList.php @@ -10,6 +10,8 @@ /** * Abstract interface for a SQL reserved keyword dictionary. + * + * @psalm-consistent-constructor */ abstract class KeywordList { diff --git a/src/Types/JsonType.php b/src/Types/JsonType.php index dc395e80377..9c89c08be82 100644 --- a/src/Types/JsonType.php +++ b/src/Types/JsonType.php @@ -14,6 +14,7 @@ use function json_encode; use function stream_get_contents; +use const JSON_PRESERVE_ZERO_FRACTION; use const JSON_THROW_ON_ERROR; /** @@ -36,7 +37,7 @@ public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform) } try { - return json_encode($value, JSON_THROW_ON_ERROR); + return json_encode($value, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION); } catch (JsonException $e) { throw SerializationFailed::new($value, 'json', $e->getMessage(), $e); } diff --git a/tests/Connection/ExpandArrayParametersTest.php b/tests/Connection/ExpandArrayParametersTest.php index 3acee1427bb..521af432857 100644 --- a/tests/Connection/ExpandArrayParametersTest.php +++ b/tests/Connection/ExpandArrayParametersTest.php @@ -98,11 +98,16 @@ public static function dataExpandListParameters(): iterable [1 => ParameterType::STRING, 2 => ParameterType::STRING], ], 'Positional: explicit keys for array params and array types' => [ - 'SELECT * FROM Foo WHERE foo IN (?) AND bar IN (?) AND baz = ?', - [1 => ['bar1', 'bar2'], 2 => true, 0 => [1, 2, 3]], - [2 => ParameterType::BOOLEAN, 1 => Connection::PARAM_STR_ARRAY, 0 => Connection::PARAM_INT_ARRAY], - 'SELECT * FROM Foo WHERE foo IN (?, ?, ?) AND bar IN (?, ?) AND baz = ?', - [1, 2, 3, 'bar1', 'bar2', true], + 'SELECT * FROM Foo WHERE foo IN (?) AND bar IN (?) AND baz = ? AND bax IN (?)', + [1 => ['bar1', 'bar2'], 2 => true, 0 => [1, 2, 3], ['bax1', 'bax2']], + [ + 3 => Connection::PARAM_ASCII_STR_ARRAY, + 2 => ParameterType::BOOLEAN, + 1 => Connection::PARAM_STR_ARRAY, + 0 => Connection::PARAM_INT_ARRAY, + ], + 'SELECT * FROM Foo WHERE foo IN (?, ?, ?) AND bar IN (?, ?) AND baz = ? AND bax IN (?, ?)', + [1, 2, 3, 'bar1', 'bar2', true, 'bax1', 'bax2'], [ ParameterType::INTEGER, ParameterType::INTEGER, @@ -110,6 +115,8 @@ public static function dataExpandListParameters(): iterable ParameterType::STRING, ParameterType::STRING, ParameterType::BOOLEAN, + ParameterType::ASCII, + ParameterType::ASCII, ], ], 'Named: Very simple with param int' => [ @@ -231,6 +238,14 @@ public static function dataExpandListParameters(): iterable [], [], ], + [ + 'SELECT * FROM Foo WHERE foo IN (:foo) OR bar IN (:bar)', + ['foo' => [], 'bar' => []], + ['foo' => Connection::PARAM_ASCII_STR_ARRAY, 'bar' => Connection::PARAM_ASCII_STR_ARRAY], + 'SELECT * FROM Foo WHERE foo IN (NULL) OR bar IN (NULL)', + [], + [], + ], [ 'SELECT * FROM Foo WHERE foo IN (:foo) OR bar = :bar OR baz = :baz', ['foo' => [1, 2], 'bar' => 'bar', 'baz' => 'baz'], diff --git a/tests/Platforms/SQLServerPlatformTestCase.php b/tests/Platforms/SQLServerPlatformTestCase.php index 94fce2d27d2..91a9dda69a9 100644 --- a/tests/Platforms/SQLServerPlatformTestCase.php +++ b/tests/Platforms/SQLServerPlatformTestCase.php @@ -1776,6 +1776,8 @@ public function testAlterTableWithSchemaSameColumnComments(): void } /** + * @psalm-param LockMode::* $lockMode + * * @dataProvider getLockHints */ public function testAppendsLockHint(int $lockMode, string $lockHint): void diff --git a/tests/Tools/TestAsset/ChildClass.php b/tests/Tools/TestAsset/ChildClass.php deleted file mode 100644 index 65d7d67344e..00000000000 --- a/tests/Tools/TestAsset/ChildClass.php +++ /dev/null @@ -1,14 +0,0 @@ -type->requiresSQLCommentHint($this->platform)); } + public function testPHPNullValueConvertsToJsonNull(): void + { + self::assertNull($this->type->convertToDatabaseValue(null, $this->platform)); + } + + public function testPHPValueConvertsToJsonString(): void + { + $source = ['foo' => 'bar', 'bar' => 'foo']; + $databaseValue = $this->type->convertToDatabaseValue($source, $this->platform); + + self::assertSame('{"foo":"bar","bar":"foo"}', $databaseValue); + } + + public function testPHPFloatValueConvertsToJsonString(): void + { + $source = ['foo' => 11.4, 'bar' => 10.0]; + $databaseValue = $this->type->convertToDatabaseValue($source, $this->platform); + + self::assertSame('{"foo":11.4,"bar":10.0}', $databaseValue); + } + public function testSerializationFailure(): void { $object = (object) [];