diff --git a/README.md b/README.md index bf14ee7..3895ef3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ -# PhpDb Adapter Mysql +# PhpDb Mysql +[![PHP Version](https://img.shields.io/packagist/php-v/php-db/phpdb-mysql)](https://packagist.org/packages/php-db/phpdb-mysql) [![Continuous Integration](https://github.com/php-db/phpdb-mysql/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/php-db/phpdb-mysql/actions/workflows/continuous-integration.yml) [![codecov](https://codecov.io/gh/php-db/phpdb-mysql/graph/badge.svg)](https://codecov.io/gh/php-db/phpdb-mysql) [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fphp-db%2Fphpdb-mysql%2F0.5.x)](https://dashboard.stryker-mutator.io/reports/github.com/php-db/phpdb-mysql/0.5.x) +[![Latest Stable Version](https://img.shields.io/packagist/v/php-db/phpdb-mysql)](https://packagist.org/packages/php-db/phpdb-mysql) +[![License](https://img.shields.io/github/license/php-db/phpdb-mysql)](LICENSE) This package provides MySQL support for PhpDb. diff --git a/analysis-baseline.toml b/analysis-baseline.toml new file mode 100644 index 0000000..384d919 --- /dev/null +++ b/analysis-baseline.toml @@ -0,0 +1,685 @@ +variant = "loose" + +[[issues]] +file = "src/AdapterPlatform.php" +code = "falsable-return-statement" +message = '''Function `PhpDb\Mysql\AdapterPlatform::quoteViaDriver` is declared to return `null|string` but possibly returns 'false' (inferred as `false|string`).''' +count = 1 + +[[issues]] +file = "src/AdapterPlatform.php" +code = "impossible-condition" +message = "This condition (type `false`) will always evaluate to false." +count = 1 + +[[issues]] +file = "src/AdapterPlatform.php" +code = "invalid-return-statement" +message = 'Invalid return type for function `PhpDb\Mysql\AdapterPlatform::quoteViaDriver`: expected `null|string`, but found `false|string`.' +count = 1 + +[[issues]] +file = "src/AdapterPlatform.php" +code = "missing-constant-type" +message = "Class constant `PLATFORM_NAME` is missing a type hint." +count = 1 + +[[issues]] +file = "src/AdapterPlatform.php" +code = "possibly-invalid-argument" +message = 'Possible argument type mismatch for argument #1 of `PhpDb\Mysql\AdapterPlatform::quoteViaDriver`: expected `string`, but possibly received `bool|float|int|string`.' +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "incompatible-property-type" +message = 'Property `PhpDb\Mysql\Connection::$resource` has an incompatible type declaration from docblock.' +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "invalid-iterator" +message = "The expression provided to `foreach` is not iterable. It resolved to type `mixed`, which is not iterable." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "invalid-property-assignment-value" +message = "Invalid type for property `$resource`: expected `mysqli`, but got `null`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #1 of `mysqli::options`: expected `int`, but found `mixed`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #1 of `mysqli::set_charset`: expected `string`, but found `mixed`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #1 of `mysqli::ssl_set`: expected `null|string`, but found `nonnull`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #2 of `mysqli::options`: expected `int|string`, but found `mixed`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #2 of `mysqli::real_connect`: expected `null|string`, but found `mixed`." +count = 2 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #2 of `mysqli::ssl_set`: expected `null|string`, but found `nonnull`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #3 of `mysqli::real_connect`: expected `null|string`, but found `mixed`." +count = 2 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #3 of `mysqli::ssl_set`: expected `null|string`, but found `nonnull`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #4 of `mysqli::real_connect`: expected `null|string`, but found `mixed`." +count = 2 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #4 of `mysqli::ssl_set`: expected `null|string`, but found `nonnull`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #5 of `mysqli::ssl_set`: expected `null|string`, but found `nonnull`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." +count = 7 + +[[issues]] +file = "src/Connection.php" +code = "mixed-assignment" +message = "Assigning `nonnull` type to a variable may lead to unexpected behavior." +count = 6 + +[[issues]] +file = "src/Connection.php" +code = "mixed-operand" +message = "Left operand in `&&` operation has `mixed` type." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-return-statement" +message = "Could not infer a precise return type for function `{closure:src/Connection.php:118:31}`. Saw type `nonnull`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "possibly-null-argument" +message = "Argument #1 of method `Exception::__construct` is possibly `null`, but parameter type `string` does not accept it." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "possibly-undefined-string-array-index" +message = "Possibly undefined array key `string('charset')` accessed on `array`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "possibly-undefined-string-array-index" +message = "Possibly undefined array key `string('driver_options')` accessed on `array`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "redundant-condition" +message = "This condition (type `true`) will always evaluate to true." +count = 2 + +[[issues]] +file = "src/Container/ConnectionInterfaceFactory.php" +code = "less-specific-argument" +message = 'Argument type mismatch for argument #1 of `PhpDb\Mysql\Connection::__construct`: expected `array|mysqli|null`, but provided type `non-empty-array` is less specific.' +count = 1 + +[[issues]] +file = "src/Container/ConnectionInterfaceFactory.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #2 of `PhpDb\Adapter\Exception\InvalidConnectionParametersException::__construct`: expected `array`, but found `nonnull`.' +count = 1 + +[[issues]] +file = "src/Container/ConnectionInterfaceFactory.php" +code = "mixed-assignment" +message = "Assigning `nonnull` type to a variable may lead to unexpected behavior." +count = 1 + +[[issues]] +file = "src/Container/DriverInterfaceFactory.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #2 of `Laminas\ServiceManager\ServiceManager::build`: expected `array|null`, but found `nonnull`.' +count = 1 + +[[issues]] +file = "src/Container/MetadataInterfaceFactory.php" +code = "missing-constant-type" +message = "Class constant `ADAPTER_SERVICE_NAME` is missing a type hint." +count = 1 + +[[issues]] +file = "src/Container/MetadataInterfaceFactory.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #1 of `PhpDb\Metadata\Source\AbstractSource::__construct`: expected `PhpDb\Adapter\AdapterInterface&PhpDb\Adapter\SchemaAwareInterface`, but found `mixed`.' +count = 1 + +[[issues]] +file = "src/Container/MetadataInterfaceFactory.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #1 of `Psr\Container\ContainerInterface::get`: expected `string`, but found `nonnull`.' +count = 1 + +[[issues]] +file = "src/Container/MetadataInterfaceFactory.php" +code = "mixed-assignment" +message = "Assigning `nonnull` type to a variable may lead to unexpected behavior." +count = 1 + +[[issues]] +file = "src/Container/PdoConnectionInterfaceFactory.php" +code = "less-specific-argument" +message = 'Argument type mismatch for argument #1 of `PhpDb\Mysql\Pdo\Connection::__construct`: expected `PDO|array`, but provided type `non-empty-array` is less specific.' +count = 1 + +[[issues]] +file = "src/Container/PdoConnectionInterfaceFactory.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #2 of `PhpDb\Adapter\Exception\InvalidConnectionParametersException::__construct`: expected `array`, but found `nonnull`.' +count = 1 + +[[issues]] +file = "src/Container/PdoConnectionInterfaceFactory.php" +code = "mixed-assignment" +message = "Assigning `nonnull` type to a variable may lead to unexpected behavior." +count = 1 + +[[issues]] +file = "src/Container/PdoDriverInterfaceFactory.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #2 of `Laminas\ServiceManager\ServiceManager::build`: expected `array|null`, but found `nonnull`.' +count = 1 + +[[issues]] +file = "src/Container/PdoDriverInterfaceFactory.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #4 of `PhpDb\Mysql\Pdo\Driver::__construct`: expected `array`, but found `nonnull`.' +count = 1 + +[[issues]] +file = "src/Container/PdoStatementFactory.php" +code = "possibly-null-argument" +message = 'Argument #1 of method `PhpDb\Adapter\Driver\Pdo\Statement::__construct` is possibly `null`, but parameter type `array` does not accept it.' +count = 1 + +[[issues]] +file = "src/Container/PlatformInterfaceFactory.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." +count = 1 + +[[issues]] +file = "src/Container/StatementInterfaceFactory.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #1 of `PhpDb\Mysql\Statement::__construct`: expected `bool`, but found `nonnull`.' +count = 1 + +[[issues]] +file = "src/Driver.php" +code = "docblock-type-mismatch" +message = "Docblock type mismatch for variable `$resource`." +count = 1 + +[[issues]] +file = "src/Driver.php" +code = "incompatible-parameter-type" +message = 'Parameter `$resource` of `PhpDb\Mysql\Driver::createresult()` expects type `mysqli|mysqli_stmt|unknown-ref(PhpDb\Mysql\mysqli_result)` but parent `PhpDb\Adapter\Driver\DriverInterface::createresult()` expects type `resource`' +count = 1 + +[[issues]] +file = "src/Driver.php" +code = "incompatible-parameter-type" +message = 'Parameter `$sqlOrResource` of `PhpDb\Mysql\Driver::createstatement()` expects type `mysqli|mysqli_stmt|string` but parent `PhpDb\Adapter\Driver\DriverInterface::createstatement()` expects type `resource|string`' +count = 1 + +[[issues]] +file = "src/Driver.php" +code = "missing-property-type" +message = "Property `$options` is missing a type hint." +count = 1 + +[[issues]] +file = "src/Driver.php" +code = "non-existent-class-like" +message = 'Cannot find class, interface, enum, or type alias `PhpDb\Mysql\mysqli_result`.' +count = 1 + +[[issues]] +file = "src/Driver.php" +code = "possibly-invalid-argument" +message = 'Possible argument type mismatch for argument #1 of `PhpDb\Mysql\Result::initialize`: expected `mysqli|mysqli_result|mysqli_stmt`, but possibly received `mysqli|mysqli_stmt|unknown-ref(PhpDb\Mysql\mysqli_result)`.' +count = 1 + +[[issues]] +file = "src/Metadata/Source.php" +code = "mixed-array-assignment" +message = "Unsafe array assignment on type `mixed`." +count = 9 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-invalid-argument" +message = "Possible argument type mismatch for argument #2 of `implode`: expected `array|null`, but possibly received `non-empty-list`." +count = 7 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-null-argument" +message = "Argument #2 of function `preg_match_all` is possibly `null`, but parameter type `string` does not accept it." +count = 1 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-null-argument" +message = "Argument #3 of function `str_replace` is possibly `null`, but parameter type `array|string` does not accept it." +count = 1 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-null-operand" +message = "Possibly null middle operand used in string concatenation (type `null|string`)." +count = 1 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-null-operand" +message = "Possibly null right operand used in string concatenation (type `null|string`)." +count = 1 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array index accessed on `list>`." +count = 1 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `int(1)` accessed on `array`." +count = 1 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-undefined-variable" +message = "Variable `$isFK` might not have been defined on all execution paths leading to this point." +count = 1 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-undefined-variable" +message = "Variable `$name` might not have been defined on all execution paths leading to this point." +count = 2 + +[[issues]] +file = "src/Metadata/Source.php" +code = "reference-constraint-violation" +message = "Invalid assignment to by-reference parameter `$c`." +count = 7 + +[[issues]] +file = "src/Metadata/Source.php" +code = "too-many-arguments" +message = 'Too many arguments provided for method `PhpDb\Metadata\Source\AbstractSource::prepareDataHierarchy`.' +count = 6 + +[[issues]] +file = "src/Metadata/Source.php" +code = "unused-method" +message = "Method `loadconstraintdatanames()` is never used." +count = 1 + +[[issues]] +file = "src/Pdo/Connection.php" +code = "invalid-type-cast" +message = "Casting `mixed` to `array`." +count = 1 + +[[issues]] +file = "src/Pdo/Connection.php" +code = "less-specific-argument" +message = "Argument type mismatch for argument #1 of `strtolower`: expected `string`, but provided type `array-key` is less specific." +count = 1 + +[[issues]] +file = "src/Pdo/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #1 of `array_diff_key`: expected `array<('K.array_diff_key() extends array-key), ('V.array_diff_key() extends mixed)>`, but found `mixed`." +count = 1 + +[[issues]] +file = "src/Pdo/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #4 of `PDO::__construct`: expected `array|null`, but found `mixed`." +count = 1 + +[[issues]] +file = "src/Pdo/Connection.php" +code = "mixed-array-assignment" +message = "Unsafe array assignment on type `mixed`." +count = 1 + +[[issues]] +file = "src/Pdo/Connection.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." +count = 3 + +[[issues]] +file = "src/Pdo/Driver.php" +code = "incompatible-parameter-type" +message = 'Parameter `$resource` of `PhpDb\Mysql\Pdo\Driver::createresult()` expects type `PDOStatement` but parent `PhpDb\Adapter\Driver\DriverInterface::createresult()` expects type `resource`' +count = 1 + +[[issues]] +file = "src/Pdo/Driver.php" +code = "invalid-property-assignment-value" +message = 'Invalid type for property `$connection`: expected `(PhpDb\Adapter\Driver\PdoConnectionInterface&PhpDb\Adapter\Driver\AbstractConnection&PhpDb\Adapter\Driver\PdoDriverAwareInterface)|PDO`, but got `(PhpDb\Adapter\Driver\PdoConnectionInterface&PhpDb\Adapter\Driver\PdoDriverAwareInterface)|PDO`.' +count = 1 + +[[issues]] +file = "src/Result.php" +code = "missing-constructor" +message = 'Class `PhpDb\Mysql\Result` has typed properties without default values but no constructor to initialize them.' +count = 1 + +[[issues]] +file = "src/Result.php" +code = "possibly-null-array-index" +message = "Possibly using `null` as an array index to access element." +count = 1 + +[[issues]] +file = "src/Result.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array index accessed on `list`." +count = 1 + +[[issues]] +file = "src/Result.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array index accessed on `list`." +count = 1 + +[[issues]] +file = "src/Result.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `non-negative-int` accessed on `array`." +count = 1 + +[[issues]] +file = "src/Result.php" +code = "unused-property" +message = "Property `$numberOfRows` is never used." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "invalid-return-statement" +message = 'Invalid return type for function `PhpDb\Mysql\Sql\Ddl\AlterTableDecorator::processChangeColumns`: expected `array>`, but found `list{array{}|non-empty-list}`.' +count = 1 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "less-specific-argument" +message = 'Argument type mismatch for argument #1 of `PhpDb\Adapter\Platform\PlatformInterface::quoteIdentifier`: expected `string`, but provided type `array-key` is less specific.' +count = 1 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #1 of `PhpDb\Adapter\Platform\PlatformInterface::quoteIdentifier`: expected `string`, but found `truthy-mixed`.' +count = 1 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #1 of `PhpDb\Adapter\Platform\PlatformInterface::quoteValue`: expected `string`, but found `truthy-mixed`.' +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #1 of `PhpDb\Sql\AbstractSql::processExpression`: expected `PhpDb\Sql\ExpressionInterface`, but found `mixed`.' +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "mixed-argument" +message = "Invalid argument type for argument #1 of `strtoupper`: expected `string`, but found `truthy-mixed`." +count = 4 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "mixed-argument" +message = "Invalid argument type for argument #1 of `uksort`: expected `array`, but found `mixed`." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." +count = 6 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "mixed-method-access" +message = "Attempting to access a method on a non-object type (`mixed`)." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "possibly-null-argument" +message = "Argument #3 of function `substr_replace` is possibly `null`, but parameter type `array|int` does not accept it." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "possibly-null-operand" +message = "Left operand in arithmetic operation might be `null` (type `int|null`)." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `int(0)|int(1)|int(2)` accessed on `array{0: int, 1: int, 2: int, 3: int, ...}`." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `int(0)|int(1)|int(2)` accessed on `array{0: int, 1: int, 2: int, 3: int}`." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `non-negative-int` accessed on `array{0: int, 1: int, 2: int, 3: int, ...}`." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `non-negative-int` accessed on `array{0: int, 1: int, 2: int, 3: int}`." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #1 of `PhpDb\Adapter\Platform\PlatformInterface::quoteValue`: expected `string`, but found `truthy-mixed`.' +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #1 of `PhpDb\Sql\AbstractSql::processExpression`: expected `PhpDb\Sql\ExpressionInterface`, but found `mixed`.' +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "mixed-argument" +message = "Invalid argument type for argument #1 of `strtoupper`: expected `string`, but found `truthy-mixed`." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "mixed-argument" +message = "Invalid argument type for argument #1 of `uksort`: expected `array`, but found `mixed`." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." +count = 3 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "mixed-method-access" +message = "Attempting to access a method on a non-object type (`mixed`)." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "possibly-null-argument" +message = "Argument #3 of function `substr_replace` is possibly `null`, but parameter type `array|int` does not accept it." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "possibly-null-operand" +message = "Left operand in arithmetic operation might be `null` (type `int|null`)." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `int(0)|int(1)|int(2)` accessed on `array{0: int, 1: int, 2: int, 3: int, ...}`." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `int(0)|int(1)|int(2)` accessed on `array{0: int, 1: int, 2: int, 3: int}`." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `non-negative-int` accessed on `array{0: int, 1: int, 2: int, 3: int, ...}`." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `non-negative-int` accessed on `array{0: int, 1: int, 2: int, 3: int}`." +count = 1 + +[[issues]] +file = "src/Sql/SelectDecorator.php" +code = "invalid-property-assignment-value" +message = "Invalid type for property `$specifications`: expected `array>|array`, but got `array{'limit': string('LIMIT 18446744073709551615'), ...|string>}`." +count = 1 + +[[issues]] +file = "src/Sql/SelectDecorator.php" +code = "invalid-return-statement" +message = 'Invalid return type for function `PhpDb\Mysql\Sql\SelectDecorator::processLimit`: expected `array|null`, but found `list{int|string}`.' +count = 1 + +[[issues]] +file = "src/Sql/SelectDecorator.php" +code = "invalid-return-statement" +message = 'Invalid return type for function `PhpDb\Mysql\Sql\SelectDecorator::processOffset`: expected `array|null`, but found `list{int|string}`.' +count = 1 + +[[issues]] +file = "src/Sql/SelectDecorator.php" +code = "less-specific-nested-return-statement" +message = '''Returned type `list{mixed}` is less specific than the declared return type `array|null` for function `PhpDb\Mysql\Sql\SelectDecorator::processLimit` due to nested 'mixed'.''' +count = 1 + +[[issues]] +file = "src/Sql/SelectDecorator.php" +code = "less-specific-nested-return-statement" +message = '''Returned type `list{mixed}` is less specific than the declared return type `array|null` for function `PhpDb\Mysql\Sql\SelectDecorator::processOffset` due to nested 'mixed'.''' +count = 1 + +[[issues]] +file = "src/Sql/SelectDecorator.php" +code = "possible-method-access-on-null" +message = "Attempting to call a method on `null`." +count = 2 + +[[issues]] +file = "src/Sql/SelectDecorator.php" +code = "write-only-property" +message = "Property `$subject` is written to but never read." +count = 1 + +[[issues]] +file = "src/Statement.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." +count = 1 + +[[issues]] +file = "src/Statement.php" +code = "uninitialized-property" +message = 'Property `$driver` is not initialized in the constructor of class `PhpDb\Mysql\Statement`.' +count = 1 + +[[issues]] +file = "src/Statement.php" +code = "uninitialized-property" +message = 'Property `$mysqli` is not initialized in the constructor of class `PhpDb\Mysql\Statement`.' +count = 1 + +[[issues]] +file = "src/Statement.php" +code = "uninitialized-property" +message = 'Property `$resource` is not initialized in the constructor of class `PhpDb\Mysql\Statement`.' +count = 1 diff --git a/mago.toml b/mago.toml index 4e2bc2b..7cf3255 100644 --- a/mago.toml +++ b/mago.toml @@ -1,7 +1,10 @@ -#:schema https://mago.carthage.software/1.45.0/schema.json +#:schema https://mago.carthage.software/1.46.0/schema.json extends = "vendor/php-db/phpdb-qa-tools/mago.toml" php-version = "8.3.0" [source] paths = ["src", "test"] includes = ["vendor"] + +[analyzer] +baseline = "analysis-baseline.toml" diff --git a/src/AdapterPlatform.php b/src/AdapterPlatform.php index aa57656..9db9b86 100644 --- a/src/AdapterPlatform.php +++ b/src/AdapterPlatform.php @@ -14,7 +14,7 @@ use function implode; use function str_replace; -class AdapterPlatform extends AbstractPlatform +final class AdapterPlatform extends AbstractPlatform { final public const PLATFORM_NAME = 'MySQL'; diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 9ba7e69..7f73013 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -12,6 +12,12 @@ final class ConfigProvider { + /** + * @return array{ + * aliases: array, + * factories: array, + * } + */ public function getDependencies(): array { return [ @@ -44,6 +50,14 @@ public function getDependencies(): array ]; } + /** + * @return array{ + * dependencies: array{ + * aliases: array, + * factories: array, + * }, + * } + */ public function __invoke(): array { return [ diff --git a/src/Connection.php b/src/Connection.php index 08752fd..4ab3800 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -6,6 +6,7 @@ use Exception as GenericException; use mysqli; +use mysqli_result; use Override; use PhpDb\Adapter\Driver\AbstractConnection; use PhpDb\Adapter\Driver\ConnectionInterface; @@ -27,9 +28,12 @@ // @mago-expect lint:cyclomatic-complexity // @mago-expect lint:kan-defect // @mago-expect lint:too-many-methods +// @mago-expect analysis:class-must-be-final class Connection extends AbstractConnection implements DriverAwareInterface { - protected Driver $driver; + protected ?DriverInterface $driver = null; + + protected ?string $driverName = null; /** @var mysqli */ protected $resource; @@ -37,6 +41,8 @@ class Connection extends AbstractConnection implements DriverAwareInterface /** * Constructor * + * @param array|mysqli|null $connectionInfo + * * @throws InvalidArgumentException */ public function __construct( @@ -53,15 +59,13 @@ public function __construct( return; } - - if (null !== $connectionInfo) { - throw new Exception\InvalidArgumentException( - '$connection must be an array of parameters, a mysqli object or null', - ); - } } - /** @inheritDoc */ + /** + * @inheritDoc + * + * @throws Exception\ExceptionInterface + */ #[Override] public function beginTransaction(): ConnectionInterface { @@ -75,7 +79,11 @@ public function beginTransaction(): ConnectionInterface return $this; } - /** @inheritDoc */ + /** + * @inheritDoc + * + * @throws Exception\ExceptionInterface + */ #[Override] public function commit(): ConnectionInterface { @@ -90,7 +98,11 @@ public function commit(): ConnectionInterface return $this; } - /** @inheritDoc */ + /** + * @inheritDoc + * + * @throws Exception\ExceptionInterface + */ // @mago-expect lint:halstead #[Override] public function connect(): ConnectionInterface @@ -99,7 +111,6 @@ public function connect(): ConnectionInterface return $this; } - /** @var array $p */ $p = $this->connectionParameters; // given a list of key names, test for existence in $p @@ -119,8 +130,7 @@ public function connect(): ConnectionInterface $username = $findParameterValue(['username', 'user']); $password = $findParameterValue(['password', 'passwd', 'pw']); $database = $findParameterValue(['database', 'dbname', 'db', 'schema']); - /** @var int|null $port */ - $port = null === ($p['port'] ?? null) ? null : (int) $p['port']; + $port = null === ($p['port'] ?? null) ? null : (int) $p['port']; /** @var string|null $socket */ $socket = $p['socket'] ?? null; @@ -205,10 +215,10 @@ public function disconnect(): ConnectionInterface /** * {@inheritDoc} * - * @throws Exception\InvalidQueryException + * @throws Exception\ExceptionInterface */ #[Override] - public function execute($sql): ?ResultInterface + public function execute(string $sql): ?ResultInterface { if (! $this->isConnected()) { $this->connect(); @@ -218,17 +228,29 @@ public function execute($sql): ?ResultInterface $resultResource = $this->resource->query($sql); - $this->profiler?->profilerFinish($sql); + $this->profiler?->profilerFinish(); // if the returnValue is something other than a mysqli_result, bypass wrapping it if (false === $resultResource) { throw new Exception\InvalidQueryException($this->resource->error); } + if (null === $this->driver) { + throw new Exception\RuntimeException('Cannot execute without a driver; call setDriver() first.'); + } + + // @mago-expect analysis:invalid-argument - DriverInterface::createResult() is documented with a + // generic `resource` type to stay valid across every RDBMS platform (see php-db/phpdb#170 for a + // proposed @template-based fix); this class always passes real mysqli|mysqli_result objects, which + // is correct for this concrete implementation. return $this->driver->createResult(true === $resultResource ? $this->resource : $resultResource); } - /** @inheritDoc */ + /** + * @inheritDoc + * + * @throws Exception\ExceptionInterface + */ #[Override] public function getCurrentSchema(): string|false { @@ -237,7 +259,19 @@ public function getCurrentSchema(): string|false } $result = $this->resource->query('SELECT DATABASE()'); - $r = $result->fetch_row(); + if (! $result instanceof mysqli_result) { + throw new Exception\RuntimeException('Failed to query current schema'); + } + + $r = $result->fetch_row(); + if (false === $r) { + throw new Exception\RuntimeException($this->resource->error); + } + + /** @var array{0: string|null}|null $r */ + if (null === $r || null === $r[0]) { + return false; + } return $r[0]; } @@ -250,12 +284,17 @@ public function getLastGeneratedValue(?string $name = null): string|int|false|nu } /** @inheritDoc */ + #[Override] public function isConnected(): bool { return $this->resource instanceof mysqli; } - /** @inheritDoc */ + /** + * @inheritDoc + * + * @throws Exception\ExceptionInterface + */ #[Override] public function rollback(): ConnectionInterface { @@ -274,6 +313,7 @@ public function rollback(): ConnectionInterface return $this; } + #[Override] public function setDriver(DriverInterface $driver): DriverAwareInterface { $this->driver = $driver; @@ -300,7 +340,7 @@ public function setResource(mysqli $resource): static * * @return mysqli */ - protected function createResource() + protected function createResource(): mysqli { return new mysqli(); } diff --git a/src/Container/ConnectionInterfaceFactory.php b/src/Container/ConnectionInterfaceFactory.php index d8ff17d..9d18ed9 100644 --- a/src/Container/ConnectionInterfaceFactory.php +++ b/src/Container/ConnectionInterfaceFactory.php @@ -13,6 +13,13 @@ final class ConnectionInterfaceFactory { + /** + * @param array|null $options + * + * @throws \PhpDb\Adapter\Exception\ExceptionInterface + * + * @mago-expect analysis:unused-parameter + */ public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Container/DriverInterfaceFactory.php b/src/Container/DriverInterfaceFactory.php index 2fb1d75..9cfb5b7 100644 --- a/src/Container/DriverInterfaceFactory.php +++ b/src/Container/DriverInterfaceFactory.php @@ -5,10 +5,8 @@ namespace PhpDb\Mysql\Container; use Laminas\ServiceManager\ServiceManager; -use PhpDb\Adapter\Driver\ConnectionInterface; use PhpDb\Adapter\Driver\DriverInterface; use PhpDb\Adapter\Driver\ResultInterface; -use PhpDb\Adapter\Driver\StatementInterface; use PhpDb\Exception\ContainerException; use PhpDb\Mysql\Connection; use PhpDb\Mysql\Driver; @@ -20,6 +18,16 @@ final class DriverInterfaceFactory { + /** + * @param array|null $options + * + * @throws \Laminas\ServiceManager\Exception\ExceptionInterface + * @throws \Psr\Container\ContainerExceptionInterface + * @throws \Psr\Container\NotFoundExceptionInterface + * @throws \PhpDb\Exception\ExceptionInterface + * + * @mago-expect analysis:unused-parameter + */ public function __invoke( ContainerInterface&ServiceManager $container, string $requestedName, @@ -33,10 +41,8 @@ public function __invoke( ); } - /** @var ConnectionInterface&Connection $connectionInstance */ $connectionInstance = $container->build(Connection::class, $options); - /** @var StatementInterface&Statement $statementInstance */ $statementInstance = $container->build( Statement::class, $options['options'] ?? [], diff --git a/src/Container/MetadataInterfaceFactory.php b/src/Container/MetadataInterfaceFactory.php index 7548758..b1b8f3f 100644 --- a/src/Container/MetadataInterfaceFactory.php +++ b/src/Container/MetadataInterfaceFactory.php @@ -13,6 +13,14 @@ final class MetadataInterfaceFactory { public const ADAPTER_SERVICE_NAME = 'adapter_service_name'; + /** + * @param array|null $options + * + * @throws \Psr\Container\ContainerExceptionInterface + * @throws \Psr\Container\NotFoundExceptionInterface + * + * @mago-expect analysis:unused-parameter + */ public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Container/PdoConnectionInterfaceFactory.php b/src/Container/PdoConnectionInterfaceFactory.php index b7c166f..8b3abd7 100644 --- a/src/Container/PdoConnectionInterfaceFactory.php +++ b/src/Container/PdoConnectionInterfaceFactory.php @@ -13,6 +13,13 @@ final class PdoConnectionInterfaceFactory { + /** + * @param array|null $options + * + * @throws \PhpDb\Adapter\Exception\ExceptionInterface + * + * @mago-expect analysis:unused-parameter + */ public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Container/PdoDriverInterfaceFactory.php b/src/Container/PdoDriverInterfaceFactory.php index 780d904..ee797f7 100644 --- a/src/Container/PdoDriverInterfaceFactory.php +++ b/src/Container/PdoDriverInterfaceFactory.php @@ -7,10 +7,8 @@ use Laminas\ServiceManager\ServiceManager; use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\Pdo\Statement; -use PhpDb\Adapter\Driver\PdoConnectionInterface; use PhpDb\Adapter\Driver\PdoDriverInterface; use PhpDb\Adapter\Driver\ResultInterface; -use PhpDb\Adapter\Driver\StatementInterface; use PhpDb\Exception\ContainerException; use PhpDb\Mysql\Pdo\Connection; use PhpDb\Mysql\Pdo\Driver; @@ -20,6 +18,15 @@ final class PdoDriverInterfaceFactory { + /** + * @param array|null $options + * + * @throws \Laminas\ServiceManager\Exception\ExceptionInterface + * @throws \Psr\Container\ContainerExceptionInterface + * @throws \Psr\Container\NotFoundExceptionInterface + * + * @mago-expect analysis:unused-parameter + */ public function __invoke( ContainerInterface&ServiceManager $container, string $requestedName, @@ -32,10 +39,8 @@ public function __invoke( '$options["connection"] must contain an array of connection configuration.', ); } - /** @var PdoConnectionInterface&Connection $connectionInstance */ $connectionInstance = $container->build(Connection::class, $options); - /** @var StatementInterface&Statement $statementInstance */ $statementInstance = $container->build( Statement::class, $options['options'] ?? [], diff --git a/src/Container/PdoStatementFactory.php b/src/Container/PdoStatementFactory.php index 0a0c0c2..76fdcab 100644 --- a/src/Container/PdoStatementFactory.php +++ b/src/Container/PdoStatementFactory.php @@ -10,6 +10,11 @@ final class PdoStatementFactory { + /** + * @param array|null $options + * + * @mago-expect analysis:unused-parameter + */ public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Container/PlatformInterfaceFactory.php b/src/Container/PlatformInterfaceFactory.php index cbb318d..a6c6165 100644 --- a/src/Container/PlatformInterfaceFactory.php +++ b/src/Container/PlatformInterfaceFactory.php @@ -13,6 +13,13 @@ final class PlatformInterfaceFactory { + /** + * @param array|null $options + * + * @throws \Psr\Container\ContainerExceptionInterface + * + * @mago-expect analysis:unused-parameter + */ public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Container/StatementInterfaceFactory.php b/src/Container/StatementInterfaceFactory.php index 9eb78a1..479149c 100644 --- a/src/Container/StatementInterfaceFactory.php +++ b/src/Container/StatementInterfaceFactory.php @@ -10,6 +10,11 @@ final class StatementInterfaceFactory { + /** + * @param array|null $options + * + * @mago-expect analysis:unused-parameter + */ public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Driver.php b/src/Driver.php index 8700610..cbe8117 100644 --- a/src/Driver.php +++ b/src/Driver.php @@ -6,8 +6,8 @@ use mysqli; use mysqli_stmt; +use Override; use PhpDb\Adapter\Driver\ConnectionInterface; -use PhpDb\Adapter\Driver\DriverAwareInterface; use PhpDb\Adapter\Driver\DriverInterface; use PhpDb\Adapter\Driver\ResultInterface; use PhpDb\Adapter\Driver\StatementInterface; @@ -28,6 +28,11 @@ final class Driver implements DriverInterface, ProfilerAwareInterface 'buffer_results' => false, ]; + /** + * @param array $options + * + * @throws \PhpDb\Exception\ExceptionInterface + */ public function __construct( protected readonly ConnectionInterface&Connection $connection, protected readonly StatementInterface&Statement $statementPrototype = new Statement(), @@ -38,15 +43,11 @@ public function __construct( $options = array_intersect_key([...$this->options, ...$options], $this->options); - if ($this->connection instanceof DriverAwareInterface) { - $this->connection->setDriver($this); - } - - if ($this->statementPrototype instanceof DriverAwareInterface) { - $this->statementPrototype->setDriver($this); - } + $this->connection->setDriver($this); + $this->statementPrototype->setDriver($this); } + #[Override] public function checkEnvironment(): bool { if (! extension_loaded('mysqli')) { @@ -62,9 +63,9 @@ public function checkEnvironment(): bool * * @param mysqli|mysqli_result|mysqli_stmt $resource */ + #[Override] public function createResult($resource, ?bool $isBuffered = null): ResultInterface&Result { - /** @var Result $result */ $result = clone $this->resultPrototype; $result->initialize($resource, $this->connection->getLastGeneratedValue(), $isBuffered); return $result; @@ -74,7 +75,10 @@ public function createResult($resource, ?bool $isBuffered = null): ResultInterfa * Create statement * * @param mysqli|mysqli_stmt|string $sqlOrResource + * + * @throws Exception\ExceptionInterface */ + #[Override] public function createStatement($sqlOrResource = null): StatementInterface&Statement { /** @@ -106,11 +110,13 @@ public function createStatement($sqlOrResource = null): StatementInterface&State /** * Format parameter name */ + #[Override] public function formatParameterName(string $name, ?string $type = null): string { return '?'; } + #[Override] public function getConnection(): ConnectionInterface&Connection { return $this->connection; @@ -119,6 +125,7 @@ public function getConnection(): ConnectionInterface&Connection /** * Get last generated value */ + #[Override] public function getLastGeneratedValue(): int|string|false|null { return $this->getConnection()->getLastGeneratedValue(); @@ -127,6 +134,7 @@ public function getLastGeneratedValue(): int|string|false|null /** * Get prepare type */ + #[Override] public function getPrepareType(): string { return self::PARAMETERIZATION_POSITIONAL; @@ -150,15 +158,12 @@ public function getStatementPrototype(): StatementInterface&Statement return $this->statementPrototype; } + #[Override] public function setProfiler(ProfilerInterface $profiler): ProfilerAwareInterface { $this->profiler = $profiler; - if ($this->connection instanceof ProfilerAwareInterface) { - $this->connection->setProfiler($profiler); - } - if ($this->statementPrototype instanceof ProfilerAwareInterface) { - $this->statementPrototype->setProfiler($profiler); - } + $this->connection->setProfiler($profiler); + $this->statementPrototype->setProfiler($profiler); return $this; } } diff --git a/src/Metadata/Source.php b/src/Metadata/Source.php index f3c1460..5370519 100644 --- a/src/Metadata/Source.php +++ b/src/Metadata/Source.php @@ -6,8 +6,10 @@ use DateTime; use Exception; +use Override; use PhpDb\Adapter\AdapterInterface; use PhpDb\Metadata\Source\AbstractSource; +use PhpDb\ResultSet\ResultSetInterface; use function array_change_key_case; use function array_walk; @@ -25,6 +27,7 @@ final class Source extends AbstractSource { // @mago-expect lint:halstead + #[Override] protected function loadColumnData(string $table, string $schema): void { if (null !== ($this->data['columns'][$schema][$table] ?? null)) { @@ -79,8 +82,10 @@ protected function loadColumnData(string $table, string $schema): void ? " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} != 'INFORMATION_SCHEMA'" : " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} = {$p->quoteTrustedValue($schema)}"; + /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $columns = []; + /** @var array{ORDINAL_POSITION: string, COLUMN_DEFAULT: ?string, IS_NULLABLE: string, DATA_TYPE: string, CHARACTER_MAXIMUM_LENGTH: ?string, CHARACTER_OCTET_LENGTH: ?string, NUMERIC_PRECISION: ?string, NUMERIC_SCALE: ?string, COLUMN_NAME: string, COLUMN_TYPE: string} $row */ foreach ($results->toArray() as $row) { $erratas = []; $matches = []; @@ -114,6 +119,7 @@ protected function loadColumnData(string $table, string $schema): void } // @mago-expect lint:halstead + #[Override] protected function loadConstraintData(string $table, string $schema): void { // phpcs:disable WebimpressCodingStandard.NamingConventions.ValidVariableName.NotCamelCaps @@ -205,10 +211,12 @@ protected function loadConstraintData(string $table, string $schema): void 'CONSTRAINT_NAME', ])}, {$p->quoteIdentifierChain(['KCU', 'ORDINAL_POSITION'])}"; + /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $realName = null; $constraints = []; + /** @var array{TABLE_NAME: string, CONSTRAINT_NAME: string, CONSTRAINT_TYPE: string, COLUMN_NAME: ?string, MATCH_OPTION: ?string, UPDATE_RULE: ?string, DELETE_RULE: ?string, REFERENCED_TABLE_SCHEMA: ?string, REFERENCED_TABLE_NAME: ?string, REFERENCED_COLUMN_NAME: ?string} $row */ foreach ($results->toArray() as $row) { if ($row['CONSTRAINT_NAME'] !== $realName) { $realName = $row['CONSTRAINT_NAME']; @@ -240,6 +248,7 @@ protected function loadConstraintData(string $table, string $schema): void // phpcs:enable WebimpressCodingStandard.NamingConventions.ValidVariableName.NotCamelCaps } + #[Override] protected function loadConstraintDataKeys(string $schema): void { if (null !== ($this->data['constraint_keys'][$schema] ?? null)) { @@ -286,9 +295,11 @@ protected function loadConstraintDataKeys(string $schema): void ? " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} != 'INFORMATION_SCHEMA'" : " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} = {$p->quoteTrustedValue($schema)}"; + /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $data = []; + /** @var array $row */ foreach ($results->toArray() as $row) { $data[] = array_change_key_case($row, CASE_LOWER); } @@ -341,9 +352,11 @@ protected function loadConstraintDataNames(string $schema): void ? " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} != 'INFORMATION_SCHEMA'" : " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} = {$p->quoteTrustedValue($schema)}"; + /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $data = []; + /** @var array $row */ foreach ($results->toArray() as $row) { $data[] = array_change_key_case($row, CASE_LOWER); } @@ -351,6 +364,7 @@ protected function loadConstraintDataNames(string $schema): void $this->data['constraint_names'][$schema] = $data; } + #[Override] protected function loadConstraintReferences(string $table, string $schema): void { parent::loadConstraintReferences($table, $schema); @@ -411,9 +425,11 @@ protected function loadConstraintReferences(string $table, string $schema): void ? " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} != 'INFORMATION_SCHEMA'" : " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} = {$p->quoteTrustedValue($schema)}"; + /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $data = []; + /** @var array $row */ foreach ($results->toArray() as $row) { $data[] = array_change_key_case($row, CASE_LOWER); } @@ -424,6 +440,7 @@ protected function loadConstraintReferences(string $table, string $schema): void /** * @throws Exception */ + #[Override] protected function loadSchemaData(): void { if (null !== ($this->data['schemas'] ?? null)) { @@ -439,9 +456,11 @@ protected function loadSchemaData(): void WHERE {$p->quoteIdentifier('SCHEMA_NAME')} != 'INFORMATION_SCHEMA' SQL; + /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $schemas = []; + /** @var array{SCHEMA_NAME: string} $row */ foreach ($results->toArray() as $row) { $schemas[] = $row['SCHEMA_NAME']; } @@ -449,6 +468,7 @@ protected function loadSchemaData(): void $this->data['schemas'] = $schemas; } + #[Override] protected function loadTableNameData(string $schema): void { if (null !== ($this->data['table_names'][$schema] ?? null)) { @@ -495,9 +515,11 @@ protected function loadTableNameData(string $schema): void ? " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} != 'INFORMATION_SCHEMA'" : " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} = {$p->quoteTrustedValue($schema)}"; + /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $tables = []; + /** @var array{TABLE_NAME: string, TABLE_TYPE: string, VIEW_DEFINITION: ?string, CHECK_OPTION: ?string, IS_UPDATABLE: ?string} $row */ foreach ($results->toArray() as $row) { $tables[$row['TABLE_NAME']] = [ 'table_type' => $row['TABLE_TYPE'], @@ -510,6 +532,7 @@ protected function loadTableNameData(string $schema): void $this->data['table_names'][$schema] = $tables; } + #[Override] protected function loadTriggerData(string $schema): void { if (null !== ($this->data['triggers'][$schema] ?? null)) { @@ -555,10 +578,13 @@ protected function loadTriggerData(string $schema): void ? "{$p->quoteIdentifier('TRIGGER_SCHEMA')} != 'INFORMATION_SCHEMA'" : "{$p->quoteIdentifier('TRIGGER_SCHEMA')} = {$p->quoteTrustedValue($schema)}"; + /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $data = []; + /** @var array{TRIGGER_NAME: string, EVENT_MANIPULATION: string, EVENT_OBJECT_CATALOG: string, EVENT_OBJECT_SCHEMA: string, EVENT_OBJECT_TABLE: string, ACTION_ORDER: string, ACTION_CONDITION: ?string, ACTION_STATEMENT: string, ACTION_ORIENTATION: string, ACTION_TIMING: string, ACTION_REFERENCE_OLD_TABLE: ?string, ACTION_REFERENCE_NEW_TABLE: ?string, ACTION_REFERENCE_OLD_ROW: ?string, ACTION_REFERENCE_NEW_ROW: ?string, CREATED: ?string} $row */ foreach ($results->toArray() as $row) { + /** @var array{trigger_name: string, event_manipulation: string, event_object_catalog: string, event_object_schema: string, event_object_table: string, action_order: string, action_condition: ?string, action_statement: string, action_orientation: string, action_timing: string, action_reference_old_table: ?string, action_reference_new_table: ?string, action_reference_old_row: ?string, action_reference_new_row: ?string, created: ?string} $row */ $row = array_change_key_case($row, CASE_LOWER); if (null !== $row['created']) { $row['created'] = new DateTime($row['created']); diff --git a/src/Pdo/Connection.php b/src/Pdo/Connection.php index bc78a3f..49fe7dc 100644 --- a/src/Pdo/Connection.php +++ b/src/Pdo/Connection.php @@ -20,11 +20,20 @@ use function strtolower; // @mago-expect lint:cyclomatic-complexity -class Connection extends AbstractPdoConnection +// @mago-expect lint:kan-defect +final class Connection extends AbstractPdoConnection { + // @mago-expect analysis:write-only-property - read by the parent's final AbstractPdoConnection::getDsn() + protected ?string $dsn = null; + + // @mago-expect analysis:write-only-property - read by AbstractConnection::getDriverName() + protected ?string $driverName = null; + /** * Constructor * + * @param array|PDO $connectionParameters + * * @throws Exception\InvalidArgumentException */ public function __construct( @@ -115,19 +124,12 @@ public function connect(): ConnectionInterface $dsn = 'mysql:' . implode(';', $dsn); } - if (! is_string($dsn)) { - throw new Exception\InvalidConnectionParametersException( - 'A dsn was not provided or could not be constructed from your parameters', - $this->connectionParameters, - ); - } - $this->dsn = $dsn; try { $this->resource = new PDO($dsn, $username, $password, $options); $this->resource->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $this->driverName = strtolower($this->resource->getAttribute(PDO::ATTR_DRIVER_NAME)); + $this->driverName = strtolower((string) $this->resource->getAttribute(PDO::ATTR_DRIVER_NAME)); } catch (PDOException $e) { $code = $e->getCode(); if (! is_int($code)) { @@ -141,6 +143,9 @@ public function connect(): ConnectionInterface /** * {@inheritDoc} + * + * @throws Exception\ExceptionInterface + * @throws PDOException */ #[Override] public function getCurrentSchema(): string|false @@ -149,18 +154,29 @@ public function getCurrentSchema(): string|false $this->connect(); } - /** @var PDOStatement $result */ + if (null === $this->resource) { + throw new Exception\RuntimeException( + 'Cannot query current schema without a connected resource; call connect() first.', + ); + } + $result = $this->resource->query('SELECT DATABASE()'); - if ($result instanceof PDOStatement) { - return $result->fetchColumn(); + if (! $result instanceof PDOStatement) { + return false; } - return false; + /** @var string|false|null $value */ + $value = $result->fetchColumn(); + return is_string($value) ? $value : false; } #[Override] public function getLastGeneratedValue(?string $name = null): string|int|false|null { + if (null === $this->resource) { + return false; + } + try { return $this->resource->lastInsertId($name); } catch (PDOException) { diff --git a/src/Pdo/Driver.php b/src/Pdo/Driver.php index 0341b17..fa412d1 100644 --- a/src/Pdo/Driver.php +++ b/src/Pdo/Driver.php @@ -15,9 +15,15 @@ use PhpDb\Adapter\Driver\PdoDriverAwareInterface; use PhpDb\Adapter\Driver\ResultInterface; use PhpDb\Adapter\Driver\StatementInterface; +use PhpDb\Adapter\Profiler\ProfilerInterface; -class Driver extends AbstractPdo +final class Driver extends AbstractPdo { + protected ?ProfilerInterface $profiler = null; + + /** + * @param array $features + */ public function __construct( (PdoConnectionInterface&PdoDriverAwareInterface)|PDO $connection, StatementInterface&PdoDriverAwareInterface $statementPrototype = new Statement(), diff --git a/src/Result.php b/src/Result.php index 64526f3..6c8d4bf 100644 --- a/src/Result.php +++ b/src/Result.php @@ -21,11 +21,12 @@ // @mago-expect lint:cyclomatic-complexity // @mago-expect lint:kan-defect // @mago-expect lint:too-many-methods +/** @implements Iterator|null> */ final class Result implements Iterator, ResultInterface { protected mysqli|mysqli_result|mysqli_stmt $resource; - protected bool $isBuffered; + protected ?bool $isBuffered = null; protected int $position = 0; @@ -38,12 +39,13 @@ final class Result implements Iterator, ResultInterface protected bool $nextComplete = false; - /** @var mixed */ - protected $currentData; + /** @var array|null */ + protected ?array $currentData = null; + /** @var array{keys: string[]|null, values: array} */ protected array $statementBindValues = ['keys' => null, 'values' => []]; - protected mixed $generatedValue; + protected string|int|false|null $generatedValue = null; /** * {@inheritDoc} @@ -53,7 +55,7 @@ final class Result implements Iterator, ResultInterface #[Override] public function buffer(): void { - if ($this->resource instanceof mysqli_stmt && true !== $this->isBuffered) { + if ($this->resource instanceof mysqli_stmt && ! $this->isBuffered) { if ($this->position > 0) { throw new Exception\RuntimeException('Cannot buffer a result set that has started iteration.'); } @@ -72,16 +74,23 @@ public function buffer(): void #[Override] public function count() { - if (false === $this->isBuffered) { + if (! $this->isBuffered) { throw new Exception\RuntimeException('Row count is not available in unbuffered result sets.'); } - return $this->resource->num_rows; + + if (! $this->resource instanceof mysqli_result && ! $this->resource instanceof mysqli_stmt) { + throw new Exception\RuntimeException('Cannot count rows in a result that is not a query result'); + } + + return (int) $this->resource->num_rows; } /** * Current * - * @return mixed + * @throws Exception\ExceptionInterface + * + * @return array|null */ #[ReturnTypeWillChange] #[Override] @@ -107,10 +116,10 @@ public function current() public function getAffectedRows(): int { if ($this->resource instanceof mysqli || $this->resource instanceof mysqli_stmt) { - return $this->resource->affected_rows; + return (int) $this->resource->affected_rows; } - return $this->resource->num_rows; + return (int) $this->resource->num_rows; } /** @@ -148,17 +157,9 @@ public function getResource(): mysqli|mysqli_result|mysqli_stmt */ public function initialize( mysqli|mysqli_result|mysqli_stmt $resource, - mixed $generatedValue, + string|int|false|null $generatedValue, ?bool $isBuffered = null, ): ResultInterface { - if ( - ! $resource instanceof mysqli - && ! $resource instanceof mysqli_result - && ! $resource instanceof mysqli_stmt - ) { - throw new Exception\InvalidArgumentException('Invalid resource provided.'); - } - /** * todo(@tyrsson): examine this closely to see if this is the correct behavior */ @@ -166,7 +167,7 @@ public function initialize( null !== $isBuffered => $isBuffered, $resource instanceof mysqli || $resource instanceof mysqli_result - || ($resource instanceof mysqli_stmt && 0 !== $resource->num_rows) + || 0 !== $resource->num_rows => true, default => $this->isBuffered, }; @@ -197,7 +198,7 @@ public function isQueryResult(): bool /** * Key * - * @return mixed + * @return int */ #[ReturnTypeWillChange] #[Override] @@ -217,7 +218,7 @@ public function next() { $this->currentComplete = false; - if (false === $this->nextComplete) { + if (! $this->nextComplete) { $this->position++; } @@ -234,10 +235,14 @@ public function next() #[Override] public function rewind() { - if (0 !== $this->position && false === $this->isBuffered) { + if (0 !== $this->position && ! $this->isBuffered) { throw new Exception\RuntimeException('Unbuffered results cannot be rewound for multiple iterations'); } + if (! $this->resource instanceof mysqli_result && ! $this->resource instanceof mysqli_stmt) { + throw new Exception\RuntimeException('Cannot rewind a result that is not a query result'); + } + $this->resource->data_seek(0); // works for both mysqli_result & mysqli_stmt $this->currentComplete = false; $this->position = 0; @@ -246,6 +251,8 @@ public function rewind() /** * Valid * + * @throws Exception\ExceptionInterface + * * @return bool */ #[ReturnTypeWillChange] @@ -275,11 +282,20 @@ public function valid() */ protected function loadDataFromMysqliStatement(): bool { + if (! $this->resource instanceof mysqli_stmt) { + throw new Exception\RuntimeException('Expected resource to be an instance of mysqli_stmt'); + } + // build the default reference based bind structure, if it does not already exist if (null === $this->statementBindValues['keys']) { $this->statementBindValues['keys'] = []; $resultResource = $this->resource->result_metadata(); + if (false === $resultResource) { + return $resultResource; + } + foreach ($resultResource->fetch_fields() as $col) { + /** @var object{name: string} $col */ $this->statementBindValues['keys'][] = $col->name; } $this->statementBindValues['values'] = array_fill( @@ -301,7 +317,7 @@ protected function loadDataFromMysqliStatement(): bool return false; } - if (false === $r) { + if (! $r) { throw new Exception\RuntimeException($this->resource->error); } @@ -317,12 +333,21 @@ protected function loadDataFromMysqliStatement(): bool /** * Load from mysqli result + * + * @throws Exception\RuntimeException */ protected function loadFromMysqliResult(): bool { $this->currentData = null; - if (($data = $this->resource->fetch_assoc()) === null) { + if (! $this->resource instanceof mysqli_result) { + throw new Exception\RuntimeException('Cannot fetch from a result that is not a mysqli_result'); + } + + /** @var array|null $data */ + $data = $this->resource->fetch_assoc(); + + if (null === $data) { return false; } diff --git a/src/Sql/Ddl/AlterTableDecorator.php b/src/Sql/Ddl/AlterTableDecorator.php index b3cfabe..a13a017 100644 --- a/src/Sql/Ddl/AlterTableDecorator.php +++ b/src/Sql/Ddl/AlterTableDecorator.php @@ -4,8 +4,10 @@ namespace PhpDb\Mysql\Sql\Ddl; +use Override; use PhpDb\Adapter\Platform\PlatformInterface; use PhpDb\Sql\Ddl\AlterTable; +use PhpDb\Sql\Exception; use PhpDb\Sql\Platform\PlatformDecoratorInterface; use PhpDb\Sql\PreparableSqlInterface; use PhpDb\Sql\SqlInterface; @@ -24,7 +26,9 @@ // @mago-expect lint:kan-defect final class AlterTableDecorator extends AlterTable implements PlatformDecoratorInterface { - protected SqlInterface|PreparableSqlInterface|null $subject; + // @mago-expect analysis:write-only-property - read by the inherited AbstractSql::$subject handling + // (get_object_vars($this->subject)), since AlterTable extends AbstractSql + protected SqlInterface|PreparableSqlInterface|null $subject = null; /** @var array{ * unsigned: int, @@ -56,6 +60,7 @@ final class AlterTableDecorator extends AlterTable implements PlatformDecoratorI 'after' => 8, ]; + #[Override] public function setSubject( SqlInterface|PreparableSqlInterface|null $subject, ): PlatformDecoratorInterface { @@ -64,6 +69,9 @@ public function setSubject( return $this; } + /** + * @return array{0: int, 1: int, 2: int, 3: int} + */ protected function getSqlInsertOffsets(string $sql): array { $sqlLength = strlen($sql); @@ -94,11 +102,22 @@ protected function getSqlInsertOffsets(string $sql): array $insertStart[$i] ??= $sqlLength; } + /** @var array{0: int, 1: int, 2: int, 3: int} $insertStart */ return $insertStart; } + /** + * @return array> + * + * @throws Exception\RuntimeException + */ + #[Override] protected function processAddColumns(?PlatformInterface $adapterPlatform = null): array { + if (null === $adapterPlatform) { + throw new Exception\RuntimeException('Cannot build column SQL without a platform.'); + } + $sqls = []; foreach ($this->addColumns as $i => $column) { @@ -157,7 +176,6 @@ protected function processAddColumns(?PlatformInterface $adapterPlatform = null) } if ($insert) { - $j ??= 0; $sql = substr_replace($sql, $insert, $insertStart[$j], length: 0); $insertStartCount = count($insertStart); for (; $j < $insertStartCount; ++$j) { @@ -170,8 +188,18 @@ protected function processAddColumns(?PlatformInterface $adapterPlatform = null) return [$sqls]; } + /** + * @return array> + * + * @throws Exception\RuntimeException + */ + #[Override] protected function processChangeColumns(?PlatformInterface $adapterPlatform = null): array { + if (null === $adapterPlatform) { + throw new Exception\RuntimeException('Cannot build column SQL without a platform.'); + } + $sqls = []; foreach ($this->changeColumns as $name => $column) { $sql = $this->processExpression($column, $adapterPlatform); @@ -226,7 +254,6 @@ protected function processChangeColumns(?PlatformInterface $adapterPlatform = nu } if ($insert) { - $j ??= 0; $sql = substr_replace($sql, $insert, $insertStart[$j], length: 0); $insertStartCount = count($insertStart); for (; $j < $insertStartCount; ++$j) { @@ -243,13 +270,8 @@ protected function processChangeColumns(?PlatformInterface $adapterPlatform = nu return [$sqls]; } - /** - * @param string $columnA - * @param string $columnB - * @return int - */ // phpcs:ignore SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedMethod - private function compareColumnOptions($columnA, $columnB) + private function compareColumnOptions(string $columnA, string $columnB): int { $columnA = $this->normalizeColumnOption($columnA); $columnA = $this->columnOptionSortOrder[$columnA] ?? count($this->columnOptionSortOrder); @@ -260,11 +282,7 @@ private function compareColumnOptions($columnA, $columnB) return $columnA - $columnB; } - /** - * @param string $name - * @return string - */ - private function normalizeColumnOption($name) + private function normalizeColumnOption(string $name): string { return strtolower(str_replace(['-', '_', ' '], replace: '', subject: $name)); } diff --git a/src/Sql/Ddl/CreateTableDecorator.php b/src/Sql/Ddl/CreateTableDecorator.php index 20956b3..14f7991 100644 --- a/src/Sql/Ddl/CreateTableDecorator.php +++ b/src/Sql/Ddl/CreateTableDecorator.php @@ -4,8 +4,10 @@ namespace PhpDb\Mysql\Sql\Ddl; +use Override; use PhpDb\Adapter\Platform\PlatformInterface; use PhpDb\Sql\Ddl\CreateTable; +use PhpDb\Sql\Exception; use PhpDb\Sql\Platform\PlatformDecoratorInterface; use PhpDb\Sql\PreparableSqlInterface; use PhpDb\Sql\SqlInterface; @@ -23,10 +25,12 @@ // @mago-expect lint:kan-defect final class CreateTableDecorator extends CreateTable implements PlatformDecoratorInterface { - protected SqlInterface|PreparableSqlInterface|null $subject; + // @mago-expect analysis:write-only-property - read by the inherited AbstractSql::$subject handling + // (get_object_vars($this->subject)), since CreateTable extends AbstractSql + protected SqlInterface|PreparableSqlInterface|null $subject = null; - /** @var int[] */ - protected $columnOptionSortOrder = [ + /** @var array */ + protected array $columnOptionSortOrder = [ 'unsigned' => 0, 'zerofill' => 1, 'charset' => 2, @@ -40,6 +44,7 @@ final class CreateTableDecorator extends CreateTable implements PlatformDecorato 'storage' => 7, ]; + #[Override] public function setSubject( PreparableSqlInterface|SqlInterface|null $subject, ): PlatformDecoratorInterface { @@ -49,10 +54,9 @@ public function setSubject( } /** - * @param string $sql - * @return array + * @return array{0: int, 1: int, 2: int, 3: int} */ - protected function getSqlInsertOffsets($sql) + protected function getSqlInsertOffsets(string $sql): array { $sqlLength = strlen($sql); $insertStart = []; @@ -82,22 +86,30 @@ protected function getSqlInsertOffsets($sql) $insertStart[$i] ??= $sqlLength; } + /** @var array{0: int, 1: int, 2: int, 3: int} $insertStart */ return $insertStart; } /** * {@inheritDoc} + * + * @throws Exception\RuntimeException */ - protected function processColumns(?PlatformInterface $platform = null): ?array + #[Override] + protected function processColumns(?PlatformInterface $adapterPlatform = null): ?array { if (! $this->columns) { return null; } + if (null === $adapterPlatform) { + throw new Exception\RuntimeException('Cannot build column SQL without a platform.'); + } + $sqls = []; foreach ($this->columns as $i => $column) { - $sql = $this->processExpression($column, $platform); + $sql = $this->processExpression($column, $adapterPlatform); $insertStart = $this->getSqlInsertOffsets($sql); $columnOptions = $column->getOptions(); @@ -134,7 +146,7 @@ protected function processColumns(?PlatformInterface $platform = null): ?array $j = 1; break; case 'comment': - $insert = " COMMENT {$platform->quoteValue($coValue)}"; + $insert = " COMMENT {$adapterPlatform->quoteValue($coValue)}"; $j = 2; break; case 'columnformat': @@ -149,7 +161,6 @@ protected function processColumns(?PlatformInterface $platform = null): ?array } if ($insert) { - $j ??= 0; $sql = substr_replace($sql, $insert, $insertStart[$j], length: 0); $insertStartCount = count($insertStart); for (; $j < $insertStartCount; ++$j) { @@ -164,13 +175,8 @@ protected function processColumns(?PlatformInterface $platform = null): ?array return [$sqls]; } - /** - * @param string $columnA - * @param string $columnB - * @return int - */ // phpcs:ignore SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedMethod - private function compareColumnOptions($columnA, $columnB) + private function compareColumnOptions(string $columnA, string $columnB): int { $columnA = $this->normalizeColumnOption($columnA); $columnA = $this->columnOptionSortOrder[$columnA] ?? count($this->columnOptionSortOrder); @@ -181,11 +187,7 @@ private function compareColumnOptions($columnA, $columnB) return $columnA - $columnB; } - /** - * @param string $name - * @return string - */ - private function normalizeColumnOption($name) + private function normalizeColumnOption(string $name): string { return strtolower(str_replace(['-', '_', ' '], replace: '', subject: $name)); } diff --git a/src/Sql/SelectDecorator.php b/src/Sql/SelectDecorator.php index fba2b63..4f05437 100644 --- a/src/Sql/SelectDecorator.php +++ b/src/Sql/SelectDecorator.php @@ -15,7 +15,7 @@ final class SelectDecorator extends Select implements PlatformDecoratorInterface { - protected SqlInterface|PreparableSqlInterface|null $subject; + protected SqlInterface|PreparableSqlInterface|null $subject = null; #[Override] public function setSubject( @@ -56,6 +56,7 @@ protected function processLimit( return [$this->limit]; } + /** @return string[]|null */ #[Override] protected function processOffset( PlatformInterface $platform, diff --git a/src/Statement.php b/src/Statement.php index 382e1bc..7affd1f 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -20,6 +20,7 @@ use function array_unshift; use function call_user_func_array; use function is_array; +use function sprintf; final class Statement implements StatementInterface, DriverAwareInterface, ProfilerAwareInterface { @@ -43,7 +44,9 @@ public function __construct( /** * Execute * - * @throws Exception\RuntimeException + * @param array|ParameterContainer|null $parameters + * + * @throws Exception\ExceptionInterface */ #[Override] public function execute(ParameterContainer|array|null $parameters = null): ?ResultInterface @@ -72,12 +75,12 @@ public function execute(ParameterContainer|array|null $parameters = null): ?Resu $this->profiler?->profilerFinish(); - if (false === $return) { + if (! $return) { throw new Exception\RuntimeException($this->resource->error); } $buffered = false; - if (true === $this->bufferResults) { + if ($this->bufferResults) { $this->resource->store_result(); $this->isPrepared = false; $buffered = true; @@ -99,7 +102,13 @@ public function getProfiler(): ?ProfilerInterface /** * @phpstan-ignore method.childReturnType + * + * @return mysqli_stmt */ + // @mago-expect analysis:incompatible-return-type - StatementInterface::getResource() declares no + // native return type, only a legacy `resource|false|null` docblock; this class's $resource is + // always a genuine mysqli_stmt, so the narrower native return type here is a valid PHP covariant + // override, not a real incompatibility. #[Override] public function getResource(): mysqli_stmt { @@ -124,6 +133,9 @@ public function isPrepared(): bool return $this->isPrepared; } + /** + * @throws Exception\ExceptionInterface + */ #[Override] public function prepare(?string $sql = null): StatementInterface { @@ -133,8 +145,8 @@ public function prepare(?string $sql = null): StatementInterface $sql = null === $sql || '' === $sql ? $this->sql : $sql; - $this->resource = $this->mysqli->prepare($sql); - if (! $this->resource instanceof mysqli_stmt) { + $resource = $this->mysqli->prepare($sql); + if (! $resource instanceof mysqli_stmt) { throw new Exception\InvalidQueryException( "Statement couldn't be produced with sql: {$sql}", $this->mysqli->errno, @@ -142,6 +154,7 @@ public function prepare(?string $sql = null): StatementInterface ); } + $this->resource = $resource; $this->isPrepared = true; return $this; } @@ -149,6 +162,10 @@ public function prepare(?string $sql = null): StatementInterface #[Override] public function setDriver(DriverInterface $driver): DriverAwareInterface { + if (! $driver instanceof Driver) { + throw new Exception\InvalidArgumentException(sprintf('Driver must be an instance of %s', Driver::class)); + } + $this->driver = $driver; return $this; } @@ -178,7 +195,7 @@ public function setResource(mysqli_stmt $mysqliStatement): StatementInterface #[Override] public function setSql(?string $sql): StatementContainerInterface { - $this->sql = $sql; + $this->sql = $sql ?? ''; return $this; } diff --git a/test/unit/AdapterPlatformTest.php b/test/unit/AdapterPlatformTest.php index 95cf0d0..541e78d 100644 --- a/test/unit/AdapterPlatformTest.php +++ b/test/unit/AdapterPlatformTest.php @@ -5,10 +5,10 @@ namespace PhpDbTest\Mysql\Platform; use Override; +use PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection; use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\Pdo\Statement; use PhpDb\Mysql\AdapterPlatform; -use PhpDb\Mysql\Pdo\Connection; use PhpDb\Mysql\Pdo\Driver; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Test; @@ -236,7 +236,7 @@ public function quoteValueRaisesNoticeWithoutPlatformSupport(): void protected function setUp(): void { $pdo = new Driver( - $this->createMock(Connection::class), + $this->createMock(AbstractPdoConnection::class), $this->createMock(Statement::class), $this->createMock(Result::class), ); diff --git a/test/unit/Pdo/ConnectionTransactionsTest.php b/test/unit/Pdo/ConnectionTransactionsTest.php index 7d32fa5..958faf6 100644 --- a/test/unit/Pdo/ConnectionTransactionsTest.php +++ b/test/unit/Pdo/ConnectionTransactionsTest.php @@ -8,11 +8,12 @@ use PhpDb\Adapter\Driver\AbstractConnection; use PhpDb\Adapter\Exception\RuntimeException; use PhpDb\Mysql\Pdo\Connection; -use PhpDbTest\Mysql\Pdo\TestAsset\ConnectionWrapper; +use PhpDbTest\Mysql\Pdo\TestAsset\PdoStubDriver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +use ReflectionProperty; /** * Tests for {@see \PhpDb\Adapter\Mysql\Driver\Pdo\Connection} transaction support @@ -25,7 +26,7 @@ #[CoversMethod(Connection::class, 'rollback')] final class ConnectionTransactionsTest extends TestCase { - protected ConnectionWrapper $wrapper; + protected Connection $wrapper; #[Test] public function beginTransactionReturnsInstanceOfConnection(): void @@ -72,22 +73,22 @@ public function nestedTransactionsCommit(): void // 1st transaction $this->wrapper->beginTransaction(); static::assertTrue($this->wrapper->inTransaction()); - static::assertSame(1, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(1, $this->getNestedTransactionsCount($this->wrapper)); // 2nd transaction $this->wrapper->beginTransaction(); static::assertTrue($this->wrapper->inTransaction()); - static::assertSame(2, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(2, $this->getNestedTransactionsCount($this->wrapper)); // 1st commit $this->wrapper->commit(); static::assertTrue($this->wrapper->inTransaction()); - static::assertSame(1, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(1, $this->getNestedTransactionsCount($this->wrapper)); // 2nd commit $this->wrapper->commit(); static::assertFalse($this->wrapper->inTransaction()); - static::assertSame(0, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(0, $this->getNestedTransactionsCount($this->wrapper)); } #[Test] @@ -98,17 +99,17 @@ public function nestedTransactionsRollback(): void // 1st transaction $this->wrapper->beginTransaction(); static::assertTrue($this->wrapper->inTransaction()); - static::assertSame(1, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(1, $this->getNestedTransactionsCount($this->wrapper)); // 2nd transaction $this->wrapper->beginTransaction(); static::assertTrue($this->wrapper->inTransaction()); - static::assertSame(2, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(2, $this->getNestedTransactionsCount($this->wrapper)); // Rollback $this->wrapper->rollback(); static::assertFalse($this->wrapper->inTransaction()); - static::assertSame(0, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(0, $this->getNestedTransactionsCount($this->wrapper)); } #[Test] @@ -151,12 +152,12 @@ public function rollbackWithoutBeginThrowsException(): void public function standaloneCommit(): void { static::assertFalse($this->wrapper->inTransaction()); - static::assertSame(0, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(0, $this->getNestedTransactionsCount($this->wrapper)); $this->wrapper->commit(); static::assertFalse($this->wrapper->inTransaction()); - static::assertSame(0, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(0, $this->getNestedTransactionsCount($this->wrapper)); } /** @@ -165,6 +166,17 @@ public function standaloneCommit(): void #[Override] protected function setUp(): void { - $this->wrapper = new ConnectionWrapper(); + $this->wrapper = new Connection([]); + // bypass setResource(), which calls PDO::getAttribute() and would fail + // against the stub's uninitialized internal PDO state + (new ReflectionProperty($this->wrapper, 'resource'))->setValue( + $this->wrapper, + new PdoStubDriver('foo', 'bar', 'baz'), + ); + } + + private function getNestedTransactionsCount(Connection $connection): int + { + return (new ReflectionProperty($connection, 'nestedTransactionsCount'))->getValue($connection); } } diff --git a/test/unit/Pdo/DriverTest.php b/test/unit/Pdo/DriverTest.php index b80cd1a..8889a03 100644 --- a/test/unit/Pdo/DriverTest.php +++ b/test/unit/Pdo/DriverTest.php @@ -7,10 +7,10 @@ use Override; use PDOStatement; use PhpDb\Adapter\Driver\DriverInterface; +use PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection; use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\Pdo\Statement; use PhpDb\Exception\RuntimeException; -use PhpDb\Mysql\Pdo\Connection; use PhpDb\Mysql\Pdo\Driver; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\DataProvider; @@ -60,7 +60,7 @@ public function createResultPassesNullRowCount(): void ->method('rowCount') ->willReturn(4); - $connection = $this->createMock(Connection::class); + $connection = $this->createMock(AbstractPdoConnection::class); $statement = $this->createMock(Statement::class); $driver = new Driver($connection, $statement, new Result()); @@ -101,7 +101,7 @@ public function getResultPrototype(): void #[Override] protected function setUp(): void { - $connection = $this->createMock(Connection::class); + $connection = $this->createMock(AbstractPdoConnection::class); $statement = $this->createMock(Statement::class); $result = $this->createMock(Result::class); $this->pdo = new Driver( diff --git a/test/unit/Pdo/StatementIntegrationTest.php b/test/unit/Pdo/StatementIntegrationTest.php index fd7673a..f7ace3e 100644 --- a/test/unit/Pdo/StatementIntegrationTest.php +++ b/test/unit/Pdo/StatementIntegrationTest.php @@ -8,7 +8,8 @@ use PDO; use PDOStatement; use PhpDb\Adapter\Driver\Pdo\Statement; -use PhpDb\Mysql\Pdo\Driver as PdoDriver; +use PhpDb\Adapter\Driver\PdoDriverInterface; +use PhpDb\Adapter\Driver\ResultInterface; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\MockObject; @@ -85,10 +86,8 @@ public function statementExecuteWillUsePdoStrForStringIntegerWhenBinding(): void #[Override] protected function setUp(): void { - $driver = $this->getMockBuilder(PdoDriver::class) - ->onlyMethods(['createResult']) - ->disableOriginalConstructor() - ->getMock(); + $driver = $this->createMock(PdoDriverInterface::class); + $driver->method('createResult')->willReturn($this->createMock(ResultInterface::class)); $this->pdoStatementMock = $this->getMockBuilder(PDOStatement::class) ->onlyMethods(['execute', 'bindParam']) diff --git a/test/unit/Pdo/StatementTest.php b/test/unit/Pdo/StatementTest.php index 43d90c4..f914a9f 100644 --- a/test/unit/Pdo/StatementTest.php +++ b/test/unit/Pdo/StatementTest.php @@ -6,12 +6,12 @@ use Override; use PDOStatement; +use PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection; use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\Pdo\Statement; use PhpDb\Adapter\Driver\PdoDriverInterface; use PhpDb\Adapter\Driver\ResultInterface; use PhpDb\Adapter\ParameterContainer; -use PhpDb\Mysql\Pdo\Connection; use PhpDb\Mysql\Pdo\Driver; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Test; @@ -123,7 +123,7 @@ protected function setUp(): void { $this->statement = new Statement(); $this->pdo = new Driver( - $this->createMock(Connection::class), + $this->createMock(AbstractPdoConnection::class), $this->statement, new Result(), ); diff --git a/test/unit/Pdo/TestAsset/ConnectionWrapper.php b/test/unit/Pdo/TestAsset/ConnectionWrapper.php deleted file mode 100644 index f89a1e9..0000000 --- a/test/unit/Pdo/TestAsset/ConnectionWrapper.php +++ /dev/null @@ -1,23 +0,0 @@ -resource = new PdoStubDriver('foo', 'bar', 'baz'); - } - - public function getNestedTransactionsCount(): int - { - return $this->nestedTransactionsCount; - } -} diff --git a/test/unit/Sql/Ddl/AlterTableDecoratorTest.php b/test/unit/Sql/Ddl/AlterTableDecoratorTest.php index bb15d82..b842518 100644 --- a/test/unit/Sql/Ddl/AlterTableDecoratorTest.php +++ b/test/unit/Sql/Ddl/AlterTableDecoratorTest.php @@ -4,10 +4,10 @@ namespace PhpDbTest\Mysql\Sql\Ddl; +use PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection; use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\Pdo\Statement; use PhpDb\Mysql\AdapterPlatform; -use PhpDb\Mysql\Pdo\Connection; use PhpDb\Mysql\Pdo\Driver; use PhpDb\Mysql\Sql\Ddl\AlterTableDecorator; use PhpDb\Sql\Ddl\AlterTable; @@ -156,7 +156,7 @@ public function changeColumnCollate(): void protected function setUp(): void { $driver = new Driver( - $this->createMock(Connection::class), + $this->createMock(AbstractPdoConnection::class), $this->createMock(Statement::class), $this->createMock(Result::class), ); diff --git a/test/unit/Sql/Ddl/CreateTableDecoratorTest.php b/test/unit/Sql/Ddl/CreateTableDecoratorTest.php index 2dced31..448f98d 100644 --- a/test/unit/Sql/Ddl/CreateTableDecoratorTest.php +++ b/test/unit/Sql/Ddl/CreateTableDecoratorTest.php @@ -4,10 +4,10 @@ namespace PhpDbTest\Mysql\Sql\Ddl; +use PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection; use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\Pdo\Statement; use PhpDb\Mysql\AdapterPlatform; -use PhpDb\Mysql\Pdo\Connection; use PhpDb\Mysql\Pdo\Driver; use PhpDb\Mysql\Sql\Ddl\CreateTableDecorator; use PhpDb\Sql\Ddl\Column; @@ -151,7 +151,7 @@ public function unsignedOption(): void protected function setUp(): void { $driver = new Driver( - $this->createMock(Connection::class), + $this->createMock(AbstractPdoConnection::class), $this->createMock(Statement::class), $this->createMock(Result::class), );