diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..973b59b --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# mago format reformat commits go here, one SHA per line +50575fc32841622fb1cff85cb62a13b4eb8d5723 diff --git a/.gitignore b/.gitignore index 811cd36..956c545 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /infection.json5 /infection.log /summary.log +/mago-results.txt 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/composer.json b/composer.json index f99a556..55831d1 100644 --- a/composer.json +++ b/composer.json @@ -38,9 +38,9 @@ "require-dev": { "ext-mysqli": "*", "ext-pdo_mysql": "*", - "infection/infection": "^0.34.1", + "infection/infection": "^0.34.2", "php-db/phpdb-qa-tools": "0.1.x-dev", - "phpunit/phpunit": "^11.5.42" + "phpunit/phpunit": "^12.5.33" }, "suggest": { "ext-mysqli": "Required for MySQLi support", @@ -59,7 +59,7 @@ } }, "scripts": { - "check": [ + "check-all": [ "@cs-check", "@static-analysis", "@test", diff --git a/composer.lock b/composer.lock index 79a3fe8..e95afe9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "122262f195dcb1288431700530dfe6f0", + "content-hash": "66c86539851d7b014c3a1346f3152db0", "packages": [ { "name": "brick/varexporter", @@ -1192,20 +1192,20 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.4", + "version": "1.14.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.0" }, "conflict": { "doctrine/collections": "<1.6.8", @@ -1240,15 +1240,15 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" + "url": "https://github.com/mnapoli", + "type": "github" } ], - "time": "2025-08-01T08:46:24+00:00" + "time": "2026-08-11T10:17:44+00:00" }, { "name": "ondram/ci-detector", @@ -1452,12 +1452,12 @@ "source": { "type": "git", "url": "https://github.com/php-db/phpdb-qa-tools.git", - "reference": "7d8d8f56e65029d1d1cb8af6f8c5b2cd95fcec1d" + "reference": "f2323423deac77dd719f3f2ca7a9b2a7e21c93f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-db/phpdb-qa-tools/zipball/7d8d8f56e65029d1d1cb8af6f8c5b2cd95fcec1d", - "reference": "7d8d8f56e65029d1d1cb8af6f8c5b2cd95fcec1d", + "url": "https://api.github.com/repos/php-db/phpdb-qa-tools/zipball/f2323423deac77dd719f3f2ca7a9b2a7e21c93f6", + "reference": "f2323423deac77dd719f3f2ca7a9b2a7e21c93f6", "shasum": "" }, "require": { @@ -1488,20 +1488,20 @@ "issues": "https://github.com/php-db/phpdb-qa-tools/issues", "source": "https://github.com/php-db/phpdb-qa-tools" }, - "time": "2026-07-23T05:13:10+00:00" + "time": "2026-08-10T01:10:13+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "11.0.12", + "version": "12.5.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + "reference": "186dab580576598076de6818596d12b61801880e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", - "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/186dab580576598076de6818596d12b61801880e", + "reference": "186dab580576598076de6818596d12b61801880e", "shasum": "" }, "require": { @@ -1509,18 +1509,16 @@ "ext-libxml": "*", "ext-xmlwriter": "*", "nikic/php-parser": "^5.7.0", - "php": ">=8.2", - "phpunit/php-file-iterator": "^5.1.0", - "phpunit/php-text-template": "^4.0.1", - "sebastian/code-unit-reverse-lookup": "^4.0.1", - "sebastian/complexity": "^4.0.1", - "sebastian/environment": "^7.2.1", - "sebastian/lines-of-code": "^3.0.1", - "sebastian/version": "^5.0.2", - "theseer/tokenizer": "^1.3.1" + "php": ">=8.3", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.1.2", + "sebastian/lines-of-code": "^4.0.1", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^11.5.46" + "phpunit/phpunit": "^12.5.28" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -1529,7 +1527,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "11.0.x-dev" + "dev-main": "12.5.x-dev" } }, "autoload": { @@ -1558,7 +1556,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.7" }, "funding": [ { @@ -1578,32 +1576,32 @@ "type": "tidelift" } ], - "time": "2025-12-24T07:01:01+00:00" + "time": "2026-06-01T13:24:19+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "5.1.1", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", - "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.1-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -1631,7 +1629,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" }, "funding": [ { @@ -1651,28 +1649,28 @@ "type": "tidelift" } ], - "time": "2026-02-02T13:52:54+00:00" + "time": "2026-02-02T14:04:18+00:00" }, { "name": "phpunit/php-invoker", - "version": "5.0.1", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", - "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-pcntl": "*" @@ -1680,7 +1678,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -1707,7 +1705,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" }, "funding": [ { @@ -1715,32 +1713,32 @@ "type": "github" } ], - "time": "2024-07-03T05:07:44+00:00" + "time": "2025-02-07T04:58:58+00:00" }, { "name": "phpunit/php-text-template", - "version": "4.0.1", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", - "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -1767,7 +1765,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" }, "funding": [ { @@ -1775,32 +1773,32 @@ "type": "github" } ], - "time": "2024-07-03T05:08:43+00:00" + "time": "2025-02-07T04:59:16+00:00" }, { "name": "phpunit/php-timer", - "version": "7.0.1", + "version": "8.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", - "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -1827,7 +1825,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", "security": "https://github.com/sebastianbergmann/php-timer/security/policy", - "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" }, "funding": [ { @@ -1835,20 +1833,20 @@ "type": "github" } ], - "time": "2024-07-03T05:09:35+00:00" + "time": "2025-02-07T04:59:38+00:00" }, { "name": "phpunit/phpunit", - "version": "11.5.56", + "version": "12.5.33", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "5f83edffa6967c3db468d48a695ec7bcb02e9256" + "reference": "b98e028a26c5c5ba7e4a54be96ccf35f2914d184" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5f83edffa6967c3db468d48a695ec7bcb02e9256", - "reference": "5f83edffa6967c3db468d48a695ec7bcb02e9256", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b98e028a26c5c5ba7e4a54be96ccf35f2914d184", + "reference": "b98e028a26c5c5ba7e4a54be96ccf35f2914d184", "shasum": "" }, "require": { @@ -1861,35 +1859,31 @@ "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", - "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.12", - "phpunit/php-file-iterator": "^5.1.1", - "phpunit/php-invoker": "^5.0.1", - "phpunit/php-text-template": "^4.0.1", - "phpunit/php-timer": "^7.0.1", - "sebastian/cli-parser": "^3.0.2", - "sebastian/code-unit": "^3.0.3", - "sebastian/comparator": "^6.3.3", - "sebastian/diff": "^6.0.2", - "sebastian/environment": "^7.2.1", - "sebastian/exporter": "^6.3.2", - "sebastian/global-state": "^7.0.2", - "sebastian/object-enumerator": "^6.0.1", - "sebastian/recursion-context": "^6.0.3", - "sebastian/type": "^5.1.3", - "sebastian/version": "^5.0.2", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.7", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.1", + "sebastian/comparator": "^7.1.8", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.1.2", + "sebastian/exporter": "^7.0.3", + "sebastian/global-state": "^8.0.3", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.4", + "sebastian/version": "^6.0.0", "staabm/side-effects-detector": "^1.0.5" }, - "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files" - }, "bin": [ "phpunit" ], "type": "library", "extra": { "branch-alias": { - "dev-main": "11.5-dev" + "dev-main": "12.5-dev" } }, "autoload": { @@ -1921,7 +1915,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.56" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.33" }, "funding": [ { @@ -1929,7 +1923,7 @@ "type": "other" } ], - "time": "2026-07-06T14:52:39+00:00" + "time": "2026-07-28T13:58:09+00:00" }, { "name": "psr/clock", @@ -2031,16 +2025,16 @@ }, { "name": "sanmai/di-container", - "version": "0.1.22", + "version": "0.1.23", "source": { "type": "git", "url": "https://github.com/sanmai/di-container.git", - "reference": "084abe756ba2c0d9a793c2d142ec6e8fb50e9869" + "reference": "8cf59c091f33297389d0a5a27ea0d688df15c376" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sanmai/di-container/zipball/084abe756ba2c0d9a793c2d142ec6e8fb50e9869", - "reference": "084abe756ba2c0d9a793c2d142ec6e8fb50e9869", + "url": "https://api.github.com/repos/sanmai/di-container/zipball/8cf59c091f33297389d0a5a27ea0d688df15c376", + "reference": "8cf59c091f33297389d0a5a27ea0d688df15c376", "shasum": "" }, "require": { @@ -2098,7 +2092,7 @@ ], "support": { "issues": "https://github.com/sanmai/di-container/issues", - "source": "https://github.com/sanmai/di-container/tree/0.1.22" + "source": "https://github.com/sanmai/di-container/tree/0.1.23" }, "funding": [ { @@ -2106,7 +2100,7 @@ "type": "github" } ], - "time": "2026-08-09T15:52:22+00:00" + "time": "2026-08-11T00:58:41+00:00" }, { "name": "sanmai/duoclock", @@ -2305,28 +2299,28 @@ }, { "name": "sebastian/cli-parser", - "version": "3.0.2", + "version": "4.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.5.25" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "4.2-dev" } }, "autoload": { @@ -2350,152 +2344,51 @@ "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - } - ], - "time": "2024-07-03T04:41:36+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", - "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" - }, - "funding": [ + }, { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2025-03-19T07:56:08+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "4.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "183a9b2632194febd219bb9246eee421dad8d45e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", - "reference": "183a9b2632194febd219bb9246eee421dad8d45e", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" - }, - "funding": [ + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" } ], - "time": "2024-07-03T04:45:54+00:00" + "time": "2026-05-17T05:29:34+00:00" }, { "name": "sebastian/comparator", - "version": "6.3.3", + "version": "7.1.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + "reference": "7c65c1e79836812819705b473a90c12399542485" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", - "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485", + "reference": "7c65c1e79836812819705b473a90c12399542485", "shasum": "" }, "require": { "ext-dom": "*", "ext-mbstring": "*", - "php": ">=8.2", - "sebastian/diff": "^6.0", - "sebastian/exporter": "^6.0" + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0.3" }, "require-dev": { - "phpunit/phpunit": "^11.4" + "phpunit/phpunit": "^12.5.25" }, "suggest": { "ext-bcmath": "For comparing BcMath\\Number objects" @@ -2503,7 +2396,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.3-dev" + "dev-main": "7.1-dev" } }, "autoload": { @@ -2543,7 +2436,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.8" }, "funding": [ { @@ -2563,33 +2456,33 @@ "type": "tidelift" } ], - "time": "2026-01-24T09:26:40+00:00" + "time": "2026-05-21T04:45:25+00:00" }, { "name": "sebastian/complexity", - "version": "4.0.1", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", - "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", "shasum": "" }, "require": { "nikic/php-parser": "^5.0", - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -2613,7 +2506,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" }, "funding": [ { @@ -2621,33 +2514,33 @@ "type": "github" } ], - "time": "2024-07-03T04:49:50+00:00" + "time": "2025-02-07T04:55:25+00:00" }, { "name": "sebastian/diff", - "version": "6.0.2", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -2680,7 +2573,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" }, "funding": [ { @@ -2688,27 +2581,27 @@ "type": "github" } ], - "time": "2024-07-03T04:53:05+00:00" + "time": "2025-02-07T04:55:46+00:00" }, { "name": "sebastian/environment", - "version": "7.2.1", + "version": "8.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", - "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^12.5.26" }, "suggest": { "ext-posix": "*" @@ -2716,7 +2609,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "7.2-dev" + "dev-main": "8.1-dev" } }, "autoload": { @@ -2744,7 +2637,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + "source": "https://github.com/sebastianbergmann/environment/tree/8.1.2" }, "funding": [ { @@ -2764,34 +2657,34 @@ "type": "tidelift" } ], - "time": "2025-05-21T11:55:47+00:00" + "time": "2026-05-25T13:40:20+00:00" }, { "name": "sebastian/exporter", - "version": "6.3.2", + "version": "7.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", - "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": ">=8.2", - "sebastian/recursion-context": "^6.0" + "php": ">=8.3", + "sebastian/recursion-context": "^7.0.1" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^12.5.25" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.3-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -2834,7 +2727,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.3" }, "funding": [ { @@ -2854,35 +2747,35 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:12:51+00:00" + "time": "2026-05-20T04:37:17+00:00" }, { "name": "sebastian/global-state", - "version": "7.0.2", + "version": "8.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", - "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9", "shasum": "" }, "require": { - "php": ">=8.2", - "sebastian/object-reflector": "^4.0", - "sebastian/recursion-context": "^6.0" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0.1" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.5.28" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -2908,41 +2801,53 @@ "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-07-03T04:57:36+00:00" + "time": "2026-06-01T15:10:33+00:00" }, { "name": "sebastian/lines-of-code", - "version": "3.0.1", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", - "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e", "shasum": "" }, "require": { - "nikic/php-parser": "^5.0", - "php": ">=8.2" + "nikic/php-parser": "^5.7.0", + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.5.25" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -2966,42 +2871,54 @@ "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" } ], - "time": "2024-07-03T04:58:38+00:00" + "time": "2026-05-19T16:22:07+00:00" }, { "name": "sebastian/object-enumerator", - "version": "6.0.1", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", - "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", "shasum": "" }, "require": { - "php": ">=8.2", - "sebastian/object-reflector": "^4.0", - "sebastian/recursion-context": "^6.0" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -3024,7 +2941,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" }, "funding": [ { @@ -3032,32 +2949,32 @@ "type": "github" } ], - "time": "2024-07-03T05:00:13+00:00" + "time": "2025-02-07T04:57:48+00:00" }, { "name": "sebastian/object-reflector", - "version": "4.0.1", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", - "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -3080,7 +2997,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" }, "funding": [ { @@ -3088,32 +3005,32 @@ "type": "github" } ], - "time": "2024-07-03T05:01:32+00:00" + "time": "2025-02-07T04:58:17+00:00" }, { "name": "sebastian/recursion-context", - "version": "6.0.3", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", - "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -3144,7 +3061,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" }, "funding": [ { @@ -3164,32 +3081,32 @@ "type": "tidelift" } ], - "time": "2025-08-13T04:42:22+00:00" + "time": "2025-08-13T04:44:59+00:00" }, { "name": "sebastian/type", - "version": "5.1.3", + "version": "6.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + "reference": "82ff822c2edc46724be9f7411d3163021f602773" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", - "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773", + "reference": "82ff822c2edc46724be9f7411d3163021f602773", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^12.5.25" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.1-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -3213,7 +3130,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + "source": "https://github.com/sebastianbergmann/type/tree/6.0.4" }, "funding": [ { @@ -3233,29 +3150,29 @@ "type": "tidelift" } ], - "time": "2025-08-09T06:55:48+00:00" + "time": "2026-05-20T06:45:45+00:00" }, { "name": "sebastian/version", - "version": "5.0.2", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -3279,7 +3196,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/version/issues", "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" }, "funding": [ { @@ -3287,7 +3204,7 @@ "type": "github" } ], - "time": "2024-10-09T05:16:32+00:00" + "time": "2025-02-07T05:00:38+00:00" }, { "name": "staabm/side-effects-detector", @@ -4451,23 +4368,23 @@ }, { "name": "theseer/tokenizer", - "version": "1.3.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", "shasum": "" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" + "php": "^8.1" }, "type": "library", "autoload": { @@ -4489,7 +4406,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" }, "funding": [ { @@ -4497,7 +4414,7 @@ "type": "github" } ], - "time": "2025-11-17T20:03:58+00:00" + "time": "2025-12-08T11:19:18+00:00" }, { "name": "webmozart/assert", diff --git a/mago.toml b/mago.toml index 4e2bc2b..09d7ae4 100644 --- a/mago.toml +++ b/mago.toml @@ -1,7 +1,13 @@ -#: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" + +[formatter] +parentheses-around-new-in-member-access = true diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a2f9255..8c1413c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,14 +1,14 @@ - + diff --git a/src/AdapterPlatform.php b/src/AdapterPlatform.php index f6cdc29..9db9b86 100644 --- a/src/AdapterPlatform.php +++ b/src/AdapterPlatform.php @@ -14,9 +14,9 @@ use function implode; use function str_replace; -class AdapterPlatform extends AbstractPlatform +final class AdapterPlatform extends AbstractPlatform { - public final const PLATFORM_NAME = 'MySQL'; + final public const PLATFORM_NAME = 'MySQL'; /** * {@inheritDoc} @@ -34,9 +34,8 @@ class AdapterPlatform extends AbstractPlatform protected string $quoteIdentifierFragmentPattern = '/([^0-9,a-z,A-Z$_\-:])/i'; public function __construct( - protected readonly DriverInterface|mysqli|PDO $driver - ) { - } + protected readonly DriverInterface|mysqli|PDO $driver, + ) {} /** * {@inheritDoc} @@ -62,41 +61,40 @@ public function getSqlPlatformDecorator(): PlatformDecoratorInterface #[Override] public function quoteIdentifierChain(array|string $identifierChain): string { - return '`' . implode('`.`', (array) str_replace('`', '``', $identifierChain)) . '`'; + return '`' . implode('`.`', (array) str_replace('`', replace: '``', subject: $identifierChain)) . '`'; } /** * {@inheritDoc} */ #[Override] - public function quoteValue(string $value): string + public function quoteTrustedValue(int|float|string|bool $value): ?string { $quotedViaDriverValue = $this->quoteViaDriver($value); - return $quotedViaDriverValue ?? parent::quoteValue($value); + return $quotedViaDriverValue ?? parent::quoteTrustedValue($value); } /** * {@inheritDoc} */ #[Override] - public function quoteTrustedValue(int|float|string|bool $value): ?string + public function quoteValue(string $value): string { $quotedViaDriverValue = $this->quoteViaDriver($value); - return $quotedViaDriverValue ?? parent::quoteTrustedValue($value); + return $quotedViaDriverValue ?? parent::quoteValue($value); } protected function quoteViaDriver(string $value): ?string { - if ($this->driver instanceof DriverInterface) { - // todo: verify this can not return a PDOStatement instance - $resource = $this->driver->getConnection()->getResource(); - } else { - $resource = $this->driver; - } + // todo(@tyrsson): verify this can not return a PDOStatement instance + $resource = $this->driver instanceof DriverInterface + ? $this->driver->getConnection()->getResource() + : $this->driver; if ($resource instanceof mysqli) { + // @mago-expect lint:string-style return '\'' . $resource->real_escape_string($value) . '\''; } diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 63dd0a0..7f73013 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -12,13 +12,12 @@ final class ConfigProvider { - public function __invoke(): array - { - return [ - 'dependencies' => $this->getDependencies(), - ]; - } - + /** + * @return array{ + * aliases: array, + * factories: array, + * } + */ public function getDependencies(): array { return [ @@ -50,4 +49,19 @@ public function getDependencies(): array ], ]; } + + /** + * @return array{ + * dependencies: array{ + * aliases: array, + * factories: array, + * }, + * } + */ + public function __invoke(): array + { + return [ + 'dependencies' => $this->getDependencies(), + ]; + } } diff --git a/src/Connection.php b/src/Connection.php index 1cd009f..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; @@ -24,9 +25,15 @@ use const MYSQLI_CLIENT_SSL; use const MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT; +// @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; @@ -34,56 +41,69 @@ class Connection extends AbstractConnection implements DriverAwareInterface /** * Constructor * + * @param array|mysqli|null $connectionInfo + * * @throws InvalidArgumentException */ public function __construct( - array|mysqli|null $connectionInfo = null + array|mysqli|null $connectionInfo = null, ) { if (is_array($connectionInfo)) { $this->setConnectionParameters($connectionInfo); - } elseif ($connectionInfo instanceof mysqli) { - $this->setResource($connectionInfo); - } elseif (null !== $connectionInfo) { - throw new Exception\InvalidArgumentException( - '$connection must be an array of parameters, a mysqli object or null' - ); + + return; } - } - public function setDriver(DriverInterface $driver): DriverAwareInterface - { - $this->driver = $driver; + if ($connectionInfo instanceof mysqli) { + $this->setResource($connectionInfo); - return $this; + return; + } } - /** @inheritDoc */ + /** + * @inheritDoc + * + * @throws Exception\ExceptionInterface + */ #[Override] - public function getCurrentSchema(): string|false + public function beginTransaction(): ConnectionInterface { if (! $this->isConnected()) { $this->connect(); } - $result = $this->resource->query('SELECT DATABASE()'); - $r = $result->fetch_row(); + $this->resource->autocommit(false); + $this->inTransaction = true; - return $r[0]; + return $this; } /** - * Set resource + * @inheritDoc * - * @return $this Provides a fluent interface + * @throws Exception\ExceptionInterface */ - public function setResource(mysqli $resource): static + #[Override] + public function commit(): ConnectionInterface { - $this->resource = $resource; + if (! $this->isConnected()) { + $this->connect(); + } + + $this->resource->commit(); + $this->inTransaction = false; + $this->resource->autocommit(true); return $this; } - /** @inheritDoc */ + /** + * @inheritDoc + * + * @throws Exception\ExceptionInterface + */ + // @mago-expect lint:halstead #[Override] public function connect(): ConnectionInterface { @@ -91,14 +111,13 @@ public function connect(): ConnectionInterface return $this; } - /** @var array $p */ $p = $this->connectionParameters; // given a list of key names, test for existence in $p /** @var string[] $names */ - $findParameterValue = function (array $names) use ($p): string|null { + $findParameterValue = static function (array $names) use ($p): ?string { foreach ($names as $name) { - if (isset($p[$name])) { + if (null !== ($p[$name] ?? null)) { return $p[$name]; } } @@ -111,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 = isset($p['port']) ? (int) $p['port'] : null; + $port = null === ($p['port'] ?? null) ? null : (int) $p['port']; /** @var string|null $socket */ $socket = $p['socket'] ?? null; @@ -126,7 +144,7 @@ public function connect(): ConnectionInterface $this->resource = $this->createResource(); - if (! empty($p['driver_options'])) { + if ([] !== ($p['driver_options'] ?? [])) { foreach ($p['driver_options'] as $option => $value) { if (is_string($option)) { $option = strtoupper($option); @@ -150,21 +168,21 @@ public function connect(): ConnectionInterface $this->resource->ssl_set($clientKey, $clientCert, $caCert, $caPath, $cipher); //MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT is not valid option, needs to be set as flag if ( - isset($p['driver_options'][MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT]) + null !== ($p['driver_options'][MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT] ?? null) ) { $flags |= MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT; } } try { - $flags === null + null === $flags ? $this->resource->real_connect($hostname, $username, $password, $database, $port, $socket) : $this->resource->real_connect($hostname, $username, $password, $database, $port, $socket, $flags); } catch (GenericException) { throw new Exception\RuntimeException( 'Connection error', $this->resource->connect_errno, - new Exception\ErrorException($this->resource->connect_error, $this->resource->connect_errno) + new Exception\ErrorException($this->resource->connect_error, $this->resource->connect_errno), ); } @@ -172,23 +190,17 @@ public function connect(): ConnectionInterface throw new Exception\RuntimeException( 'Connection error', $this->resource->connect_errno, - new Exception\ErrorException($this->resource->connect_error, $this->resource->connect_errno) + new Exception\ErrorException($this->resource->connect_error, $this->resource->connect_errno), ); } - if (! empty($p['charset'])) { + if ('' !== ($p['charset'] ?? '')) { $this->resource->set_charset($p['charset']); } return $this; } - /** @inheritDoc */ - public function isConnected(): bool - { - return $this->resource instanceof mysqli; - } - /** @inheritDoc */ #[Override] public function disconnect(): ConnectionInterface @@ -200,37 +212,90 @@ public function disconnect(): ConnectionInterface return $this; } - /** @inheritDoc */ + /** + * {@inheritDoc} + * + * @throws Exception\ExceptionInterface + */ #[Override] - public function beginTransaction(): ConnectionInterface + public function execute(string $sql): ?ResultInterface { if (! $this->isConnected()) { $this->connect(); } - $this->resource->autocommit(false); - $this->inTransaction = true; + $this->profiler?->profilerStart($sql); - return $this; + $resultResource = $this->resource->query($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 commit(): ConnectionInterface + public function getCurrentSchema(): string|false { if (! $this->isConnected()) { $this->connect(); } - $this->resource->commit(); - $this->inTransaction = false; - $this->resource->autocommit(true); + $result = $this->resource->query('SELECT DATABASE()'); + if (! $result instanceof mysqli_result) { + throw new Exception\RuntimeException('Failed to query current schema'); + } - return $this; + $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]; } /** @inheritDoc */ #[Override] + public function getLastGeneratedValue(?string $name = null): string|int|false|null + { + return $this->resource->insert_id; + } + + /** @inheritDoc */ + #[Override] + public function isConnected(): bool + { + return $this->resource instanceof mysqli; + } + + /** + * @inheritDoc + * + * @throws Exception\ExceptionInterface + */ + #[Override] public function rollback(): ConnectionInterface { if (! $this->isConnected()) { @@ -248,47 +313,34 @@ public function rollback(): ConnectionInterface return $this; } - /** - * {@inheritDoc} - * - * @throws Exception\InvalidQueryException - */ #[Override] - public function execute($sql): ?ResultInterface + public function setDriver(DriverInterface $driver): DriverAwareInterface { - if (! $this->isConnected()) { - $this->connect(); - } - - $this->profiler?->profilerStart($sql); - - $resultResource = $this->resource->query($sql); - - $this->profiler?->profilerFinish($sql); - - // if the returnValue is something other than a mysqli_result, bypass wrapping it - if ($resultResource === false) { - throw new Exception\InvalidQueryException($this->resource->error); - } + $this->driver = $driver; - return $this->driver->createResult($resultResource === true ? $this->resource : $resultResource); + return $this; } - /** @inheritDoc */ - #[Override] - public function getLastGeneratedValue(?string $name = null): string|int|false|null + /** + * Set resource + * + * @return $this Provides a fluent interface + */ + public function setResource(mysqli $resource): static { - return $this->resource->insert_id; + $this->resource = $resource; + + return $this; } /** * Create a new mysqli resource * - * todo: why do we have this random method here? + * todo(@tyrsson): why do we have this random method here? * * @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 3712fef..9d18ed9 100644 --- a/src/Container/ConnectionInterfaceFactory.php +++ b/src/Container/ConnectionInterfaceFactory.php @@ -13,16 +13,23 @@ final class ConnectionInterfaceFactory { + /** + * @param array|null $options + * + * @throws \PhpDb\Adapter\Exception\ExceptionInterface + * + * @mago-expect analysis:unused-parameter + */ public function __invoke( ContainerInterface $container, string $requestedName, - ?array $options = null + ?array $options = null, ): ConnectionInterface&Connection { $conn = $options['connection'] ?? []; - if (! is_array($conn) || $conn === []) { + if (! is_array($conn) || [] === $conn) { throw new InvalidConnectionParametersException( 'Connection configuration must be an array of parameters passed via $options["connection"]', - $conn + $conn, ); } diff --git a/src/Container/DriverInterfaceFactory.php b/src/Container/DriverInterfaceFactory.php index 0ffd825..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; @@ -16,28 +14,38 @@ use PhpDb\Mysql\Statement; use Psr\Container\ContainerInterface; +use function array_key_exists; + 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, - ?array $options = null + ?array $options = null, ): DriverInterface&Driver { - if (! isset($options['connection'])) { + if (null === $options || ! array_key_exists('connection', $options)) { throw ContainerException::forService( Driver::class, self::class, - '$options["connection"] must contain an array of connection configuration.' + '$options["connection"] must contain an array of connection configuration.', ); } - /** @var ConnectionInterface&Connection $connectionInstance */ $connectionInstance = $container->build(Connection::class, $options); - /** @var StatementInterface&Statement $statementInstance */ $statementInstance = $container->build( Statement::class, - $options['options'] ?? [] + $options['options'] ?? [], ); /** @var ResultInterface&Result $resultInstance */ @@ -49,7 +57,7 @@ public function __invoke( $connectionInstance, $statementInstance, $resultInstance, - $options + $options, ); } } diff --git a/src/Container/MetadataInterfaceFactory.php b/src/Container/MetadataInterfaceFactory.php index a9ef58f..b1b8f3f 100644 --- a/src/Container/MetadataInterfaceFactory.php +++ b/src/Container/MetadataInterfaceFactory.php @@ -12,10 +12,19 @@ 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, - ?array $options = null + ?array $options = null, ): MetadataInterface&Metadata\Source { $adapterServiceName = $options[self::ADAPTER_SERVICE_NAME] ?? AdapterInterface::class; diff --git a/src/Container/PdoConnectionInterfaceFactory.php b/src/Container/PdoConnectionInterfaceFactory.php index 6ebcc8c..8b3abd7 100644 --- a/src/Container/PdoConnectionInterfaceFactory.php +++ b/src/Container/PdoConnectionInterfaceFactory.php @@ -13,16 +13,23 @@ final class PdoConnectionInterfaceFactory { + /** + * @param array|null $options + * + * @throws \PhpDb\Adapter\Exception\ExceptionInterface + * + * @mago-expect analysis:unused-parameter + */ public function __invoke( ContainerInterface $container, string $requestedName, - ?array $options = null + ?array $options = null, ): PdoConnectionInterface&Connection { $conn = $options['connection'] ?? []; - if (! is_array($conn) || $conn === []) { + if (! is_array($conn) || [] === $conn) { throw new InvalidConnectionParametersException( 'Connection configuration must be an array of parameters passed via $options["connection"]', - $conn + $conn, ); } diff --git a/src/Container/PdoDriverInterfaceFactory.php b/src/Container/PdoDriverInterfaceFactory.php index 1460219..ee797f7 100644 --- a/src/Container/PdoDriverInterfaceFactory.php +++ b/src/Container/PdoDriverInterfaceFactory.php @@ -7,36 +7,43 @@ 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; use Psr\Container\ContainerInterface; +use function array_key_exists; + 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, - ?array $options = null + ?array $options = null, ): PdoDriverInterface&Driver { - if (! isset($options['connection'])) { + if (null === $options || ! array_key_exists('connection', $options)) { throw ContainerException::forService( Driver::class, self::class, - '$options["connection"] must contain an array of connection configuration.' + '$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'] ?? [] + $options['options'] ?? [], ); /** @var ResultInterface&Result $resultInstance */ @@ -48,7 +55,7 @@ public function __invoke( $connectionInstance, $statementInstance, $resultInstance, - $options['pdo_features'] ?? [] + $options['pdo_features'] ?? [], ); } } diff --git a/src/Container/PdoStatementFactory.php b/src/Container/PdoStatementFactory.php index 190a74f..76fdcab 100644 --- a/src/Container/PdoStatementFactory.php +++ b/src/Container/PdoStatementFactory.php @@ -10,10 +10,15 @@ final class PdoStatementFactory { + /** + * @param array|null $options + * + * @mago-expect analysis:unused-parameter + */ public function __invoke( ContainerInterface $container, string $requestedName, - ?array $options = null + ?array $options = null, ): StatementInterface&Statement { return new Statement(options: $options); } diff --git a/src/Container/PlatformInterfaceFactory.php b/src/Container/PlatformInterfaceFactory.php index c64f696..a6c6165 100644 --- a/src/Container/PlatformInterfaceFactory.php +++ b/src/Container/PlatformInterfaceFactory.php @@ -13,17 +13,24 @@ final class PlatformInterfaceFactory { + /** + * @param array|null $options + * + * @throws \Psr\Container\ContainerExceptionInterface + * + * @mago-expect analysis:unused-parameter + */ public function __invoke( ContainerInterface $container, string $requestedName, - ?array $options = null + ?array $options = null, ): PlatformInterface&AdapterPlatform { $driverInstance = $options['driver'] ?? null; if (! $driverInstance instanceof MysqliDriver && ! $driverInstance instanceof PdoDriver) { throw ContainerException::forService( AdapterPlatform::class, self::class, - '$options["driver"] must be an instance of ' . MysqliDriver::class . ' or ' . PdoDriver::class . '.' + '$options["driver"] must be an instance of ' . MysqliDriver::class . ' or ' . PdoDriver::class . '.', ); } return new AdapterPlatform($driverInstance); diff --git a/src/Container/StatementInterfaceFactory.php b/src/Container/StatementInterfaceFactory.php index ddc5770..479149c 100644 --- a/src/Container/StatementInterfaceFactory.php +++ b/src/Container/StatementInterfaceFactory.php @@ -10,10 +10,15 @@ final class StatementInterfaceFactory { + /** + * @param array|null $options + * + * @mago-expect analysis:unused-parameter + */ public function __invoke( ContainerInterface $container, string $requestedName, - ?array $options = null + ?array $options = null, ): StatementInterface&Statement { return new Statement(bufferResults: $options['buffer_results'] ?? false); } diff --git a/src/Driver.php b/src/Driver.php index 8061df5..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; @@ -16,7 +16,6 @@ use PhpDb\Adapter\Profiler\ProfilerInterface; use function array_intersect_key; -use function array_merge; use function extension_loaded; use function is_string; @@ -29,75 +28,57 @@ 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(), protected readonly ResultInterface&Result $resultPrototype = new Result(), - array $options = [] + array $options = [], ) { $this->checkEnvironment(); - $options = array_intersect_key(array_merge($this->options, $options), $this->options); - - if ($this->connection instanceof DriverAwareInterface) { - $this->connection->setDriver($this); - } - - if ($this->statementPrototype instanceof DriverAwareInterface) { - $this->statementPrototype->setDriver($this); - } - } - - 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); - } - return $this; - } + $options = array_intersect_key([...$this->options, ...$options], $this->options); - public function getProfiler(): ?ProfilerInterface - { - return $this->profiler; - } - - /** - * Get statement prototype - */ - public function getStatementPrototype(): StatementInterface&Statement - { - return $this->statementPrototype; - } - - public function getResultPrototype(): ResultInterface&Result - { - return $this->resultPrototype; + $this->connection->setDriver($this); + $this->statementPrototype->setDriver($this); } + #[Override] public function checkEnvironment(): bool { if (! extension_loaded('mysqli')) { throw new Exception\RuntimeException( - 'The Mysqli extension is required for this adapter but the extension is not loaded' + 'The Mysqli extension is required for this adapter but the extension is not loaded', ); } return true; } - public function getConnection(): ConnectionInterface&Connection + /** + * Create result + * + * @param mysqli|mysqli_result|mysqli_stmt $resource + */ + #[Override] + public function createResult($resource, ?bool $isBuffered = null): ResultInterface&Result { - return $this->connection; + $result = clone $this->resultPrototype; + $result->initialize($resource, $this->connection->getLastGeneratedValue(), $isBuffered); + return $result; } /** * Create statement * * @param mysqli|mysqli_stmt|string $sqlOrResource + * + * @throws Exception\ExceptionInterface */ + #[Override] public function createStatement($sqlOrResource = null): StatementInterface&Statement { /** @@ -110,54 +91,79 @@ public function createStatement($sqlOrResource = null): StatementInterface&State $statement = clone $this->statementPrototype; if ($sqlOrResource instanceof mysqli_stmt) { $statement->setResource($sqlOrResource); - } else { - if (is_string($sqlOrResource)) { - $statement->setSql($sqlOrResource); - } - if (! $this->connection->isConnected()) { - $this->connection->connect(); - } - /** @var mysqli $resource */ - $resource = $this->connection->getResource(); - $statement->initialize($resource); + + return $statement; + } + + if (is_string($sqlOrResource)) { + $statement->setSql($sqlOrResource); } + if (! $this->connection->isConnected()) { + $this->connection->connect(); + } + /** @var mysqli $resource */ + $resource = $this->connection->getResource(); + $statement->initialize($resource); return $statement; } /** - * Create result - * - * @param mysqli|mysqli_result|mysqli_stmt $resource + * Format parameter name */ - public function createResult($resource, ?bool $isBuffered = null): ResultInterface&Result + #[Override] + public function formatParameterName(string $name, ?string $type = null): string { - /** @var Result $result */ - $result = clone $this->resultPrototype; - $result->initialize($resource, $this->connection->getLastGeneratedValue(), $isBuffered); - return $result; + return '?'; + } + + #[Override] + public function getConnection(): ConnectionInterface&Connection + { + return $this->connection; + } + + /** + * Get last generated value + */ + #[Override] + public function getLastGeneratedValue(): int|string|false|null + { + return $this->getConnection()->getLastGeneratedValue(); } /** * Get prepare type */ + #[Override] public function getPrepareType(): string { return self::PARAMETERIZATION_POSITIONAL; } - /** - * Format parameter name - */ - public function formatParameterName(string $name, ?string $type = null): string + public function getProfiler(): ?ProfilerInterface { - return '?'; + return $this->profiler; + } + + public function getResultPrototype(): ResultInterface&Result + { + return $this->resultPrototype; } /** - * Get last generated value + * Get statement prototype */ - public function getLastGeneratedValue(): int|string|null|false + public function getStatementPrototype(): StatementInterface&Statement { - return $this->getConnection()->getLastGeneratedValue(); + return $this->statementPrototype; + } + + #[Override] + public function setProfiler(ProfilerInterface $profiler): ProfilerAwareInterface + { + $this->profiler = $profiler; + $this->connection->setProfiler($profiler); + $this->statementPrototype->setProfiler($profiler); + return $this; } } diff --git a/src/Metadata/Source.php b/src/Metadata/Source.php index 5bc976d..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; @@ -20,94 +22,15 @@ use const CASE_LOWER; use const PREG_PATTERN_ORDER; +// @mago-expect lint:cyclomatic-complexity +// @mago-expect lint:kan-defect final class Source extends AbstractSource { - /** - * @throws Exception - */ - protected function loadSchemaData(): void - { - if (isset($this->data['schemas'])) { - return; - } - $this->prepareDataHierarchy('schemas'); - - $p = $this->adapter->getPlatform(); - - $sql = 'SELECT ' . $p->quoteIdentifier('SCHEMA_NAME') - . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'SCHEMATA']) - . ' WHERE ' . $p->quoteIdentifier('SCHEMA_NAME') - . ' != \'INFORMATION_SCHEMA\''; - - $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); - - $schemas = []; - foreach ($results->toArray() as $row) { - $schemas[] = $row['SCHEMA_NAME']; - } - - $this->data['schemas'] = $schemas; - } - - protected function loadTableNameData(string $schema): void - { - if (isset($this->data['table_names'][$schema])) { - return; - } - $this->prepareDataHierarchy('table_names', $schema); - - $p = $this->adapter->getPlatform(); - - $isColumns = [ - ['T', 'TABLE_NAME'], - ['T', 'TABLE_TYPE'], - ['V', 'VIEW_DEFINITION'], - ['V', 'CHECK_OPTION'], - ['V', 'IS_UPDATABLE'], - ]; - - array_walk($isColumns, function (&$c) use ($p) { - $c = $p->quoteIdentifierChain($c); - }); - - $sql = 'SELECT ' . implode(', ', $isColumns) - . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) . 'T' - - . ' LEFT JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'VIEWS']) . ' V' - . ' ON ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' = ' . $p->quoteIdentifierChain(['V', 'TABLE_SCHEMA']) - . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) - . ' = ' . $p->quoteIdentifierChain(['V', 'TABLE_NAME']) - - . ' WHERE ' . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) - . ' IN (\'BASE TABLE\', \'VIEW\')'; - - if ($schema !== self::DEFAULT_SCHEMA) { - $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' = ' . $p->quoteTrustedValue($schema); - } else { - $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' != \'INFORMATION_SCHEMA\''; - } - - $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); - - $tables = []; - foreach ($results->toArray() as $row) { - $tables[$row['TABLE_NAME']] = [ - 'table_type' => $row['TABLE_TYPE'], - 'view_definition' => $row['VIEW_DEFINITION'], - 'check_option' => $row['CHECK_OPTION'], - 'is_updatable' => 'YES' === $row['IS_UPDATABLE'], - ]; - } - - $this->data['table_names'][$schema] = $tables; - } - + // @mago-expect lint:halstead + #[Override] protected function loadColumnData(string $table, string $schema): void { - if (isset($this->data['columns'][$schema][$table])) { + if (null !== ($this->data['columns'][$schema][$table] ?? null)) { return; } $this->prepareDataHierarchy('columns', $schema, $table); @@ -126,49 +49,56 @@ protected function loadColumnData(string $table, string $schema): void ['C', 'COLUMN_TYPE'], ]; - array_walk($isColumns, function (&$c) use ($p) { + array_walk($isColumns, static function (&$c) use ($p) { $c = $p->quoteIdentifierChain($c); }); - $sql = 'SELECT ' . implode(', ', $isColumns) - . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) . 'T' - . ' INNER JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'COLUMNS']) . 'C' - . ' ON ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' = ' . $p->quoteIdentifierChain(['C', 'TABLE_SCHEMA']) - . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) - . ' = ' . $p->quoteIdentifierChain(['C', 'TABLE_NAME']) - . ' WHERE ' . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) - . ' IN (\'BASE TABLE\', \'VIEW\')' - . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) - . ' = ' . $p->quoteTrustedValue($table); - - if ($schema !== self::DEFAULT_SCHEMA) { - $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' = ' . $p->quoteTrustedValue($schema); - } else { - $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' != \'INFORMATION_SCHEMA\''; - } - + $sql = + 'SELECT ' + . implode(', ', $isColumns) + . ' FROM ' + . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) + . 'T' + . ' INNER JOIN ' + . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'COLUMNS']) + . 'C' + . ' ON ' + . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' + . $p->quoteIdentifierChain(['C', 'TABLE_SCHEMA']) + . ' AND ' + . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' + . $p->quoteIdentifierChain(['C', 'TABLE_NAME']) + . ' WHERE ' + . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) + . ' IN (\'BASE TABLE\', \'VIEW\')' + . ' AND ' + . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' + . $p->quoteTrustedValue($table); + + $sql .= self::DEFAULT_SCHEMA === $schema + ? " 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 = []; if (preg_match('/^(?:enum|set)\((.+)\)$/i', $row['COLUMN_TYPE'], $matches)) { $permittedValues = $matches[1]; - if ( - preg_match_all( - "/\\s*'((?:[^']++|'')*+)'\\s*(?:,|\$)/", - $permittedValues, - $matches, - PREG_PATTERN_ORDER - ) - ) { - $permittedValues = str_replace("''", "'", $matches[1]); - } else { - $permittedValues = [$permittedValues]; - } + $permittedValues = preg_match_all( + "/\\s*'((?:[^']++|'')*+)'\\s*(?:,|\$)/", + $permittedValues, + $matches, + PREG_PATTERN_ORDER, + ) + ? str_replace("''", replace: "'", subject: $matches[1]) + : [$permittedValues]; $erratas['permitted_values'] = $permittedValues; } $columns[$row['COLUMN_NAME']] = [ @@ -188,23 +118,25 @@ protected function loadColumnData(string $table, string $schema): void $this->data['columns'][$schema][$table] = $columns; } + // @mago-expect lint:halstead + #[Override] protected function loadConstraintData(string $table, string $schema): void { // phpcs:disable WebimpressCodingStandard.NamingConventions.ValidVariableName.NotCamelCaps - if (isset($this->data['constraints'][$schema][$table])) { + if (null !== ($this->data['constraints'][$schema][$table] ?? null)) { return; } $this->prepareDataHierarchy('constraints', $schema, $table); $isColumns = [ - ['T', 'TABLE_NAME'], - ['TC', 'CONSTRAINT_NAME'], - ['TC', 'CONSTRAINT_TYPE'], + ['T', 'TABLE_NAME'], + ['TC', 'CONSTRAINT_NAME'], + ['TC', 'CONSTRAINT_TYPE'], ['KCU', 'COLUMN_NAME'], - ['RC', 'MATCH_OPTION'], - ['RC', 'UPDATE_RULE'], - ['RC', 'DELETE_RULE'], + ['RC', 'MATCH_OPTION'], + ['RC', 'UPDATE_RULE'], + ['RC', 'DELETE_RULE'], ['KCU', 'REFERENCED_TABLE_SCHEMA'], ['KCU', 'REFERENCED_TABLE_NAME'], ['KCU', 'REFERENCED_COLUMN_NAME'], @@ -212,68 +144,84 @@ protected function loadConstraintData(string $table, string $schema): void $p = $this->adapter->getPlatform(); - array_walk($isColumns, function (&$c) use ($p) { + array_walk($isColumns, static function (&$c) use ($p) { $c = $p->quoteIdentifierChain($c); }); - $sql = 'SELECT ' . implode(', ', $isColumns) - . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) . ' T' - - . ' INNER JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLE_CONSTRAINTS']) . ' TC' - . ' ON ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' = ' . $p->quoteIdentifierChain(['TC', 'TABLE_SCHEMA']) - . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) - . ' = ' . $p->quoteIdentifierChain(['TC', 'TABLE_NAME']) - - . ' LEFT JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'KEY_COLUMN_USAGE']) . ' KCU' - . ' ON ' . $p->quoteIdentifierChain(['TC', 'TABLE_SCHEMA']) - . ' = ' . $p->quoteIdentifierChain(['KCU', 'TABLE_SCHEMA']) - . ' AND ' . $p->quoteIdentifierChain(['TC', 'TABLE_NAME']) - . ' = ' . $p->quoteIdentifierChain(['KCU', 'TABLE_NAME']) - . ' AND ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_NAME']) - . ' = ' . $p->quoteIdentifierChain(['KCU', 'CONSTRAINT_NAME']) - - . ' LEFT JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'REFERENTIAL_CONSTRAINTS']) . ' RC' - . ' ON ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_SCHEMA']) - . ' = ' . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_SCHEMA']) - . ' AND ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_NAME']) - . ' = ' . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_NAME']) - - . ' WHERE ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) - . ' = ' . $p->quoteTrustedValue($table) - . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) - . ' IN (\'BASE TABLE\', \'VIEW\')'; - - if ($schema !== self::DEFAULT_SCHEMA) { - $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' = ' . $p->quoteTrustedValue($schema); - } else { - $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' != \'INFORMATION_SCHEMA\''; - } - - $sql .= ' ORDER BY CASE ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_TYPE']) - . " WHEN 'PRIMARY KEY' THEN 1" - . " WHEN 'UNIQUE' THEN 2" - . " WHEN 'FOREIGN KEY' THEN 3" - . " ELSE 4 END" - - . ', ' . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_NAME']) - . ', ' . $p->quoteIdentifierChain(['KCU', 'ORDINAL_POSITION']); - + $sql = + 'SELECT ' + . implode(', ', $isColumns) + . ' FROM ' + . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) + . ' T' + . ' INNER JOIN ' + . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLE_CONSTRAINTS']) + . ' TC' + . ' ON ' + . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' + . $p->quoteIdentifierChain(['TC', 'TABLE_SCHEMA']) + . ' AND ' + . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' + . $p->quoteIdentifierChain(['TC', 'TABLE_NAME']) + . ' LEFT JOIN ' + . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'KEY_COLUMN_USAGE']) + . ' KCU' + . ' ON ' + . $p->quoteIdentifierChain(['TC', 'TABLE_SCHEMA']) + . ' = ' + . $p->quoteIdentifierChain(['KCU', 'TABLE_SCHEMA']) + . ' AND ' + . $p->quoteIdentifierChain(['TC', 'TABLE_NAME']) + . ' = ' + . $p->quoteIdentifierChain(['KCU', 'TABLE_NAME']) + . ' AND ' + . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_NAME']) + . ' = ' + . $p->quoteIdentifierChain(['KCU', 'CONSTRAINT_NAME']) + . ' LEFT JOIN ' + . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'REFERENTIAL_CONSTRAINTS']) + . ' RC' + . ' ON ' + . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_SCHEMA']) + . ' = ' + . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_SCHEMA']) + . ' AND ' + . $p->quoteIdentifierChain(['TC', 'CONSTRAINT_NAME']) + . ' = ' + . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_NAME']) + . ' WHERE ' + . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' + . $p->quoteTrustedValue($table) + . ' AND ' + . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + $sql .= self::DEFAULT_SCHEMA === $schema + ? " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} != 'INFORMATION_SCHEMA'" + : " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} = {$p->quoteTrustedValue($schema)}"; + + $sql .= " ORDER BY CASE {$p->quoteIdentifierChain([ + 'TC', + 'CONSTRAINT_TYPE', + ])} WHEN 'PRIMARY KEY' THEN 1 WHEN 'UNIQUE' THEN 2 WHEN 'FOREIGN KEY' THEN 3 ELSE 4 END, {$p->quoteIdentifierChain([ + 'TC', + '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']; - $isFK = 'FOREIGN KEY' === $row['CONSTRAINT_TYPE']; - if ($isFK) { - $name = $realName; - } else { - $name = '_laminas_' . $row['TABLE_NAME'] . '_' . $realName; - } + $realName = $row['CONSTRAINT_NAME']; + $isFK = 'FOREIGN KEY' === $row['CONSTRAINT_TYPE']; + $name = $isFK ? $realName : "_phpdb_{$row['TABLE_NAME']}_{$realName}"; $constraints[$name] = [ 'constraint_name' => $name, 'constraint_type' => $row['CONSTRAINT_TYPE'], @@ -296,108 +244,127 @@ protected function loadConstraintData(string $table, string $schema): void } $this->data['constraints'][$schema][$table] = $constraints; + // phpcs:enable WebimpressCodingStandard.NamingConventions.ValidVariableName.NotCamelCaps } - protected function loadConstraintDataNames(string $schema): void + #[Override] + protected function loadConstraintDataKeys(string $schema): void { - if (isset($this->data['constraint_names'][$schema])) { + if (null !== ($this->data['constraint_keys'][$schema] ?? null)) { return; } - $this->prepareDataHierarchy('constraint_names', $schema); + $this->prepareDataHierarchy('constraint_keys', $schema); $p = $this->adapter->getPlatform(); $isColumns = [ - ['TC', 'TABLE_NAME'], - ['TC', 'CONSTRAINT_NAME'], - ['TC', 'CONSTRAINT_TYPE'], + ['T', 'TABLE_NAME'], + ['KCU', 'CONSTRAINT_NAME'], + ['KCU', 'COLUMN_NAME'], + ['KCU', 'ORDINAL_POSITION'], ]; - array_walk($isColumns, function (&$c) use ($p) { + array_walk($isColumns, static function (&$c) use ($p) { $c = $p->quoteIdentifierChain($c); }); - $sql = 'SELECT ' . implode(', ', $isColumns) - . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) . 'T' - . ' INNER JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLE_CONSTRAINTS']) . 'TC' - . ' ON ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' = ' . $p->quoteIdentifierChain(['TC', 'TABLE_SCHEMA']) - . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) - . ' = ' . $p->quoteIdentifierChain(['TC', 'TABLE_NAME']) - . ' WHERE ' . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) - . ' IN (\'BASE TABLE\', \'VIEW\')'; - - if ($schema !== self::DEFAULT_SCHEMA) { - $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' = ' . $p->quoteTrustedValue($schema); - } else { - $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' != \'INFORMATION_SCHEMA\''; - } - + $sql = + 'SELECT ' + . implode(', ', $isColumns) + . ' FROM ' + . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) + . 'T' + . ' INNER JOIN ' + . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'KEY_COLUMN_USAGE']) + . 'KCU' + . ' ON ' + . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' + . $p->quoteIdentifierChain(['KCU', 'TABLE_SCHEMA']) + . ' AND ' + . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' + . $p->quoteIdentifierChain(['KCU', 'TABLE_NAME']) + . ' WHERE ' + . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + $sql .= self::DEFAULT_SCHEMA === $schema + ? " 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); } - $this->data['constraint_names'][$schema] = $data; + $this->data['constraint_keys'][$schema] = $data; } - protected function loadConstraintDataKeys(string $schema): void + protected function loadConstraintDataNames(string $schema): void { - if (isset($this->data['constraint_keys'][$schema])) { + if (null !== ($this->data['constraint_names'][$schema] ?? null)) { return; } - $this->prepareDataHierarchy('constraint_keys', $schema); + $this->prepareDataHierarchy('constraint_names', $schema); $p = $this->adapter->getPlatform(); $isColumns = [ - ['T', 'TABLE_NAME'], - ['KCU', 'CONSTRAINT_NAME'], - ['KCU', 'COLUMN_NAME'], - ['KCU', 'ORDINAL_POSITION'], + ['TC', 'TABLE_NAME'], + ['TC', 'CONSTRAINT_NAME'], + ['TC', 'CONSTRAINT_TYPE'], ]; - array_walk($isColumns, function (&$c) use ($p) { + array_walk($isColumns, static function (&$c) use ($p) { $c = $p->quoteIdentifierChain($c); }); - $sql = 'SELECT ' . implode(', ', $isColumns) - . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) . 'T' - - . ' INNER JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'KEY_COLUMN_USAGE']) . 'KCU' - . ' ON ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' = ' . $p->quoteIdentifierChain(['KCU', 'TABLE_SCHEMA']) - . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) - . ' = ' . $p->quoteIdentifierChain(['KCU', 'TABLE_NAME']) - - . ' WHERE ' . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) - . ' IN (\'BASE TABLE\', \'VIEW\')'; - - if ($schema !== self::DEFAULT_SCHEMA) { - $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' = ' . $p->quoteTrustedValue($schema); - } else { - $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' != \'INFORMATION_SCHEMA\''; - } - + $sql = + 'SELECT ' + . implode(', ', $isColumns) + . ' FROM ' + . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) + . 'T' + . ' INNER JOIN ' + . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLE_CONSTRAINTS']) + . 'TC' + . ' ON ' + . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' + . $p->quoteIdentifierChain(['TC', 'TABLE_SCHEMA']) + . ' AND ' + . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' + . $p->quoteIdentifierChain(['TC', 'TABLE_NAME']) + . ' WHERE ' + . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + $sql .= self::DEFAULT_SCHEMA === $schema + ? " 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); } - $this->data['constraint_keys'][$schema] = $data; + $this->data['constraint_names'][$schema] = $data; } + #[Override] protected function loadConstraintReferences(string $table, string $schema): void { parent::loadConstraintReferences($table, $schema); @@ -405,50 +372,64 @@ protected function loadConstraintReferences(string $table, string $schema): void $p = $this->adapter->getPlatform(); $isColumns = [ - ['RC', 'TABLE_NAME'], - ['RC', 'CONSTRAINT_NAME'], - ['RC', 'UPDATE_RULE'], - ['RC', 'DELETE_RULE'], + ['RC', 'TABLE_NAME'], + ['RC', 'CONSTRAINT_NAME'], + ['RC', 'UPDATE_RULE'], + ['RC', 'DELETE_RULE'], ['KCU', 'REFERENCED_TABLE_SCHEMA'], ['KCU', 'REFERENCED_TABLE_NAME'], ['KCU', 'REFERENCED_COLUMN_NAME'], ]; - array_walk($isColumns, function (&$c) use ($p) { + array_walk($isColumns, static function (&$c) use ($p) { $c = $p->quoteIdentifierChain($c); }); - $sql = 'SELECT ' . implode(', ', $isColumns) - . 'FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) . 'T' - - . ' INNER JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'REFERENTIAL_CONSTRAINTS']) . 'RC' - . ' ON ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' = ' . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_SCHEMA']) - . ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) - . ' = ' . $p->quoteIdentifierChain(['RC', 'TABLE_NAME']) - - . ' INNER JOIN ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'KEY_COLUMN_USAGE']) . 'KCU' - . ' ON ' . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_SCHEMA']) - . ' = ' . $p->quoteIdentifierChain(['KCU', 'TABLE_SCHEMA']) - . ' AND ' . $p->quoteIdentifierChain(['RC', 'TABLE_NAME']) - . ' = ' . $p->quoteIdentifierChain(['KCU', 'TABLE_NAME']) - . ' AND ' . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_NAME']) - . ' = ' . $p->quoteIdentifierChain(['KCU', 'CONSTRAINT_NAME']) - - . 'WHERE ' . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) - . ' IN (\'BASE TABLE\', \'VIEW\')'; - - if ($schema !== self::DEFAULT_SCHEMA) { - $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' = ' . $p->quoteTrustedValue($schema); - } else { - $sql .= ' AND ' . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) - . ' != \'INFORMATION_SCHEMA\''; - } - + $sql = + 'SELECT ' + . implode(', ', $isColumns) + . 'FROM ' + . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) + . 'T' + . ' INNER JOIN ' + . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'REFERENTIAL_CONSTRAINTS']) + . 'RC' + . ' ON ' + . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' + . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_SCHEMA']) + . ' AND ' + . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' + . $p->quoteIdentifierChain(['RC', 'TABLE_NAME']) + . ' INNER JOIN ' + . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'KEY_COLUMN_USAGE']) + . 'KCU' + . ' ON ' + . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_SCHEMA']) + . ' = ' + . $p->quoteIdentifierChain(['KCU', 'TABLE_SCHEMA']) + . ' AND ' + . $p->quoteIdentifierChain(['RC', 'TABLE_NAME']) + . ' = ' + . $p->quoteIdentifierChain(['KCU', 'TABLE_NAME']) + . ' AND ' + . $p->quoteIdentifierChain(['RC', 'CONSTRAINT_NAME']) + . ' = ' + . $p->quoteIdentifierChain(['KCU', 'CONSTRAINT_NAME']) + . 'WHERE ' + . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + $sql .= self::DEFAULT_SCHEMA === $schema + ? " 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); } @@ -456,9 +437,105 @@ protected function loadConstraintReferences(string $table, string $schema): void $this->data['constraint_references'][$schema] = $data; } + /** + * @throws Exception + */ + #[Override] + protected function loadSchemaData(): void + { + if (null !== ($this->data['schemas'] ?? null)) { + return; + } + $this->prepareDataHierarchy('schemas'); + + $p = $this->adapter->getPlatform(); + + $sql = <<quoteIdentifier('SCHEMA_NAME')} + FROM {$p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'SCHEMATA'])} + 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']; + } + + $this->data['schemas'] = $schemas; + } + + #[Override] + protected function loadTableNameData(string $schema): void + { + if (null !== ($this->data['table_names'][$schema] ?? null)) { + return; + } + $this->prepareDataHierarchy('table_names', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = [ + ['T', 'TABLE_NAME'], + ['T', 'TABLE_TYPE'], + ['V', 'VIEW_DEFINITION'], + ['V', 'CHECK_OPTION'], + ['V', 'IS_UPDATABLE'], + ]; + + array_walk($isColumns, static function (&$c) use ($p) { + $c = $p->quoteIdentifierChain($c); + }); + + $sql = + 'SELECT ' + . implode(', ', $isColumns) + . ' FROM ' + . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TABLES']) + . 'T' + . ' LEFT JOIN ' + . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'VIEWS']) + . ' V' + . ' ON ' + . $p->quoteIdentifierChain(['T', 'TABLE_SCHEMA']) + . ' = ' + . $p->quoteIdentifierChain(['V', 'TABLE_SCHEMA']) + . ' AND ' + . $p->quoteIdentifierChain(['T', 'TABLE_NAME']) + . ' = ' + . $p->quoteIdentifierChain(['V', 'TABLE_NAME']) + . ' WHERE ' + . $p->quoteIdentifierChain(['T', 'TABLE_TYPE']) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + $sql .= self::DEFAULT_SCHEMA === $schema + ? " 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'], + 'view_definition' => $row['VIEW_DEFINITION'], + 'check_option' => $row['CHECK_OPTION'], + 'is_updatable' => 'YES' === $row['IS_UPDATABLE'], + ]; + } + + $this->data['table_names'][$schema] = $tables; + } + + #[Override] protected function loadTriggerData(string $schema): void { - if (isset($this->data['triggers'][$schema])) { + if (null !== ($this->data['triggers'][$schema] ?? null)) { return; } @@ -486,26 +563,28 @@ protected function loadTriggerData(string $schema): void 'CREATED', ]; - array_walk($isColumns, function (&$c) use ($p) { + array_walk($isColumns, static function (&$c) use ($p) { $c = $p->quoteIdentifier($c); }); - $sql = 'SELECT ' . implode(', ', $isColumns) - . ' FROM ' . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TRIGGERS']) - . ' WHERE '; + $sql = + 'SELECT ' + . implode(', ', $isColumns) + . ' FROM ' + . $p->quoteIdentifierChain(['INFORMATION_SCHEMA', 'TRIGGERS']) + . ' WHERE '; - if ($schema !== self::DEFAULT_SCHEMA) { - $sql .= $p->quoteIdentifier('TRIGGER_SCHEMA') - . ' = ' . $p->quoteTrustedValue($schema); - } else { - $sql .= $p->quoteIdentifier('TRIGGER_SCHEMA') - . ' != \'INFORMATION_SCHEMA\''; - } + $sql .= self::DEFAULT_SCHEMA === $schema + ? "{$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 8c93155..49fe7dc 100644 --- a/src/Pdo/Connection.php +++ b/src/Pdo/Connection.php @@ -19,40 +19,33 @@ use function is_string; use function strtolower; -class Connection extends AbstractPdoConnection +// @mago-expect lint:cyclomatic-complexity +// @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( - PDO|array $connectionParameters + PDO|array $connectionParameters, ) { if (is_array($connectionParameters)) { $this->setConnectionParameters($connectionParameters); - } elseif ($connectionParameters instanceof PDO) { - $this->setResource($connectionParameters); - } - } - /** - * {@inheritDoc} - */ - #[Override] - public function getCurrentSchema(): string|false - { - if (! $this->isConnected()) { - $this->connect(); - } - - /** @var PDOStatement $result */ - $result = $this->resource->query('SELECT DATABASE()'); - if ($result instanceof PDOStatement) { - return $result->fetchColumn(); + return; } - return false; + $this->setResource($connectionParameters); } /** @@ -61,6 +54,7 @@ public function getCurrentSchema(): string|false * @throws Exception\InvalidConnectionParametersException * @throws Exception\RuntimeException */ + // @mago-expect lint:halstead #[Override] public function connect(): ConnectionInterface { @@ -68,91 +62,126 @@ public function connect(): ConnectionInterface return $this; } - $dsn = $username = $password = $hostname = $port = $charset = $database = $unixSocket = $version = null; - $options = []; + $dsn = null; + $username = null; + $password = null; + $hostname = null; + $port = null; + $charset = null; + $database = null; + $unixSocket = null; + $version = null; + $options = []; foreach ($this->connectionParameters as $key => $value) { $result = match (strtolower($key)) { - 'dsn' => $dsn = (string) $value, - 'user', 'username' => $username = (string) $value, - 'password', 'passwd', 'pw' => $password = (string) $value, - 'host', 'hostname' => $hostname = (string) $value, - 'port' => $port = (int) $value, - 'charset' => $charset = (string) $value, - 'dbname', 'database', 'db', 'schema' => $database = (string) $value, + 'dsn' => $dsn = (string) $value, + 'user', 'username' => $username = (string) $value, + 'password', 'passwd', 'pw' => $password = (string) $value, + 'host', 'hostname' => $hostname = (string) $value, + 'port' => $port = (int) $value, + 'charset' => $charset = (string) $value, + 'dbname', 'database', 'db', 'schema' => $database = (string) $value, 'unix_socket' => $unixSocket = (string) $value, - 'version' => $version = (string) $value, - // todo: should we suppport sslmode for pdo pgsql? - 'driver_options' => (function (&$options, $value): void { + 'version' => $version = (string) $value, + // todo(@tyrsson): should we suppport sslmode for pdo pgsql? + 'driver_options' => (static function (&$options, $value): void { $value = (array) $value; $options = array_diff_key($options, $value) + $value; })($options, $value), - default => $options[$key] = $value, + default => $options[$key] = $value, }; } unset($result); - if (isset($hostname) && isset($unixSocket)) { + if (null !== $hostname && null !== $unixSocket) { throw new Exception\InvalidConnectionParametersException( 'Ambiguous connection parameters, both hostname and unix_socket parameters were set', - $this->connectionParameters + $this->connectionParameters, ); } - if (! isset($dsn)) { + if (null === $dsn) { $dsn = []; - if (isset($database)) { + if (null !== $database) { $dsn[] = "dbname={$database}"; } - if (isset($hostname)) { + if (null !== $hostname) { $dsn[] = "host={$hostname}"; } - if (isset($port)) { + if (null !== $port) { $dsn[] = "port={$port}"; } - if (isset($charset)) { + if (null !== $charset) { $dsn[] = "charset={$charset}"; } - if (isset($unixSocket)) { + if (null !== $unixSocket) { $dsn[] = "unix_socket={$unixSocket}"; } - if (isset($version)) { + if (null !== $version) { $dsn[] = "version={$version}"; } $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)) { $code = 0; } - throw new Exception\RuntimeException('Connect Error: ' . $e->getMessage(), $code, $e); + throw new Exception\RuntimeException("Connect Error: {$e->getMessage()}", $code, $e); } return $this; } + /** + * {@inheritDoc} + * + * @throws Exception\ExceptionInterface + * @throws PDOException + */ + #[Override] + public function getCurrentSchema(): string|false + { + if (! $this->isConnected()) { + $this->connect(); + } + + 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 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 (\Exception) { - // do nothing + } catch (PDOException) { + // not all pdo drivers support lastInsertId; fall through to false + // @mago-expect lint:no-empty-catch-clause } return false; diff --git a/src/Pdo/Driver.php b/src/Pdo/Driver.php index acf17cf..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(), @@ -35,7 +41,7 @@ public function __construct( $this->statementPrototype->setDriver($this); // $features is not constructor promoted because $this->features is defined in the trait - if ($features !== [] && $this instanceof DriverFeatureProviderInterface) { + if ([] !== $features && $this instanceof DriverFeatureProviderInterface) { $this->addFeatures($features); } } diff --git a/src/Result.php b/src/Result.php index ad8b41f..6c8d4bf 100644 --- a/src/Result.php +++ b/src/Result.php @@ -18,11 +18,15 @@ use function call_user_func_array; use function count; +// @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; @@ -35,50 +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; - - /** - * Initialize - * - * @throws Exception\InvalidArgumentException - * psalm-suppress PossiblyUnusedMethod - */ - public function initialize( - mysqli|mysqli_result|mysqli_stmt $resource, - mixed $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: examine this closely to see if this is the correct behavior - */ - if ($isBuffered !== null) { - $this->isBuffered = $isBuffered; - } else { - if ( - $resource instanceof mysqli || $resource instanceof mysqli_result - || $resource instanceof mysqli_stmt && $resource->num_rows !== 0 - ) { - $this->isBuffered = true; - } - } - - $this->resource = $resource; - $this->generatedValue = $generatedValue; - return $this; - } + protected string|int|false|null $generatedValue = null; /** * {@inheritDoc} @@ -88,7 +55,7 @@ public function initialize( #[Override] public function buffer(): void { - if ($this->resource instanceof mysqli_stmt && $this->isBuffered !== true) { + 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.'); } @@ -98,130 +65,146 @@ public function buffer(): void } /** - * {@inheritDoc} + * Count + * + * @throws Exception\RuntimeException + * @return int */ + #[ReturnTypeWillChange] #[Override] - public function isBuffered(): ?bool + public function count() { - return $this->isBuffered; + if (! $this->isBuffered) { + throw new Exception\RuntimeException('Row count is not available in unbuffered result sets.'); + } + + 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; } /** - * {@inheritDoc} + * Current + * + * @throws Exception\ExceptionInterface + * + * @return array|null */ + #[ReturnTypeWillChange] #[Override] - public function getResource(): mysqli|mysqli_result|mysqli_stmt + public function current() { - return $this->resource; + if ($this->currentComplete) { + return $this->currentData; + } + + if ($this->resource instanceof mysqli_stmt) { + $this->loadDataFromMysqliStatement(); + return $this->currentData; + } + + $this->loadFromMysqliResult(); + return $this->currentData; } /** * {@inheritDoc} */ #[Override] - public function isQueryResult(): bool + public function getAffectedRows(): int { - return $this->resource->field_count > 0; + if ($this->resource instanceof mysqli || $this->resource instanceof mysqli_stmt) { + return (int) $this->resource->affected_rows; + } + + return (int) $this->resource->num_rows; } /** * {@inheritDoc} */ #[Override] - public function getAffectedRows(): int + public function getFieldCount(): int { - if ($this->resource instanceof mysqli || $this->resource instanceof mysqli_stmt) { - return $this->resource->affected_rows; - } - - return $this->resource->num_rows; + return $this->resource->field_count; } /** - * Current - * - * @return mixed + * Get generated value */ - #[ReturnTypeWillChange] #[Override] - public function current() + public function getGeneratedValue(): string|int|false|null { - if ($this->currentComplete) { - return $this->currentData; - } - - if ($this->resource instanceof mysqli_stmt) { - $this->loadDataFromMysqliStatement(); - return $this->currentData; - } else { - $this->loadFromMysqliResult(); - return $this->currentData; - } + return $this->generatedValue; } /** - * Mysqli's binding and returning of statement values - * - * Mysqli requires you to bind variables to the extension in order to - * get data out. These values have to be references: - * - * @see http://php.net/manual/en/mysqli-stmt.bind-result.php - * - * @throws Exception\RuntimeException + * {@inheritDoc} */ - protected function loadDataFromMysqliStatement(): bool + #[Override] + public function getResource(): mysqli|mysqli_result|mysqli_stmt { - // build the default reference based bind structure, if it does not already exist - if ($this->statementBindValues['keys'] === null) { - $this->statementBindValues['keys'] = []; - $resultResource = $this->resource->result_metadata(); - foreach ($resultResource->fetch_fields() as $col) { - $this->statementBindValues['keys'][] = $col->name; - } - $this->statementBindValues['values'] = array_fill(0, count($this->statementBindValues['keys']), null); - $refs = []; - foreach ($this->statementBindValues['values'] as $i => &$f) { - $refs[$i] = &$f; - } - call_user_func_array([$this->resource, 'bind_result'], $this->statementBindValues['values']); - } + return $this->resource; + } - if (($r = $this->resource->fetch()) === null) { - if (! $this->isBuffered) { - $this->resource->close(); - } - return false; - } elseif ($r === false) { - throw new Exception\RuntimeException($this->resource->error); - } + /** + * Initialize + * + * @throws Exception\InvalidArgumentException + * psalm-suppress PossiblyUnusedMethod + */ + public function initialize( + mysqli|mysqli_result|mysqli_stmt $resource, + string|int|false|null $generatedValue, + ?bool $isBuffered = null, + ): ResultInterface { + /** + * todo(@tyrsson): examine this closely to see if this is the correct behavior + */ + $this->isBuffered = match (true) { + null !== $isBuffered => $isBuffered, + $resource instanceof mysqli + || $resource instanceof mysqli_result + || 0 !== $resource->num_rows + => true, + default => $this->isBuffered, + }; - // dereference - for ($i = 0, $count = count($this->statementBindValues['keys']); $i < $count; $i++) { - $this->currentData[$this->statementBindValues['keys'][$i]] = $this->statementBindValues['values'][$i]; - } - $this->currentComplete = true; - $this->nextComplete = true; - $this->position++; - return true; + $this->resource = $resource; + $this->generatedValue = $generatedValue; + return $this; } /** - * Load from mysqli result + * {@inheritDoc} */ - protected function loadFromMysqliResult(): bool + #[Override] + public function isBuffered(): ?bool { - $this->currentData = null; + return $this->isBuffered; + } - if (($data = $this->resource->fetch_assoc()) === null) { - return false; - } + /** + * {@inheritDoc} + */ + #[Override] + public function isQueryResult(): bool + { + return $this->resource->field_count > 0; + } - $this->position++; - $this->currentData = $data; - $this->currentComplete = true; - $this->nextComplete = true; - $this->position++; - return true; + /** + * Key + * + * @return int + */ + #[ReturnTypeWillChange] + #[Override] + public function key() + { + return $this->position; } /** @@ -235,25 +218,13 @@ public function next() { $this->currentComplete = false; - if ($this->nextComplete === false) { + if (! $this->nextComplete) { $this->position++; } $this->nextComplete = false; } - /** - * Key - * - * @return mixed - */ - #[ReturnTypeWillChange] - #[Override] - public function key() - { - return $this->position; - } - /** * Rewind * @@ -264,10 +235,14 @@ public function key() #[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; @@ -276,6 +251,8 @@ public function rewind() /** * Valid * + * @throws Exception\ExceptionInterface + * * @return bool */ #[ReturnTypeWillChange] @@ -294,36 +271,91 @@ public function valid() } /** - * Count + * Mysqli's binding and returning of statement values + * + * Mysqli requires you to bind variables to the extension in order to + * get data out. These values have to be references: + * + * @see http://php.net/manual/en/mysqli-stmt.bind-result.php * * @throws Exception\RuntimeException - * @return int */ - #[ReturnTypeWillChange] - #[Override] - public function count() + protected function loadDataFromMysqliStatement(): bool { - if ($this->isBuffered === false) { - throw new Exception\RuntimeException('Row count is not available in unbuffered result sets.'); + if (! $this->resource instanceof mysqli_stmt) { + throw new Exception\RuntimeException('Expected resource to be an instance of mysqli_stmt'); } - return $this->resource->num_rows; - } - /** - * {@inheritDoc} - */ - #[Override] - public function getFieldCount(): int - { - return $this->resource->field_count; + // 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( + 0, + count($this->statementBindValues['keys']), + value: null, + ); + $refs = []; + foreach ($this->statementBindValues['values'] as $i => &$f) { + $refs[$i] = &$f; + } + call_user_func_array([$this->resource, 'bind_result'], $this->statementBindValues['values']); + } + + if (($r = $this->resource->fetch()) === null) { + if (! $this->isBuffered) { + $this->resource->close(); + } + return false; + } + + if (! $r) { + throw new Exception\RuntimeException($this->resource->error); + } + + // dereference + for ($i = 0, $count = count($this->statementBindValues['keys']); $i < $count; $i++) { + $this->currentData[$this->statementBindValues['keys'][$i]] = $this->statementBindValues['values'][$i]; + } + $this->currentComplete = true; + $this->nextComplete = true; + $this->position++; + return true; } /** - * Get generated value + * Load from mysqli result + * + * @throws Exception\RuntimeException */ - #[Override] - public function getGeneratedValue(): string|int|false|null + protected function loadFromMysqliResult(): bool { - return $this->generatedValue; + $this->currentData = 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; + } + + $this->position++; + $this->currentData = $data; + $this->currentComplete = true; + $this->nextComplete = true; + $this->position++; + return true; } } diff --git a/src/Sql/Ddl/AlterTableDecorator.php b/src/Sql/Ddl/AlterTableDecorator.php index 0dd89b6..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; @@ -20,9 +22,13 @@ use function substr_replace; use function uksort; +// @mago-expect lint:cyclomatic-complexity +// @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, @@ -54,46 +60,64 @@ final class AlterTableDecorator extends AlterTable implements PlatformDecoratorI 'after' => 8, ]; + #[Override] public function setSubject( - SqlInterface|PreparableSqlInterface|null $subject + SqlInterface|PreparableSqlInterface|null $subject, ): PlatformDecoratorInterface { $this->subject = $subject; return $this; } + /** + * @return array{0: int, 1: int, 2: int, 3: int} + */ protected function getSqlInsertOffsets(string $sql): array { $sqlLength = strlen($sql); $insertStart = []; foreach (['NOT NULL', 'NULL', 'DEFAULT', 'UNIQUE', 'PRIMARY', 'REFERENCES'] as $needle) { - $insertPos = strpos($sql, ' ' . $needle); + $insertPos = strpos($sql, " {$needle}"); - if ($insertPos !== false) { + if (false !== $insertPos) { switch ($needle) { case 'REFERENCES': - $insertStart[2] = ! isset($insertStart[2]) ? $insertPos : $insertStart[2]; + $insertStart[2] ??= $insertPos; // no break case 'PRIMARY': case 'UNIQUE': - $insertStart[1] = ! isset($insertStart[1]) ? $insertPos : $insertStart[1]; + $insertStart[1] ??= $insertPos; // no break default: - $insertStart[0] = ! isset($insertStart[0]) ? $insertPos : $insertStart[0]; + $insertStart[0] ??= $insertPos; } } } - foreach (range(0, 3) as $i) { - $insertStart[$i] = $insertStart[$i] ?? $sqlLength; + foreach (range( + start: 0, + end: 3, + ) as $i) { + $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) { @@ -120,11 +144,11 @@ protected function processAddColumns(?PlatformInterface $adapterPlatform = null) $j = 0; break; case 'charset': - $insert = ' CHARACTER SET ' . $coValue; + $insert = " CHARACTER SET {$coValue}"; $j = 0; break; case 'collate': - $insert = ' COLLATE ' . $coValue; + $insert = " COLLATE {$coValue}"; $j = 0; break; case 'identity': @@ -134,7 +158,7 @@ protected function processAddColumns(?PlatformInterface $adapterPlatform = null) $j = 1; break; case 'comment': - $insert = ' COMMENT ' . $adapterPlatform->quoteValue($coValue); + $insert = " COMMENT {$adapterPlatform->quoteValue($coValue)}"; $j = 2; break; case 'columnformat': @@ -147,13 +171,12 @@ protected function processAddColumns(?PlatformInterface $adapterPlatform = null) $j = 2; break; case 'after': - $insert = ' AFTER ' . $adapterPlatform->quoteIdentifier($coValue); + $insert = " AFTER {$adapterPlatform->quoteIdentifier($coValue)}"; $j = 2; } if ($insert) { - $j = $j ?? 0; - $sql = substr_replace($sql, $insert, $insertStart[$j], 0); + $sql = substr_replace($sql, $insert, $insertStart[$j], length: 0); $insertStartCount = count($insertStart); for (; $j < $insertStartCount; ++$j) { $insertStart[$j] += strlen($insert); @@ -165,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); @@ -192,11 +225,11 @@ protected function processChangeColumns(?PlatformInterface $adapterPlatform = nu $j = 0; break; case 'charset': - $insert = ' CHARACTER SET ' . $coValue; + $insert = " CHARACTER SET {$coValue}"; $j = 0; break; case 'collate': - $insert = ' COLLATE ' . $coValue; + $insert = " COLLATE {$coValue}"; $j = 0; break; case 'identity': @@ -206,7 +239,7 @@ protected function processChangeColumns(?PlatformInterface $adapterPlatform = nu $j = 1; break; case 'comment': - $insert = ' COMMENT ' . $adapterPlatform->quoteValue($coValue); + $insert = " COMMENT {$adapterPlatform->quoteValue($coValue)}"; $j = 2; break; case 'columnformat': @@ -221,8 +254,7 @@ protected function processChangeColumns(?PlatformInterface $adapterPlatform = nu } if ($insert) { - $j = $j ?? 0; - $sql = substr_replace($sql, $insert, $insertStart[$j], 0); + $sql = substr_replace($sql, $insert, $insertStart[$j], length: 0); $insertStartCount = count($insertStart); for (; $j < $insertStartCount; ++$j) { $insertStart[$j] += strlen($insert); @@ -238,22 +270,8 @@ protected function processChangeColumns(?PlatformInterface $adapterPlatform = nu return [$sqls]; } - /** - * @param string $name - * @return string - */ - private function normalizeColumnOption($name) - { - return strtolower(str_replace(['-', '_', ' '], '', $name)); - } - - /** - * @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); @@ -263,4 +281,9 @@ private function compareColumnOptions($columnA, $columnB) return $columnA - $columnB; } + + 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 fd1f575..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; @@ -20,12 +22,15 @@ use function substr_replace; use function uksort; +// @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, @@ -39,8 +44,9 @@ final class CreateTableDecorator extends CreateTable implements PlatformDecorato 'storage' => 7, ]; + #[Override] public function setSubject( - PreparableSqlInterface|SqlInterface|null $subject + PreparableSqlInterface|SqlInterface|null $subject, ): PlatformDecoratorInterface { $this->subject = $subject; @@ -48,52 +54,62 @@ 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 = []; foreach (['NOT NULL', 'NULL', 'DEFAULT', 'UNIQUE', 'PRIMARY', 'REFERENCES'] as $needle) { - $insertPos = strpos($sql, ' ' . $needle); + $insertPos = strpos($sql, " {$needle}"); - if ($insertPos !== false) { + if (false !== $insertPos) { switch ($needle) { case 'REFERENCES': - $insertStart[2] = ! isset($insertStart[2]) ? $insertPos : $insertStart[2]; - // no break + $insertStart[2] ??= $insertPos; + // no break case 'PRIMARY': case 'UNIQUE': - $insertStart[1] = ! isset($insertStart[1]) ? $insertPos : $insertStart[1]; - // no break + $insertStart[1] ??= $insertPos; + // no break default: - $insertStart[0] = ! isset($insertStart[0]) ? $insertPos : $insertStart[0]; + $insertStart[0] ??= $insertPos; } } } - foreach (range(0, 3) as $i) { - $insertStart[$i] = $insertStart[$i] ?? $sqlLength; + foreach (range( + start: 0, + end: 3, + ) as $i) { + $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(); @@ -116,11 +132,11 @@ protected function processColumns(?PlatformInterface $platform = null): ?array $j = 0; break; case 'charset': - $insert = ' CHARACTER SET ' . $coValue; + $insert = " CHARACTER SET {$coValue}"; $j = 0; break; case 'collate': - $insert = ' COLLATE ' . $coValue; + $insert = " COLLATE {$coValue}"; $j = 0; break; case 'identity': @@ -130,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': @@ -145,8 +161,7 @@ protected function processColumns(?PlatformInterface $platform = null): ?array } if ($insert) { - $j = $j ?? 0; - $sql = substr_replace($sql, $insert, $insertStart[$j], 0); + $sql = substr_replace($sql, $insert, $insertStart[$j], length: 0); $insertStartCount = count($insertStart); for (; $j < $insertStartCount; ++$j) { $insertStart[$j] += strlen($insert); @@ -160,22 +175,8 @@ protected function processColumns(?PlatformInterface $platform = null): ?array return [$sqls]; } - /** - * @param string $name - * @return string - */ - private function normalizeColumnOption($name) - { - return strtolower(str_replace(['-', '_', ' '], '', $name)); - } - - /** - * @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); @@ -185,4 +186,9 @@ private function compareColumnOptions($columnA, $columnB) return $columnA - $columnB; } + + 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 6809f5b..4f05437 100644 --- a/src/Sql/SelectDecorator.php +++ b/src/Sql/SelectDecorator.php @@ -15,11 +15,11 @@ final class SelectDecorator extends Select implements PlatformDecoratorInterface { - protected SqlInterface|PreparableSqlInterface|null $subject; + protected SqlInterface|PreparableSqlInterface|null $subject = null; #[Override] public function setSubject( - SqlInterface|PreparableSqlInterface|null $subject + SqlInterface|PreparableSqlInterface|null $subject, ): PlatformDecoratorInterface { $this->subject = $subject; return $this; @@ -29,7 +29,7 @@ public function setSubject( protected function localizeVariables(): void { parent::localizeVariables(); - if ($this->limit === null && $this->offset !== null) { + if (null === $this->limit && null !== $this->offset) { $this->specifications[self::LIMIT] = 'LIMIT 18446744073709551615'; } } @@ -39,36 +39,37 @@ protected function localizeVariables(): void protected function processLimit( PlatformInterface $platform, ?DriverInterface $driver = null, - ?ParameterContainer $parameterContainer = null + ?ParameterContainer $parameterContainer = null, ): ?array { - if ($this->limit === null && $this->offset !== null) { + if (null === $this->limit && null !== $this->offset) { return ['']; } - if ($this->limit === null) { + if (null === $this->limit) { return null; } if ($parameterContainer) { $paramPrefix = $this->processInfo['paramPrefix']; - $parameterContainer->offsetSet($paramPrefix . 'limit', $this->limit, ParameterContainer::TYPE_INTEGER); - return [$driver->formatParameterName($paramPrefix . 'limit')]; + $parameterContainer->offsetSet("{$paramPrefix}limit", $this->limit, ParameterContainer::TYPE_INTEGER); + return [$driver->formatParameterName("{$paramPrefix}limit")]; } return [$this->limit]; } + /** @return string[]|null */ #[Override] protected function processOffset( PlatformInterface $platform, ?DriverInterface $driver = null, - ?ParameterContainer $parameterContainer = null + ?ParameterContainer $parameterContainer = null, ): ?array { - if ($this->offset === null) { + if (null === $this->offset) { return null; } if ($parameterContainer) { $paramPrefix = $this->processInfo['paramPrefix']; - $parameterContainer->offsetSet($paramPrefix . 'offset', $this->offset, ParameterContainer::TYPE_INTEGER); - return [$driver->formatParameterName($paramPrefix . 'offset')]; + $parameterContainer->offsetSet("{$paramPrefix}offset", $this->offset, ParameterContainer::TYPE_INTEGER); + return [$driver->formatParameterName("{$paramPrefix}offset")]; } return [$this->offset]; diff --git a/src/Statement.php b/src/Statement.php index 945d8d2..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 { @@ -37,76 +38,93 @@ final class Statement implements StatementInterface, DriverAwareInterface, Profi public function __construct( protected ParameterContainer $parameterContainer = new ParameterContainer(), - protected bool $bufferResults = false - ) { - } + protected bool $bufferResults = false, + ) {} + /** + * Execute + * + * @param array|ParameterContainer|null $parameters + * + * @throws Exception\ExceptionInterface + */ #[Override] - public function setDriver(DriverInterface $driver): DriverAwareInterface + public function execute(ParameterContainer|array|null $parameters = null): ?ResultInterface { - $this->driver = $driver; - return $this; - } + if (! $this->isPrepared) { + $this->prepare(); + } - #[Override] - public function setProfiler(ProfilerInterface $profiler): ProfilerAwareInterface - { - $this->profiler = $profiler; - return $this; - } + /** START Standard ParameterContainer Merging Block */ + if ($parameters instanceof ParameterContainer) { + $this->parameterContainer = $parameters; + } - public function getProfiler(): ?ProfilerInterface - { - return $this->profiler; - } + if (is_array($parameters)) { + $this->parameterContainer->setFromArray($parameters); + } - public function initialize(mysqli $mysqli): static - { - $this->mysqli = $mysqli; - return $this; - } + if ($this->parameterContainer->count() > 0) { + $this->bindParametersFromContainer(); + } + /** END Standard ParameterContainer Merging Block */ - #[Override] - public function getSql(): ?string - { - return $this->sql; + $this->profiler?->profilerStart($this); + + $return = $this->resource->execute(); + + $this->profiler?->profilerFinish(); + + if (! $return) { + throw new Exception\RuntimeException($this->resource->error); + } + + $buffered = false; + if ($this->bufferResults) { + $this->resource->store_result(); + $this->isPrepared = false; + $buffered = true; + } + + return $this->driver->createResult($this->resource, $buffered); } #[Override] - public function setSql(?string $sql): StatementContainerInterface + public function getParameterContainer(): ?ParameterContainer { - $this->sql = $sql; - return $this; + return $this->parameterContainer; } - #[Override] - public function setParameterContainer( - ParameterContainer $parameterContainer - ): StatementContainerInterface { - $this->parameterContainer = $parameterContainer; - return $this; + public function getProfiler(): ?ProfilerInterface + { + return $this->profiler; } /** * @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 { return $this->resource; } - public function setResource(mysqli_stmt $mysqliStatement): StatementInterface + #[Override] + public function getSql(): ?string { - $this->resource = $mysqliStatement; - $this->isPrepared = true; - return $this; + return $this->sql; } - #[Override] - public function getParameterContainer(): ?ParameterContainer + public function initialize(mysqli $mysqli): static { - return $this->parameterContainer; + $this->mysqli = $mysqli; + return $this; } #[Override] @@ -115,6 +133,9 @@ public function isPrepared(): bool return $this->isPrepared; } + /** + * @throws Exception\ExceptionInterface + */ #[Override] public function prepare(?string $sql = null): StatementInterface { @@ -122,71 +143,60 @@ public function prepare(?string $sql = null): StatementInterface throw new Exception\RuntimeException('This statement has already been prepared'); } - $sql = $sql ?: $this->sql; + $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, + "Statement couldn't be produced with sql: {$sql}", $this->mysqli->errno, - new Exception\ErrorException($this->mysqli->error, $this->mysqli->errno) + new Exception\ErrorException($this->mysqli->error, $this->mysqli->errno), ); } + $this->resource = $resource; $this->isPrepared = true; return $this; } - /** - * Execute - * - * @throws Exception\RuntimeException - */ #[Override] - public function execute(ParameterContainer|array|null $parameters = null): ?ResultInterface + public function setDriver(DriverInterface $driver): DriverAwareInterface { - if (! $this->isPrepared) { - $this->prepare(); - } - - /** START Standard ParameterContainer Merging Block */ - if (! $this->parameterContainer instanceof ParameterContainer) { - if ($parameters instanceof ParameterContainer) { - $this->parameterContainer = $parameters; - $parameters = null; - } else { - $this->parameterContainer = new ParameterContainer(); - } - } - - if (is_array($parameters)) { - $this->parameterContainer->setFromArray($parameters); + if (! $driver instanceof Driver) { + throw new Exception\InvalidArgumentException(sprintf('Driver must be an instance of %s', Driver::class)); } - if ($this->parameterContainer->count() > 0) { - $this->bindParametersFromContainer(); - } - /** END Standard ParameterContainer Merging Block */ - - $this->profiler?->profilerStart($this); - - $return = $this->resource->execute(); + $this->driver = $driver; + return $this; + } - $this->profiler?->profilerFinish(); + #[Override] + public function setParameterContainer( + ParameterContainer $parameterContainer, + ): StatementContainerInterface { + $this->parameterContainer = $parameterContainer; + return $this; + } - if ($return === false) { - throw new Exception\RuntimeException($this->resource->error); - } + #[Override] + public function setProfiler(ProfilerInterface $profiler): ProfilerAwareInterface + { + $this->profiler = $profiler; + return $this; + } - if ($this->bufferResults === true) { - $this->resource->store_result(); - $this->isPrepared = false; - $buffered = true; - } else { - $buffered = false; - } + public function setResource(mysqli_stmt $mysqliStatement): StatementInterface + { + $this->resource = $mysqliStatement; + $this->isPrepared = true; + return $this; + } - return $this->driver->createResult($this->resource, $buffered); + #[Override] + public function setSql(?string $sql): StatementContainerInterface + { + $this->sql = $sql ?? ''; + return $this; } /** @@ -214,8 +224,6 @@ protected function bindParametersFromContainer(): void $type .= 's'; break; } - } else { - $type .= 's'; } $args[] = &$value; } diff --git a/test/integration/AdapterPlatformTest.php b/test/integration/AdapterPlatformTest.php index 757785f..a66fdb7 100644 --- a/test/integration/AdapterPlatformTest.php +++ b/test/integration/AdapterPlatformTest.php @@ -11,6 +11,7 @@ use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[Group('integration')] @@ -32,25 +33,27 @@ public static function quoteValueProvider(): array ]; } + #[Test] #[DataProvider('quoteValueProvider')] - public function testQuoteValueWithMysqli(string $input, string $expected): void + public function quoteValueWithMysqli(string $input, string $expected): void { $this->driver = Driver::class; $adapter = $this->getAdapter(); $platform = new AdapterPlatform($adapter->getDriver()); $value = $platform->quoteValue($input); - self::assertSame($expected, $value); + static::assertSame($expected, $value); } + #[Test] #[DataProvider('quoteValueProvider')] - public function testQuoteValueWithPdoMysql(string $input, string $expected): void + public function quoteValueWithPdoMysql(string $input, string $expected): void { $this->driver = PdoDriver::class; $adapter = $this->getAdapter(); $platform = new AdapterPlatform($adapter->getDriver()); $value = $platform->quoteValue($input); - self::assertSame($expected, $value); + static::assertSame($expected, $value); } } diff --git a/test/integration/ConnectionTest.php b/test/integration/ConnectionTest.php index 4318dd4..b97bfe4 100644 --- a/test/integration/ConnectionTest.php +++ b/test/integration/ConnectionTest.php @@ -4,10 +4,12 @@ namespace PhpDbIntegrationTest\Mysql; +use PhpDb\Adapter\AdapterInterface; use PhpDb\Mysql\Connection; use PhpDbIntegrationTest\Mysql\Container\TestAsset\SetupTrait; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[Group('integration')] @@ -19,15 +21,16 @@ final class ConnectionTest extends TestCase { use SetupTrait; - public function testConnectionOk(): void + #[Test] + public function connectionOk(): void { /** @var array $config */ - $config = ['db' => ['driver' => 'Mysqli']]; + $config = [AdapterInterface::class => ['driver' => 'Mysqli']]; /** @var Connection $connection */ $connection = $this->getAdapter($config)->getDriver()->getConnection(); $connection->connect(); - self::assertTrue($connection->isConnected()); + static::assertTrue($connection->isConnected()); $connection->disconnect(); - self::assertFalse($connection->isConnected()); + static::assertFalse($connection->isConnected()); } } diff --git a/test/integration/Container/ConnectionInterfaceFactoryTest.php b/test/integration/Container/ConnectionInterfaceFactoryTest.php index 1063167..a90e024 100644 --- a/test/integration/Container/ConnectionInterfaceFactoryTest.php +++ b/test/integration/Container/ConnectionInterfaceFactoryTest.php @@ -10,6 +10,7 @@ use PhpDb\Mysql\Connection; use PhpDb\Mysql\Container\ConnectionInterfaceFactory; use PHPUnit\Framework\Attributes; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[Attributes\CoversClass(ConnectionInterfaceFactory::class)] @@ -21,27 +22,29 @@ final class ConnectionInterfaceFactoryTest extends TestCase { use TestAsset\SetupTrait; - public function testInvokeReturnsMysqliConnection(): void + #[Test] + public function invokeReturnsMysqliConnection(): void { $factory = new ConnectionInterfaceFactory(); $connection = $factory( $this->container, Connection::class, - $this->config[AdapterInterface::class] + $this->config[AdapterInterface::class], ); - self::assertInstanceOf(ConnectionInterface::class, $connection); - self::assertInstanceOf(Connection::class, $connection); + static::assertInstanceOf(ConnectionInterface::class, $connection); + static::assertInstanceOf(Connection::class, $connection); } - public function testInvokeThrowsExceptionWithoutConnectionConfig(): void + #[Test] + public function invokeThrowsExceptionWithoutConnectionConfig(): void { $this->expectException(InvalidConnectionParametersException::class); $factory = new ConnectionInterfaceFactory(); $factory( $this->container, - Connection::class + Connection::class, ); } } diff --git a/test/integration/Container/DriverInterfaceFactoryTest.php b/test/integration/Container/DriverInterfaceFactoryTest.php index ad4ed44..d491464 100644 --- a/test/integration/Container/DriverInterfaceFactoryTest.php +++ b/test/integration/Container/DriverInterfaceFactoryTest.php @@ -4,13 +4,17 @@ namespace PhpDbIntegrationTest\Mysql\Container; +use Laminas\ServiceManager\ServiceManager; use PhpDb\Adapter\AdapterInterface; use PhpDb\Adapter\Driver\DriverInterface; +use PhpDb\Adapter\Driver\ResultInterface; use PhpDb\Exception\ContainerException; use PhpDb\Mysql\Connection; use PhpDb\Mysql\Container\DriverInterfaceFactory; use PhpDb\Mysql\Driver; +use PhpDb\Mysql\Result; use PHPUnit\Framework\Attributes; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[Attributes\CoversClass(DriverInterfaceFactory::class)] @@ -22,26 +26,45 @@ final class DriverInterfaceFactoryTest extends TestCase { use TestAsset\SetupTrait; - public function testFactoryReturnsMysqliDriver(): void + #[Test] + public function factoryReturnsMysqliDriver(): void { $factory = new DriverInterfaceFactory(); $driver = $factory( $this->container, DriverInterface::class, - $this->config[AdapterInterface::class] + $this->config[AdapterInterface::class], ); - self::assertInstanceOf(DriverInterface::class, $driver); - $this->assertInstanceOf(Driver::class, $driver); + static::assertInstanceOf(DriverInterface::class, $driver); + static::assertInstanceOf(Driver::class, $driver); } - public function testInvokeThrowsExceptionWithoutConnectionConfig(): void + #[Test] + public function invokeThrowsExceptionWithoutConnectionConfig(): void { $this->expectException(ContainerException::class); $factory = new DriverInterfaceFactory(); $factory( $this->container, - Connection::class + Connection::class, ); } + + #[Test] + public function invokeUsesRegisteredResultInterface(): void + { + /** @var ServiceManager $container */ + $container = $this->container; + $container->setService(ResultInterface::class, new Result()); + + $factory = new DriverInterfaceFactory(); + $driver = $factory( + $container, + DriverInterface::class, + $this->config[AdapterInterface::class], + ); + + static::assertInstanceOf(Driver::class, $driver); + } } diff --git a/test/integration/Container/MetadataInterfaceFactoryTest.php b/test/integration/Container/MetadataInterfaceFactoryTest.php index 378dc0b..a54ab3e 100644 --- a/test/integration/Container/MetadataInterfaceFactoryTest.php +++ b/test/integration/Container/MetadataInterfaceFactoryTest.php @@ -10,6 +10,7 @@ use PhpDbIntegrationTest\Mysql\Container\TestAsset\SetupTrait; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[CoversClass(MetadataInterfaceFactory::class)] @@ -18,11 +19,12 @@ final class MetadataInterfaceFactoryTest extends TestCase { use SetupTrait; - public function testFactoryReturnsMysqlMetadata(): void + #[Test] + public function factoryReturnsMysqlMetadata(): void { $factory = new MetadataInterfaceFactory(); $metadata = $factory($this->container, MetadataInterface::class); - self::assertInstanceOf(MetadataInterface::class, $metadata); - self::assertInstanceOf(Source::class, $metadata); + static::assertInstanceOf(MetadataInterface::class, $metadata); + static::assertInstanceOf(Source::class, $metadata); } } diff --git a/test/integration/Container/PdoConnectionInterfaceFactoryTest.php b/test/integration/Container/PdoConnectionInterfaceFactoryTest.php index fd7a4b6..e1df53d 100644 --- a/test/integration/Container/PdoConnectionInterfaceFactoryTest.php +++ b/test/integration/Container/PdoConnectionInterfaceFactoryTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[Group('container')] @@ -23,27 +24,29 @@ final class PdoConnectionInterfaceFactoryTest extends TestCase { use TestAsset\SetupTrait; - public function testInvokeReturnsPdoConnection(): void + #[Test] + public function invokeReturnsPdoConnection(): void { $factory = new PdoConnectionInterfaceFactory(); $instance = $factory( $this->container, PdoConnectionInterface::class, - $this->config[AdapterInterface::class] + $this->config[AdapterInterface::class], ); - self::assertInstanceOf(ConnectionInterface::class, $instance); - self::assertInstanceOf(PdoConnectionInterface::class, $instance); - self::assertInstanceOf(Connection::class, $instance); + static::assertInstanceOf(ConnectionInterface::class, $instance); + static::assertInstanceOf(PdoConnectionInterface::class, $instance); + static::assertInstanceOf(Connection::class, $instance); } - public function testInvokeThrowsExceptionWithoutConnectionConfig(): void + #[Test] + public function invokeThrowsExceptionWithoutConnectionConfig(): void { $this->expectException(InvalidConnectionParametersException::class); $factory = new PdoConnectionInterfaceFactory(); $factory( $this->container, - PdoConnectionInterface::class + PdoConnectionInterface::class, ); } } diff --git a/test/integration/Container/PdoDriverInterfaceFactoryTest.php b/test/integration/Container/PdoDriverInterfaceFactoryTest.php index 1e5f3e6..842532c 100644 --- a/test/integration/Container/PdoDriverInterfaceFactoryTest.php +++ b/test/integration/Container/PdoDriverInterfaceFactoryTest.php @@ -4,13 +4,18 @@ namespace PhpDbIntegrationTest\Mysql\Container; +use Laminas\ServiceManager\ServiceManager; use PhpDb\Adapter\AdapterInterface; +use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\PdoDriverInterface; +use PhpDb\Adapter\Driver\ResultInterface; +use PhpDb\Exception\ContainerException; use PhpDb\Mysql\Container\PdoDriverInterfaceFactory; use PhpDb\Mysql\Pdo\Driver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[Group('container')] @@ -21,16 +26,43 @@ final class PdoDriverInterfaceFactoryTest extends TestCase { use TestAsset\SetupTrait; - public function testInvokeReturnsPdoDriver(): void + #[Test] + public function invokeReturnsPdoDriver(): void { $factory = new PdoDriverInterfaceFactory(); $instance = $factory( $this->container, PdoDriverInterface::class, - $this->config[AdapterInterface::class] + $this->config[AdapterInterface::class], ); - self::assertInstanceOf(PdoDriverInterface::class, $instance); - self::assertInstanceOf(Driver::class, $instance); + static::assertInstanceOf(PdoDriverInterface::class, $instance); + static::assertInstanceOf(Driver::class, $instance); + } + + #[Test] + public function invokeThrowsWhenOptionsMissingConnection(): void + { + $factory = new PdoDriverInterfaceFactory(); + + $this->expectException(ContainerException::class); + $factory($this->container, PdoDriverInterface::class, options: null); + } + + #[Test] + public function invokeUsesRegisteredResultInterface(): void + { + /** @var ServiceManager $container */ + $container = $this->container; + $container->setService(ResultInterface::class, new Result()); + + $factory = new PdoDriverInterfaceFactory(); + $instance = $factory( + $container, + PdoDriverInterface::class, + $this->config[AdapterInterface::class], + ); + + static::assertInstanceOf(Driver::class, $instance); } } diff --git a/test/integration/Container/PdoStatementFactoryTest.php b/test/integration/Container/PdoStatementFactoryTest.php index ca6d9e3..08139ee 100644 --- a/test/integration/Container/PdoStatementFactoryTest.php +++ b/test/integration/Container/PdoStatementFactoryTest.php @@ -11,6 +11,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[Group('container')] @@ -21,15 +22,16 @@ final class PdoStatementFactoryTest extends TestCase { use TestAsset\SetupTrait; - public function testInvokeReturnsPdoStatement(): void + #[Test] + public function invokeReturnsPdoStatement(): void { $factory = new PdoStatementFactory(); $statement = $factory( $this->container, StatementInterface::class, - $this->config[AdapterInterface::class] + $this->config[AdapterInterface::class], ); - self::assertInstanceOf(StatementInterface::class, $statement); - self::assertInstanceOf(Statement::class, $statement); + static::assertInstanceOf(StatementInterface::class, $statement); + static::assertInstanceOf(Statement::class, $statement); } } diff --git a/test/integration/Container/PlatformInterfaceFactoryTest.php b/test/integration/Container/PlatformInterfaceFactoryTest.php index 55742b1..06a4fe8 100644 --- a/test/integration/Container/PlatformInterfaceFactoryTest.php +++ b/test/integration/Container/PlatformInterfaceFactoryTest.php @@ -6,12 +6,14 @@ use PhpDb\Adapter\AdapterInterface; use PhpDb\Adapter\Platform\PlatformInterface; +use PhpDb\Exception\ContainerException; use PhpDb\Mysql\AdapterPlatform; use PhpDb\Mysql\Container\PlatformInterfaceFactory; use PhpDb\Mysql\Pdo\Driver as PdoDriver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[Group('integration')] @@ -22,7 +24,8 @@ final class PlatformInterfaceFactoryTest extends TestCase { use TestAsset\SetupTrait; - public function testInvokeReturnsPlatformInterfaceWhenDbDriverIsPdo(): void + #[Test] + public function invokeReturnsPlatformInterfaceWhenDbDriverIsPdo(): void { $adapter = $this->getAdapter(['driver' => PdoDriver::class]); @@ -32,10 +35,19 @@ public function testInvokeReturnsPlatformInterfaceWhenDbDriverIsPdo(): void $instance = $factory( $this->container, PlatformInterface::class, - $this->config[AdapterInterface::class] + $this->config[AdapterInterface::class], ); - self::assertInstanceOf(PlatformInterface::class, $instance); - self::assertInstanceOf(AdapterPlatform::class, $instance); + static::assertInstanceOf(PlatformInterface::class, $instance); + static::assertInstanceOf(AdapterPlatform::class, $instance); + } + + #[Test] + public function invokeThrowsForInvalidDriver(): void + { + $factory = new PlatformInterfaceFactory(); + + $this->expectException(ContainerException::class); + $factory($this->container, PlatformInterface::class, ['driver' => 'not-a-driver']); } } diff --git a/test/integration/Container/StatementInterfaceFactoryTest.php b/test/integration/Container/StatementInterfaceFactoryTest.php index b1072c7..671e0e9 100644 --- a/test/integration/Container/StatementInterfaceFactoryTest.php +++ b/test/integration/Container/StatementInterfaceFactoryTest.php @@ -9,6 +9,7 @@ use PhpDb\Mysql\Container\StatementInterfaceFactory; use PhpDb\Mysql\Statement; use PHPUnit\Framework\Attributes; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[Attributes\CoversClass(StatementInterfaceFactory::class)] @@ -20,10 +21,11 @@ final class StatementInterfaceFactoryTest extends TestCase { use TestAsset\SetupTrait; - public function testInvokeReturnsMysqliStatement(): void + #[Test] + public function invokeReturnsMysqliStatement(): void { $this->getAdapter([ - 'db' => [ + AdapterInterface::class => [ 'driver' => 'Mysqli', 'options' => [ 'buffer_results' => false, @@ -35,10 +37,10 @@ public function testInvokeReturnsMysqliStatement(): void $statement = $factory( $this->container, StatementInterface::class, - $this->config[AdapterInterface::class] + $this->config[AdapterInterface::class], ); - self::assertInstanceOf(StatementInterface::class, $statement); - self::assertInstanceOf(Statement::class, $statement); + static::assertInstanceOf(StatementInterface::class, $statement); + static::assertInstanceOf(Statement::class, $statement); } } diff --git a/test/integration/Container/TestAsset/SetupTrait.php b/test/integration/Container/TestAsset/SetupTrait.php index a9bb19f..9574007 100644 --- a/test/integration/Container/TestAsset/SetupTrait.php +++ b/test/integration/Container/TestAsset/SetupTrait.php @@ -33,23 +33,20 @@ trait SetupTrait protected DriverInterface|string|null $driver = null; - protected function setUp(): void - { - $this->getAdapter(); - parent::setUp(); - } - protected function getAdapter(array $config = []): AdapterInterface { + $hostname = (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_HOSTNAME'); + $port = (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_PORT'); + $connectionConfig = [ AdapterInterface::class => [ 'driver' => $this->driver ?? Driver::class, 'connection' => [ - 'hostname' => (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_HOSTNAME') ?: 'localhost', + 'hostname' => '' === $hostname ? 'localhost' : $hostname, 'username' => (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_USERNAME'), 'password' => (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_PASSWORD'), 'database' => (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_DATABASE'), - 'port' => (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_PORT') ?: '3306', + 'port' => '' === $port ? '3306' : $port, 'charset' => 'utf8', 'driver_options' => [], ], @@ -62,16 +59,16 @@ protected function getAdapter(array $config = []): AdapterInterface // merge service config from both PhpDb and PhpDb\Adapter\Mysql $serviceManagerConfig = ArrayUtils::merge( (new LaminasDbConfigProvider())()['dependencies'], - (new ConfigProvider())()['dependencies'] + (new ConfigProvider())()['dependencies'], ); $serviceManagerConfig = ArrayUtils::merge( $serviceManagerConfig, - $connectionConfig + $connectionConfig, ); // prefer passed config over environment variables - if ($config !== []) { + if ([] !== $config) { $serviceManagerConfig = ArrayUtils::merge($serviceManagerConfig, $config); } @@ -81,7 +78,7 @@ protected function getAdapter(array $config = []): AdapterInterface 'services' => [ 'config' => $serviceManagerConfig, ], - ] + ], ); $this->config = $serviceManagerConfig; @@ -100,4 +97,10 @@ protected function getHostname(): string { return $this->getConfig()[AdapterInterface::class]['connection']['hostname']; } + + protected function setUp(): void + { + $this->getAdapter(); + parent::setUp(); + } } diff --git a/test/integration/Extension/IntegrationTestStartedListener.php b/test/integration/Extension/IntegrationTestStartedListener.php index 615816d..8034a8d 100644 --- a/test/integration/Extension/IntegrationTestStartedListener.php +++ b/test/integration/Extension/IntegrationTestStartedListener.php @@ -31,7 +31,7 @@ public function notify(Started $event): void $this->fixtureLoaders[] = new MysqlFixtureLoader(); } - if (empty($this->fixtureLoaders)) { + if ([] === $this->fixtureLoaders) { return; } diff --git a/test/integration/Extension/IntegrationTestStoppedListener.php b/test/integration/Extension/IntegrationTestStoppedListener.php index 135e69a..7e6d6f3 100644 --- a/test/integration/Extension/IntegrationTestStoppedListener.php +++ b/test/integration/Extension/IntegrationTestStoppedListener.php @@ -19,7 +19,7 @@ public function notify(Finished $event): void { if ( $event->testSuite()->name() !== 'integration test' - || empty($this->fixtureLoaders) + || [] === $this->fixtureLoaders ) { return; } diff --git a/test/integration/Extension/ListenerExtension.php b/test/integration/Extension/ListenerExtension.php index 2357037..80dc5c0 100644 --- a/test/integration/Extension/ListenerExtension.php +++ b/test/integration/Extension/ListenerExtension.php @@ -14,7 +14,7 @@ final class ListenerExtension implements Extension public function bootstrap( Configuration $configuration, Facade $facade, - ParameterCollection $parameters + ParameterCollection $parameters, ): void { $facade->registerSubscribers( new IntegrationTestStartedListener(), diff --git a/test/integration/FixtureLoader/MysqlFixtureLoader.php b/test/integration/FixtureLoader/MysqlFixtureLoader.php index 8f3e7bb..ec1b9ac 100644 --- a/test/integration/FixtureLoader/MysqlFixtureLoader.php +++ b/test/integration/FixtureLoader/MysqlFixtureLoader.php @@ -27,14 +27,14 @@ public function createDatabase(): void if ( false === $this->pdo->exec(sprintf( - "CREATE DATABASE IF NOT EXISTS %s", - getenv('TESTS_PHPDB_ADAPTER_MYSQL_DATABASE') + 'CREATE DATABASE IF NOT EXISTS %s', + getenv('TESTS_PHPDB_ADAPTER_MYSQL_DATABASE'), )) ) { throw new Exception(sprintf( - "I cannot create the MySQL %s test database: %s", + 'I cannot create the MySQL %s test database: %s', getenv('TESTS_PHPDB_ADAPTER_MYSQL_DATABASE'), - print_r($this->pdo->errorInfo(), true) + print_r($this->pdo->errorInfo(), return: true), )); } @@ -42,10 +42,10 @@ public function createDatabase(): void if (false === $this->pdo->exec(file_get_contents($this->fixtureFile))) { throw new Exception(sprintf( - "I cannot create the table for %s database. Check the %s file. %s ", + 'I cannot create the table for %s database. Check the %s file. %s ', getenv('TESTS_PHPDB_ADAPTER_MYSQL_DATABASE'), $this->fixtureFile, - print_r($this->pdo->errorInfo(), true) + print_r($this->pdo->errorInfo(), return: true), )); } @@ -57,8 +57,8 @@ public function dropDatabase(): void $this->connect(); $this->pdo->exec(sprintf( - "DROP DATABASE IF EXISTS %s", - getenv('TESTS_PHPDB_ADAPTER_MYSQL_DATABASE') + 'DROP DATABASE IF EXISTS %s', + getenv('TESTS_PHPDB_ADAPTER_MYSQL_DATABASE'), )); $this->disconnect(); @@ -74,7 +74,7 @@ protected function connect(): void $this->pdo = new PDO( $dsn, getenv('TESTS_PHPDB_ADAPTER_MYSQL_USERNAME'), - getenv('TESTS_PHPDB_ADAPTER_MYSQL_PASSWORD') + getenv('TESTS_PHPDB_ADAPTER_MYSQL_PASSWORD'), ); } diff --git a/test/integration/Metadata/SourceTest.php b/test/integration/Metadata/SourceTest.php new file mode 100644 index 0000000..18c0115 --- /dev/null +++ b/test/integration/Metadata/SourceTest.php @@ -0,0 +1,200 @@ +source->getColumnNames('test'), + ); + } + + #[Test] + public function getColumnParsesEnumErratas(): void + { + $column = $this->source->getColumn('status', 'test_enum'); + + static::assertInstanceOf(ColumnObject::class, $column); + static::assertSame('enum', $column->getDataType()); + static::assertSame(['active', 'inactive'], $column->getErrata('permitted_values')); + } + + #[Test] + public function getColumnReturnsTypedColumn(): void + { + $column = $this->source->getColumn('id', 'test'); + + static::assertInstanceOf(ColumnObject::class, $column); + static::assertSame('id', $column->getName()); + static::assertSame('int', $column->getDataType()); + static::assertFalse($column->getIsNullable()); + } + + #[Test] + public function getConstraintKeysReturnsForeignKeyColumn(): void + { + $keys = $this->source->getConstraintKeys('fk_test_audit_trail_test', 'test_audit_trail'); + + static::assertCount(1, $keys); + static::assertSame('test_id', $keys[0]->getColumnName()); + static::assertSame('test', $keys[0]->getReferencedTableName()); + static::assertSame('id', $keys[0]->getReferencedColumnName()); + } + + #[Test] + public function getConstraintKeysReturnsPrimaryKeyColumn(): void + { + $keys = $this->source->getConstraintKeys('PRIMARY', 'test'); + + static::assertCount(1, $keys); + static::assertSame('id', $keys[0]->getColumnName()); + } + + #[Test] + public function getConstraintReturnsForeignKey(): void + { + $constraints = $this->source->getConstraints('test_audit_trail'); + + $foreignKey = null; + foreach ($constraints as $constraint) { + if ('FOREIGN KEY' !== $constraint->getType()) { + continue; + } + + $foreignKey = $constraint; + } + + static::assertNotNull($foreignKey); + static::assertSame('fk_test_audit_trail_test', $foreignKey->getName()); + static::assertSame('test', $foreignKey->getReferencedTableName()); + static::assertSame(['id'], $foreignKey->getReferencedColumns()); + } + + #[Test] + public function getConstraintsReturnsPrimaryKey(): void + { + $constraints = $this->source->getConstraints('test'); + + static::assertContainsOnlyInstancesOf(ConstraintObject::class, $constraints); + + $primary = null; + foreach ($constraints as $constraint) { + if ('PRIMARY KEY' !== $constraint->getType()) { + continue; + } + + $primary = $constraint; + } + + static::assertNotNull($primary); + static::assertSame('_phpdb_test_PRIMARY', $primary->getName()); + static::assertSame(['id'], $primary->getColumns()); + } + + #[Test] + public function getSchemasContainsCurrentDatabase(): void + { + static::assertContains( + (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_DATABASE'), + $this->source->getSchemas(), + ); + } + + #[Test] + public function getTableNamesExcludesViewsByDefault(): void + { + $tableNames = $this->source->getTableNames(); + + static::assertContains('test', $tableNames); + static::assertContains('test_charset', $tableNames); + static::assertContains('test_audit_trail', $tableNames); + static::assertNotContains('test_view', $tableNames); + } + + #[Test] + public function getTableNamesIncludesViewsWhenRequested(): void + { + static::assertContains('test_view', $this->source->getTableNames(null, true)); + } + + #[Test] + public function getTableReturnsTableWithColumnsAndConstraints(): void + { + $table = $this->source->getTable('test'); + + static::assertInstanceOf(TableObject::class, $table); + static::assertSame('test', $table->getName()); + static::assertCount(3, $table->getColumns()); + static::assertNotSame([], $table->getConstraints()); + } + + #[Test] + public function getTablesReturnsTableObjects(): void + { + $tables = $this->source->getTables(); + + static::assertContainsOnlyInstancesOf(TableObject::class, $tables); + static::assertNotSame([], $tables); + } + + #[Test] + public function getTriggerNames(): void + { + static::assertContains('after_test_update', $this->source->getTriggerNames()); + } + + #[Test] + public function getViewNamesAndGetView(): void + { + static::assertContains('test_view', $this->source->getViewNames()); + + $view = $this->source->getView('test_view'); + + static::assertInstanceOf(ViewObject::class, $view); + static::assertSame('test_view', $view->getName()); + } + + protected function setUp(): void + { + $this->getAdapter(); + + $factory = new MetadataInterfaceFactory(); + $this->source = $factory($this->container, MetadataInterface::class); + + parent::setUp(); + } +} diff --git a/test/integration/Mysqli/ConnectionTest.php b/test/integration/Mysqli/ConnectionTest.php new file mode 100644 index 0000000..e4c748a --- /dev/null +++ b/test/integration/Mysqli/ConnectionTest.php @@ -0,0 +1,205 @@ +createConnection(); + + $connection->beginTransaction(); + $connection->commit(); + + static::assertTrue($connection->isConnected()); + } + + #[Test] + public function beginTransactionAndRollback(): void + { + $connection = $this->createConnection(); + + $connection->beginTransaction(); + $connection->rollback(); + + static::assertTrue($connection->isConnected()); + } + + #[Test] + public function beginTransactionAutoConnects(): void + { + $connection = new Connection($this->connectionParameters()); + new Driver($connection, new Statement(), new Result()); + + $connection->beginTransaction(); + + static::assertTrue($connection->isConnected()); + $connection->rollback(); + } + + #[Test] + public function commitAutoConnects(): void + { + $connection = new Connection($this->connectionParameters()); + new Driver($connection, new Statement(), new Result()); + + $connection->commit(); + + static::assertTrue($connection->isConnected()); + } + + #[Test] + public function connectAndDisconnect(): void + { + $connection = new Connection($this->connectionParameters()); + + static::assertFalse($connection->isConnected()); + + $connection->connect(); + static::assertTrue($connection->isConnected()); + + $connection->disconnect(); + static::assertFalse($connection->isConnected()); + } + + #[Test] + public function constructWithMysqliResource(): void + { + $connection = new Connection($this->createMysqli()); + + static::assertTrue($connection->isConnected()); + } + + #[Test] + public function executeAutoConnects(): void + { + $connection = new Connection($this->connectionParameters()); + new Driver($connection, new Statement(), new Result()); + + static::assertNotNull($connection->execute('SELECT 1')); + static::assertTrue($connection->isConnected()); + } + + #[Test] + public function executeInsertReturnsGeneratedValue(): void + { + $connection = $this->createConnection(); + + $connection->execute('INSERT INTO test (name, value) VALUES (\'generated\', \'value\')'); + + static::assertIsInt($connection->getLastGeneratedValue()); + + $connection->execute('DELETE FROM test WHERE name = \'generated\''); + } + + #[Test] + public function executeSelect(): void + { + $connection = $this->createConnection(); + + $result = $connection->execute('SELECT * FROM test WHERE id = 1'); + + static::assertNotNull($result); + static::assertTrue($result->isBuffered()); + static::assertTrue($result->isQueryResult()); + static::assertSame(3, $result->getFieldCount()); + static::assertSame(1, $result->count()); + } + + #[Test] + public function getCurrentSchema(): void + { + $connection = $this->createConnection(); + + static::assertSame( + (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_DATABASE'), + $connection->getCurrentSchema(), + ); + } + + #[Test] + public function getCurrentSchemaAutoConnects(): void + { + $connection = new Connection($this->connectionParameters()); + new Driver($connection, new Statement(), new Result()); + + static::assertSame( + (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_DATABASE'), + $connection->getCurrentSchema(), + ); + static::assertTrue($connection->isConnected()); + } + + #[Test] + public function rollbackWithoutTransactionThrows(): void + { + $connection = $this->createConnection(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Must call beginTransaction() before you can rollback.'); + $connection->rollback(); + } + + /** + * @return array{hostname: string, username: string, password: string, database: string, port: int, charset: string} + */ + private function connectionParameters(): array + { + $host = (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_HOSTNAME'); + if ('' === $host) { + $host = 'localhost'; + } + + $port = (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_PORT'); + $port = '' === $port ? 3306 : (int) $port; + + return [ + 'hostname' => $host, + 'username' => (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_USERNAME'), + 'password' => (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_PASSWORD'), + 'database' => (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_DATABASE'), + 'port' => $port, + 'charset' => 'utf8', + ]; + } + + private function createConnection(): Connection + { + $connection = new Connection($this->createMysqli()); + new Driver($connection, new Statement(), new Result()); + + return $connection; + } + + private function createMysqli(): mysqli + { + $parameters = $this->connectionParameters(); + + return new mysqli( + $parameters['hostname'], + $parameters['username'], + $parameters['password'], + $parameters['database'], + $parameters['port'], + ); + } +} diff --git a/test/integration/Mysqli/StatementResultTest.php b/test/integration/Mysqli/StatementResultTest.php new file mode 100644 index 0000000..6bd92bb --- /dev/null +++ b/test/integration/Mysqli/StatementResultTest.php @@ -0,0 +1,334 @@ +offsetSet('id', 1.5, ParameterContainer::TYPE_DOUBLE); + $container->offsetSet('name', null, ParameterContainer::TYPE_NULL); + + $result = $this->createDriver(false) + ->createStatement('SELECT * FROM test WHERE id IN (?, ?)') + ->execute($container); + + static::assertNotNull($result); + static::assertTrue($result->isQueryResult()); + } + + #[Test] + public function bufferAfterIterationStartedThrows(): void + { + $result = $this->createDriver(false) + ->createStatement('SELECT * FROM test WHERE id = ?') + ->execute($this->createParameterContainer([1])); + + static::assertNotNull($result); + $result->current(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Cannot buffer a result set that has started iteration.'); + $result->buffer(); + } + + #[Test] + public function bufferedStatementResultSupportsCountAndRewind(): void + { + $driver = $this->createDriver(true); + $statement = $driver->createStatement('SELECT * FROM test WHERE value = ?'); + + static::assertFalse($statement->isPrepared()); + static::assertSame('SELECT * FROM test WHERE value = ?', $statement->getSql()); + + $result = $statement->execute($this->createParameterContainer(['bar'])); + static::assertNotNull($result); + + static::assertTrue($result->isBuffered()); + static::assertTrue($result->isQueryResult()); + static::assertSame(3, $result->getFieldCount()); + static::assertSame(3, $result->count()); + + static::assertCount(3, iterator_to_array($result)); + + $result->rewind(); + static::assertSame(['id' => 1, 'name' => 'foo', 'value' => 'bar'], $result->current()); + } + + #[Test] + public function bufferUnbufferedResult(): void + { + $result = $this->createDriver(false) + ->createStatement('SELECT * FROM test WHERE id = ?') + ->execute($this->createParameterContainer([1])); + + static::assertNotNull($result); + static::assertFalse($result->isBuffered()); + + $result->buffer(); + + static::assertTrue($result->isBuffered()); + } + + #[Test] + public function connectionExecuteUsesMysqliResult(): void + { + $mysqli = $this->createMysqli(); + $connection = new Connection($mysqli); + new Driver($connection, new Statement(), new Result()); + + $result = $connection->execute('SELECT * FROM test WHERE id = 1'); + static::assertNotNull($result); + + static::assertTrue($result->isBuffered()); + static::assertTrue($result->isQueryResult()); + static::assertInstanceOf(mysqli_result::class, $result->getResource()); + static::assertSame(3, $result->getFieldCount()); + static::assertSame(1, $result->count()); + static::assertSame(1, $result->getAffectedRows()); + + static::assertCount(1, iterator_to_array($result)); + + $result->rewind(); + static::assertSame(['id' => '1', 'name' => 'foo', 'value' => 'bar'], $result->current()); + } + + #[Test] + public function countOnNonQueryResultThrows(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Cannot count rows in a result that is not a query result'); + + $this->executeNonQuery()->count(); + } + + #[Test] + public function createStatementFromMysqliStmtResource(): void + { + $mysqli = $this->createMysqli(); + $driver = new Driver(new Connection($mysqli), new Statement(), new Result()); + + $resource = $mysqli->prepare('SELECT * FROM test WHERE id = ?'); + static::assertInstanceOf(mysqli_stmt::class, $resource); + + $statement = $driver->createStatement($resource); + + static::assertSame($resource, $statement->getResource()); + static::assertTrue($statement->isPrepared()); + } + + #[Test] + public function currentOnNonQueryResultThrows(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Cannot fetch from a result that is not a mysqli_result'); + + $this->executeNonQuery()->current(); + } + + #[Test] + public function executeWithEmptyArray(): void + { + $result = $this->createDriver(false) + ->createStatement('SELECT 1') + ->execute([]); + + static::assertNotNull($result); + static::assertTrue($result->isQueryResult()); + } + + #[Test] + public function insertReturnsGeneratedValueAndAffectedRows(): void + { + $driver = $this->createDriver(false); + $result = $driver->createStatement('INSERT INTO test (name, value) VALUES (?, ?)') + ->execute($this->createParameterContainer(['new', 'val'])); + + static::assertNotNull($result); + static::assertSame(1, $result->getAffectedRows()); + static::assertSame($driver->getLastGeneratedValue(), $result->getGeneratedValue()); + static::assertIsInt($driver->getLastGeneratedValue()); + + $this->createDriver(false) + ->createStatement('DELETE FROM test WHERE name = ?') + ->execute($this->createParameterContainer(['new'])); + } + + #[Test] + public function rewindOnNonQueryResultThrows(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Cannot rewind a result that is not a query result'); + + $this->executeNonQuery()->rewind(); + } + + #[Test] + public function rewindUnbufferedAfterIterationThrows(): void + { + $result = $this->createDriver(false) + ->createStatement('SELECT * FROM test WHERE id = ?') + ->execute($this->createParameterContainer([1])); + + static::assertNotNull($result); + $result->current(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Unbuffered results cannot be rewound for multiple iterations'); + $result->rewind(); + } + + #[Test] + public function statementContainerAccessors(): void + { + $driver = $this->createDriver(false); + $statement = $driver->createStatement('SELECT 1'); + + static::assertInstanceOf(ParameterContainer::class, $statement->getParameterContainer()); + + $container = new ParameterContainer(); + static::assertSame($statement, $statement->setParameterContainer($container)); + static::assertSame($container, $statement->getParameterContainer()); + + static::assertSame($statement, $statement->setSql('SELECT 2')); + static::assertSame('SELECT 2', $statement->getSql()); + } + + #[Test] + public function unbufferedResultCountThrows(): void + { + $result = $this->createDriver(false) + ->createStatement('SELECT * FROM test WHERE id = ?') + ->execute($this->createParameterContainer([1])); + + static::assertNotNull($result); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Row count is not available in unbuffered result sets.'); + $result->count(); + } + + #[Test] + public function unbufferedStatementResultIterates(): void + { + $result = $this->createDriver(false) + ->createStatement('SELECT * FROM test WHERE id = ?') + ->execute($this->createParameterContainer([1])); + + static::assertNotNull($result); + static::assertFalse($result->isBuffered()); + static::assertSame(['id' => 1, 'name' => 'foo', 'value' => 'bar'], $result->current()); + } + + #[Test] + public function updateReturnsAffectedRows(): void + { + $driver = $this->createDriver(false); + $result = $driver->createStatement('UPDATE test SET value = ? WHERE id = ?') + ->execute($this->createParameterContainer(['updated', 1])); + + static::assertNotNull($result); + static::assertSame(1, $result->getAffectedRows()); + + $driver->createStatement('UPDATE test SET value = ? WHERE id = ?') + ->execute($this->createParameterContainer(['bar', 1])); + } + + #[Test] + public function validReturnsTrueAfterCurrent(): void + { + $result = $this->createDriver(false) + ->createStatement('SELECT * FROM test WHERE id = ?') + ->execute($this->createParameterContainer([1])); + + static::assertNotNull($result); + $result->current(); + + static::assertTrue($result->valid()); + } + + private function createDriver(bool $bufferResults = false): Driver + { + return new Driver( + new Connection($this->createMysqli()), + new Statement(bufferResults: $bufferResults), + new Result(), + ); + } + + private function createMysqli(): mysqli + { + $host = (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_HOSTNAME'); + if ('' === $host) { + $host = 'localhost'; + } + + $port = (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_PORT'); + $port = '' === $port ? 3306 : (int) $port; + + return new mysqli( + $host, + (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_USERNAME'), + (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_PASSWORD'), + (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_DATABASE'), + $port, + ); + } + + /** + * @param list $values + */ + private function createParameterContainer(array $values): ParameterContainer + { + $container = new ParameterContainer(); + foreach ($values as $key => $value) { + $container->offsetSet( + $key, + $value, + is_int($value) ? ParameterContainer::TYPE_INTEGER : ParameterContainer::TYPE_STRING, + ); + } + + return $container; + } + + private function executeNonQuery(): ResultInterface + { + $mysqli = $this->createMysqli(); + $connection = new Connection($mysqli); + new Driver($connection, new Statement(), new Result()); + + $result = $connection->execute('UPDATE test SET name = name WHERE id = 1'); + static::assertNotNull($result); + + return $result; + } +} diff --git a/test/integration/Pdo/AbstractAdapterTestCase.php b/test/integration/Pdo/AbstractAdapterTestCase.php index 586b066..4dcd5c7 100644 --- a/test/integration/Pdo/AbstractAdapterTestCase.php +++ b/test/integration/Pdo/AbstractAdapterTestCase.php @@ -12,6 +12,7 @@ use PhpDb\Mysql\Pdo\Driver; use PhpDbIntegrationTest\Mysql\Container\TestAsset\SetupTrait; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; #[CoversMethod(Adapter::class, 'getCurrentSchema')] @@ -23,63 +24,66 @@ abstract class AbstractAdapterTestCase extends TestCase { use SetupTrait; - public function testConnection(): void + #[Test] + public function connection(): void { /** @var ConnectionInterface $connection */ $connection = $this->getAdapter()->getDriver()->getConnection(); - $this->assertInstanceOf(ConnectionInterface::class, $connection); + static::assertInstanceOf(ConnectionInterface::class, $connection); } - public function testGetCurrentSchema(): void - { - /** @var AdapterInterface&SchemaAwareInterface&Adapter $adapter */ - $adapter = $this->getAdapter(); - $schema = $adapter->getCurrentSchema(); - self::assertIsString($schema); - self::assertNotEmpty($schema); - } - - public function testDriverDisconnectAfterQuoteWithPlatform(): void + #[Test] + public function driverDisconnectAfterQuoteWithPlatform(): void { $isTcpConnection = $this->isTcpConnection(); /** @var AdapterInterface&Adapter $adapter */ $adapter = $this->getAdapter([ - 'db' => [ + AdapterInterface::class => [ 'driver' => Driver::class, ], ]); $adapter->getDriver()->getConnection()->connect(); - self::assertTrue($adapter->getDriver()->getConnection()->isConnected()); + static::assertTrue($adapter->getDriver()->getConnection()->isConnected()); if ($isTcpConnection) { - self::assertTrue($adapter->getDriver()->getConnection()->isConnected()); + static::assertTrue($adapter->getDriver()->getConnection()->isConnected()); } $adapter->getDriver()->getConnection()->disconnect(); - self::assertFalse($adapter->getDriver()->getConnection()->isConnected()); + static::assertFalse($adapter->getDriver()->getConnection()->isConnected()); if ($isTcpConnection) { - self::assertFalse($adapter->getDriver()->getConnection()->isConnected()); + static::assertFalse($adapter->getDriver()->getConnection()->isConnected()); } $adapter->getDriver()->getConnection()->connect(); - self::assertTrue($adapter->getDriver()->getConnection()->isConnected()); + static::assertTrue($adapter->getDriver()->getConnection()->isConnected()); if ($isTcpConnection) { - self::assertTrue($adapter->getDriver()->getConnection()->isConnected()); + static::assertTrue($adapter->getDriver()->getConnection()->isConnected()); } $adapter->getPlatform()->quoteValue('test'); $adapter->getDriver()->getConnection()->disconnect(); - self::assertFalse($adapter->getDriver()->getConnection()->isConnected()); + static::assertFalse($adapter->getDriver()->getConnection()->isConnected()); if ($isTcpConnection) { - self::assertFalse($adapter->getDriver()->getConnection()->isConnected()); + static::assertFalse($adapter->getDriver()->getConnection()->isConnected()); } } + #[Test] + public function getCurrentSchema(): void + { + /** @var AdapterInterface&SchemaAwareInterface&Adapter $adapter */ + $adapter = $this->getAdapter(); + $schema = $adapter->getCurrentSchema(); + static::assertIsString($schema); + static::assertNotEmpty($schema); + } + protected function isTcpConnection(): bool { $hostName = $this->getHostname(); - return $hostName !== 'localhost' && $hostName !== '127.0.0.1'; + return 'localhost' !== $hostName && '127.0.0.1' !== $hostName; } } diff --git a/test/integration/Pdo/AdapterTest.php b/test/integration/Pdo/AdapterTest.php index 53d6774..87de179 100644 --- a/test/integration/Pdo/AdapterTest.php +++ b/test/integration/Pdo/AdapterTest.php @@ -5,7 +5,6 @@ namespace PhpDbIntegrationTest\Mysql\Pdo; use PhpDbIntegrationTest\Mysql\Container\TestAsset\SetupTrait; -use PhpDbIntegrationTest\Mysql\Pdo\AbstractAdapterTestCase; use PHPUnit\Framework\Attributes\CoversNothing; #[CoversNothing] diff --git a/test/integration/Pdo/ConnectionTest.php b/test/integration/Pdo/ConnectionTest.php index 3ec97c1..d959a00 100644 --- a/test/integration/Pdo/ConnectionTest.php +++ b/test/integration/Pdo/ConnectionTest.php @@ -18,168 +18,193 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +use function getenv; + #[Group('integration')] #[Group('integration-pdo')] #[CoversClass(Connection::class)] -#[CoversMethod(Connection::class, 'prepare')] -#[CoversMethod(Connection::class, 'execute')] -#[CoversMethod(Connection::class, 'getResource')] #[CoversMethod(Connection::class, 'getLastGeneratedValue')] final class ConnectionTest extends TestCase { use SetupTrait; - public function testGetResource(): void + #[Test] + public function autocommitRestoredAfterCommit(): void { + /** @var Connection $connection */ $connection = $this->getAdapter()->getDriver()->getConnection(); - self::assertInstanceOf(PDO::class, $connection->getResource()); - } + $connection->connect(); + static::assertTrue($connection->isConnected()); - public function testExecute(): void - { - $connection = $this->getAdapter()->getDriver()->getConnection(); - /** @var ResultInterface&Result $result */ - $result = $connection->execute('SELECT \'foo\''); - self::assertInstanceOf(ResultInterface::class, $result); - self::assertInstanceOf(Result::class, $result); - } + $connection->beginTransaction(); + static::assertTrue($connection->inTransaction()); + $connection->commit(); + static::assertFalse($connection->inTransaction()); - public function testPrepare(): void - { - /** @var ConnectionInterface&PdoConnectionInterface&AbstractConnection&AbstractPdoConnection&Connection $connection */ - $connection = $this->getAdapter()->getDriver()->getConnection(); - /** @var StatementInterface&Statement $statement */ - $statement = $connection->prepare('SELECT \'foo\''); - self::assertInstanceOf(StatementInterface::class, $statement); - self::assertInstanceOf(Statement::class, $statement); - } + $connection->execute("INSERT INTO test (name, value) VALUES ('tx_autocommit', 'test')"); + + $connection->disconnect(); - public function testGetLastGeneratedValue(): void - { - /** @var ConnectionInterface&PdoConnectionInterface&AbstractConnection&AbstractPdoConnection&Connection $connection */ - $connection = $this->getAdapter()->getDriver()->getConnection(); $connection->connect(); - $lastId = (int) $connection->getLastGeneratedValue(); - self::assertIsInt($lastId); + $result = $connection->execute("SELECT COUNT(*) AS cnt FROM test WHERE name = 'tx_autocommit'"); + static::assertSame(1, $result->getResource()->fetchColumn()); + + $connection->execute("DELETE FROM test WHERE name = 'tx_autocommit'"); $connection->disconnect(); } - public function testConnectMethodReturnsConnectionInterface(): void + #[Test] + public function autocommitRestoredAfterRollback(): void { - /** @var ConnectionInterface&PdoConnectionInterface&AbstractConnection&AbstractPdoConnection&Connection $connection */ + /** @var Connection $connection */ $connection = $this->getAdapter()->getDriver()->getConnection(); - self::assertInstanceOf(ConnectionInterface::class, $connection->connect()); + $connection->connect(); + static::assertTrue($connection->isConnected()); + + $connection->beginTransaction(); + static::assertTrue($connection->inTransaction()); + $connection->rollback(); + static::assertFalse($connection->inTransaction()); + + $connection->execute("INSERT INTO test (name, value) VALUES ('tx_autocommit_rb', 'test')"); + + $connection->disconnect(); + + $connection->connect(); + $result = $connection->execute("SELECT COUNT(*) AS cnt FROM test WHERE name = 'tx_autocommit_rb'"); + static::assertSame(1, $result->getResource()->fetchColumn()); + + $connection->execute("DELETE FROM test WHERE name = 'tx_autocommit_rb'"); $connection->disconnect(); } - public function testBeginTransaction(): void + #[Test] + public function beginTransaction(): void { $connection = $this->getAdapter()->getDriver()->getConnection(); $connection->connect(); - self::assertTrue($connection->isConnected()); - self::assertFalse($connection->inTransaction()); + static::assertTrue($connection->isConnected()); + static::assertFalse($connection->inTransaction()); $result = $connection->beginTransaction(); - self::assertInstanceOf(Connection::class, $result); - self::assertTrue($connection->inTransaction()); + static::assertInstanceOf(Connection::class, $result); + static::assertTrue($connection->inTransaction()); $connection->rollback(); - self::assertFalse($connection->inTransaction()); + static::assertFalse($connection->inTransaction()); $connection->disconnect(); } - public function testCommit(): void + #[Test] + public function commit(): void { $connection = $this->getAdapter()->getDriver()->getConnection(); $connection->connect(); - self::assertTrue($connection->isConnected()); + static::assertTrue($connection->isConnected()); $connection->beginTransaction(); - self::assertTrue($connection->inTransaction()); + static::assertTrue($connection->inTransaction()); $connection->execute("INSERT INTO test (name, value) VALUES ('tx_commit', 'test')"); $result = $connection->commit(); - self::assertInstanceOf(Connection::class, $result); - self::assertFalse($connection->inTransaction()); + static::assertInstanceOf(Connection::class, $result); + static::assertFalse($connection->inTransaction()); $result = $connection->execute("SELECT COUNT(*) AS cnt FROM test WHERE name = 'tx_commit'"); - self::assertSame(1, $result->getResource()->fetchColumn()); + static::assertSame(1, $result->getResource()->fetchColumn()); $connection->execute("DELETE FROM test WHERE name = 'tx_commit'"); $connection->disconnect(); } - public function testRollback(): void + #[Test] + public function connectMethodReturnsConnectionInterface(): void { + /** @var ConnectionInterface&PdoConnectionInterface&AbstractConnection&AbstractPdoConnection&Connection $connection */ $connection = $this->getAdapter()->getDriver()->getConnection(); - $connection->connect(); - self::assertTrue($connection->isConnected()); - - $connection->beginTransaction(); - self::assertTrue($connection->inTransaction()); - - $connection->execute("INSERT INTO test (name, value) VALUES ('tx_rollback', 'test')"); - - $result = $connection->rollback(); - self::assertInstanceOf(Connection::class, $result); - self::assertFalse($connection->inTransaction()); - - $result = $connection->execute("SELECT COUNT(*) AS cnt FROM test WHERE name = 'tx_rollback'"); - self::assertSame(0, $result->getResource()->fetchColumn()); - + static::assertInstanceOf(ConnectionInterface::class, $connection->connect()); $connection->disconnect(); } - public function testAutocommitRestoredAfterCommit(): void + #[Test] + public function execute(): void + { + $connection = $this->getAdapter()->getDriver()->getConnection(); + /** @var ResultInterface&Result $result */ + $result = $connection->execute('SELECT \'foo\''); + static::assertInstanceOf(ResultInterface::class, $result); + static::assertInstanceOf(Result::class, $result); + } + + #[Test] + public function getCurrentSchema(): void { /** @var Connection $connection */ $connection = $this->getAdapter()->getDriver()->getConnection(); $connection->connect(); - self::assertTrue($connection->isConnected()); - - $connection->beginTransaction(); - self::assertTrue($connection->inTransaction()); - $connection->commit(); - self::assertFalse($connection->inTransaction()); - $connection->execute("INSERT INTO test (name, value) VALUES ('tx_autocommit', 'test')"); + static::assertSame( + (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_DATABASE'), + $connection->getCurrentSchema(), + ); $connection->disconnect(); + } + #[Test] + public function getLastGeneratedValue(): void + { + /** @var ConnectionInterface&PdoConnectionInterface&AbstractConnection&AbstractPdoConnection&Connection $connection */ + $connection = $this->getAdapter()->getDriver()->getConnection(); $connection->connect(); - $result = $connection->execute("SELECT COUNT(*) AS cnt FROM test WHERE name = 'tx_autocommit'"); - self::assertSame(1, $result->getResource()->fetchColumn()); - - $connection->execute("DELETE FROM test WHERE name = 'tx_autocommit'"); + $lastId = (int) $connection->getLastGeneratedValue(); + static::assertIsInt($lastId); $connection->disconnect(); } - public function testAutocommitRestoredAfterRollback(): void + #[Test] + public function getResource(): void + { + $connection = $this->getAdapter()->getDriver()->getConnection(); + static::assertInstanceOf(PDO::class, $connection->getResource()); + } + + #[Test] + public function prepare(): void + { + /** @var ConnectionInterface&PdoConnectionInterface&AbstractConnection&AbstractPdoConnection&Connection $connection */ + $connection = $this->getAdapter()->getDriver()->getConnection(); + /** @var StatementInterface&Statement $statement */ + $statement = $connection->prepare('SELECT \'foo\''); + static::assertInstanceOf(StatementInterface::class, $statement); + static::assertInstanceOf(Statement::class, $statement); + } + + #[Test] + public function rollback(): void { - /** @var Connection $connection */ $connection = $this->getAdapter()->getDriver()->getConnection(); $connection->connect(); - self::assertTrue($connection->isConnected()); + static::assertTrue($connection->isConnected()); $connection->beginTransaction(); - self::assertTrue($connection->inTransaction()); - $connection->rollback(); - self::assertFalse($connection->inTransaction()); + static::assertTrue($connection->inTransaction()); - $connection->execute("INSERT INTO test (name, value) VALUES ('tx_autocommit_rb', 'test')"); + $connection->execute("INSERT INTO test (name, value) VALUES ('tx_rollback', 'test')"); - $connection->disconnect(); + $result = $connection->rollback(); + static::assertInstanceOf(Connection::class, $result); + static::assertFalse($connection->inTransaction()); - $connection->connect(); - $result = $connection->execute("SELECT COUNT(*) AS cnt FROM test WHERE name = 'tx_autocommit_rb'"); - self::assertSame(1, $result->getResource()->fetchColumn()); + $result = $connection->execute("SELECT COUNT(*) AS cnt FROM test WHERE name = 'tx_rollback'"); + static::assertSame(0, $result->getResource()->fetchColumn()); - $connection->execute("DELETE FROM test WHERE name = 'tx_autocommit_rb'"); $connection->disconnect(); } } diff --git a/test/integration/Pdo/QueryTest.php b/test/integration/Pdo/QueryTest.php index 4d8b5e0..4da763d 100644 --- a/test/integration/Pdo/QueryTest.php +++ b/test/integration/Pdo/QueryTest.php @@ -5,19 +5,18 @@ namespace PhpDbIntegrationTest\Mysql\Pdo; use Exception; -use PhpDb\Adapter\Adapter; use PhpDb\Adapter\Driver\Pdo\Result as PdoResult; use PhpDb\Adapter\Driver\StatementInterface; use PhpDb\Adapter\Exception\RuntimeException; use PhpDb\ResultSet\ResultSet; use PhpDb\Sql\Sql; use PhpDbIntegrationTest\Mysql\Container\TestAsset\SetupTrait; -use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -#[CoversMethod(Adapter::class, 'query')] -#[CoversMethod(ResultSet::class, 'current')] +#[CoversNothing] final class QueryTest extends TestCase { use SetupTrait; @@ -45,72 +44,13 @@ public static function getQueriesWithRowResult(): array ]; } - /** - * @throws Exception - */ - #[DataProvider('getQueriesWithRowResult')] - public function testQuery(string $query, array $params, array $expected): void - { - /** @todo Have AdapterInterface implement query */ - $result = $this->getAdapter()->query($query, $params); - $this->assertInstanceOf(ResultSet::class, $result); - $current = $result->current(); - // test as array value - $this->assertEquals($expected, (array) $current); - // test as object value - /** @var string $value */ - foreach ($expected as $key => $value) { - $this->assertEquals($value, $current->$key); - } - } - - /** - * @see https://github.com/zendframework/zend-db/issues/288 - * - * @throws Exception - */ - public function testSetSessionTimeZone(): void - { - $result = $this->getAdapter()->query('SET @@session.time_zone = :tz', [':tz' => 'SYSTEM']); - $this->assertInstanceOf(PdoResult::class, $result); - } - - /** - * @throws Exception - */ - public function testSelectWithNotPermittedBindParamName(): void - { - $this->expectException(RuntimeException::class); - $this->getAdapter()->query('SET @@session.time_zone = :tz$', [':tz$' => 'SYSTEM']); - } - - public function testSelectResultCountReturnsActualRowCount(): void - { - $result = $this->getAdapter()->query('SELECT * FROM test WHERE value = ?', ['bar']); - $this->assertInstanceOf(ResultSet::class, $result); - self::assertSame(3, $result->count()); - } - - public function testSelectResultCountWithWhereClause(): void - { - $result = $this->getAdapter()->query('SELECT * FROM test WHERE name = ?', ['foo']); - $this->assertInstanceOf(ResultSet::class, $result); - self::assertSame(1, $result->count()); - } - - public function testSelectResultCountReturnsZeroForNoResults(): void - { - $result = $this->getAdapter()->query('SELECT * FROM test WHERE name = ?', ['nonexistent']); - $this->assertInstanceOf(ResultSet::class, $result); - self::assertSame(0, $result->count()); - } - /** * @see https://github.com/laminas/laminas-db/issues/47 */ - public function testNamedParameters(): void + #[Test] + public function namedParameters(): void { - $this->assertNotNull($this->adapter); + static::assertNotNull($this->adapter); $sql = new Sql($this->adapter); $insert = $sql->update('test'); @@ -120,7 +60,7 @@ public function testNamedParameters(): void ])->where(['id' => ':id']); /** @var StatementInterface $stmt */ $stmt = $sql->prepareStatementForSqlObject($insert); - $this->assertInstanceOf(StatementInterface::class, $stmt); + static::assertInstanceOf(StatementInterface::class, $stmt); //positional parameters $stmt->execute([ @@ -143,4 +83,70 @@ public function testNamedParameters(): void 'value' => 'bar', ]); } + + /** + * @throws Exception + */ + #[Test] + #[DataProvider('getQueriesWithRowResult')] + public function query(string $query, array $params, array $expected): void + { + /** @todo Have AdapterInterface implement query */ + $result = $this->getAdapter()->query($query, $params); + static::assertInstanceOf(ResultSet::class, $result); + $current = $result->current(); + // test as array value + static::assertEquals($expected, (array) $current); + // test as object value + /** @var string $value */ + foreach ($expected as $key => $value) { + static::assertEquals($value, $current->$key); + } + } + + #[Test] + public function selectResultCountReturnsActualRowCount(): void + { + $result = $this->getAdapter()->query('SELECT * FROM test WHERE value = ?', ['bar']); + static::assertInstanceOf(ResultSet::class, $result); + static::assertSame(3, $result->count()); + } + + #[Test] + public function selectResultCountReturnsZeroForNoResults(): void + { + $result = $this->getAdapter()->query('SELECT * FROM test WHERE name = ?', ['nonexistent']); + static::assertInstanceOf(ResultSet::class, $result); + static::assertSame(0, $result->count()); + } + + #[Test] + public function selectResultCountWithWhereClause(): void + { + $result = $this->getAdapter()->query('SELECT * FROM test WHERE name = ?', ['foo']); + static::assertInstanceOf(ResultSet::class, $result); + static::assertSame(1, $result->count()); + } + + /** + * @throws Exception + */ + #[Test] + public function selectWithNotPermittedBindParamName(): void + { + $this->expectException(RuntimeException::class); + $this->getAdapter()->query('SET @@session.time_zone = :tz$', [':tz$' => 'SYSTEM']); + } + + /** + * @see https://github.com/zendframework/zend-db/issues/288 + * + * @throws Exception + */ + #[Test] + public function setSessionTimeZone(): void + { + $result = $this->getAdapter()->query('SET @@session.time_zone = :tz', [':tz' => 'SYSTEM']); + static::assertInstanceOf(PdoResult::class, $result); + } } diff --git a/test/integration/Pdo/TableGatewayAndAdapterTest.php b/test/integration/Pdo/TableGatewayAndAdapterTest.php index 79ef480..ca8ce1b 100644 --- a/test/integration/Pdo/TableGatewayAndAdapterTest.php +++ b/test/integration/Pdo/TableGatewayAndAdapterTest.php @@ -9,8 +9,9 @@ use PhpDb\ResultSet\AbstractResultSet; use PhpDb\TableGateway\TableGateway; use PhpDbIntegrationTest\Mysql\Container\TestAsset\SetupTrait; -use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use function array_fill; @@ -21,27 +22,33 @@ * On tear down disconnected from the database and set the driver adapter on null * Running many tests ended up in consuming all mysql connections and not releasing them */ -#[CoversMethod(Connection::class, 'disconnect')] +#[CoversClass(Connection::class)] final class TableGatewayAndAdapterTest extends TestCase { use SetupTrait; + public static function connections(): array + { + return array_fill(0, count: 200, value: []); + } + /** * @throws Exception */ + #[Test] #[DataProvider('connections')] - public function testGetOutOfConnections(): void + public function getOutOfConnections(): void { $adapter = $this->getAdapter(); $adapter->query('SELECT VERSION();'); - $table = new TableGateway( + $table = new TableGateway( 'test', - $this->adapter + $this->adapter, ); $select = $table->getSql()->select()->where(['name' => 'foo']); /** @var AbstractResultSet $result */ $result = $table->selectWith($select); - self::assertCount(3, $result->current()); + static::assertCount(3, $result->current()); } protected function tearDown(): void @@ -51,9 +58,4 @@ protected function tearDown(): void } $this->adapter = null; } - - public static function connections(): array - { - return array_fill(0, 200, []); - } } diff --git a/test/integration/Pdo/TableGatewayTest.php b/test/integration/Pdo/TableGatewayTest.php index e564a30..556e506 100644 --- a/test/integration/Pdo/TableGatewayTest.php +++ b/test/integration/Pdo/TableGatewayTest.php @@ -15,60 +15,60 @@ use PhpDb\TableGateway\Feature\MetadataFeature; use PhpDb\TableGateway\TableGateway; use PhpDbIntegrationTest\Mysql\Container\TestAsset\SetupTrait; -use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Depends; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use function count; -#[CoversMethod(TableGateway::class, '__construct')] -#[CoversMethod(TableGateway::class, 'select')] -#[CoversMethod(TableGateway::class, 'insert')] +#[CoversNothing] final class TableGatewayTest extends TestCase { use SetupTrait; - public function testConstructor(): void + /** @psalm-return array */ + public static function tableProvider(): array { - /** @var AdapterInterface&Adapter $adapter */ - $adapter = $this->getAdapter(['db' => ['driver' => Driver::class]]); - $tableGateway = new TableGateway('test', $adapter); - $this->assertInstanceOf(TableGateway::class, $tableGateway); + return [ + 'string' => ['test'], + 'aliased string' => [['foo' => 'test']], + 'TableIdentifier' => [new TableIdentifier('test')], + 'aliased TableIdentifier' => [['foo' => new TableIdentifier('test')]], + ]; } - public function testSelect(): void + #[Test] + public function constructor(): void { - $tableGateway = new TableGateway('test', $this->getAdapter(['db' => ['driver' => Driver::class]])); - /** @var ResultSet $rowset */ - $rowset = $tableGateway->select(); - $this->assertTrue(count($rowset) > 0); - /** @var ArrayObject $row */ - foreach ($rowset as $row) { - $this->assertTrue(isset($row->id)); - $this->assertNotEmpty(isset($row->name)); - $this->assertNotEmpty(isset($row->value)); - } + /** @var AdapterInterface&Adapter $adapter */ + $adapter = $this->getAdapter([AdapterInterface::class => ['driver' => Driver::class]]); + $tableGateway = new TableGateway('test', $adapter); + static::assertInstanceOf(TableGateway::class, $tableGateway); } - public function testInsert(): void + #[Test] + public function insert(): void { - $tableGateway = new TableGateway('test', $this->getAdapter(['db' => ['driver' => Driver::class]])); + $tableGateway = new TableGateway('test', $this->getAdapter([ + AdapterInterface::class => ['driver' => Driver::class], + ])); $tableGateway->select(); - $data = [ + $data = [ 'name' => 'test_name', 'value' => 'test_value', ]; $affectedRows = $tableGateway->insert($data); - $this->assertEquals(1, $affectedRows); + static::assertSame(1, $affectedRows); /** @var ResultSet $rowSet */ $rowSet = $tableGateway->select(['id' => $tableGateway->getLastInsertValue()]); /** @var ArrayObject $row */ $row = $rowSet->current(); foreach ($data as $key => $value) { - $this->assertEquals($row->$key, $value); + static::assertEquals($row->$key, $value); } } @@ -76,65 +76,78 @@ public function testInsert(): void * @see https://github.com/zendframework/zend-db/issues/35 * @see https://github.com/zendframework/zend-db/pull/178 */ - public function testInsertWithExtendedCharsetFieldName(): int|string + #[Test] + public function insertWithExtendedCharsetFieldName(): int|string { - $tableGateway = new TableGateway('test_charset', $this->getAdapter(['db' => ['driver' => Driver::class]])); + $tableGateway = new TableGateway('test_charset', $this->getAdapter([ + AdapterInterface::class => ['driver' => Driver::class], + ])); $affectedRows = $tableGateway->insert([ 'field$' => 'test_value1', 'field_' => 'test_value2', ]); - $this->assertEquals(1, $affectedRows); + static::assertSame(1, $affectedRows); return $tableGateway->getLastInsertValue(); } - #[Depends('testInsertWithExtendedCharsetFieldName')] - public function testUpdateWithExtendedCharsetFieldName(mixed $id): void + #[Test] + public function select(): void { - $tableGateway = new TableGateway('test_charset', $this->getAdapter(['db' => ['driver' => Driver::class]])); - - $data = [ - 'field$' => 'test_value3', - 'field_' => 'test_value4', - ]; - $affectedRows = $tableGateway->update($data, ['id' => $id]); - $this->assertEquals(1, $affectedRows); - /** @var ResultSet $rowSet */ - $rowSet = $tableGateway->select(['id' => $id]); + $tableGateway = new TableGateway('test', $this->getAdapter([ + AdapterInterface::class => ['driver' => Driver::class], + ])); + /** @var ResultSet $rowset */ + $rowset = $tableGateway->select(); + static::assertTrue(count($rowset) > 0); /** @var ArrayObject $row */ - $row = $rowSet->current(); - - foreach ($data as $key => $value) { - $this->assertEquals($row->$key, $value); + foreach ($rowset as $row) { + static::assertTrue(null !== ($row->id ?? null)); + static::assertNotEmpty(null !== ($row->name ?? null)); + static::assertNotEmpty(null !== ($row->value ?? null)); } } + #[Test] #[DataProvider('tableProvider')] - public function testTableGatewayWithMetadataFeature(array|string|TableIdentifier $table): void + public function tableGatewayWithMetadataFeature(array|string|TableIdentifier $table): void { /** @var AdapterInterface&SchemaAwareInterface&Adapter $adapter */ - $adapter = $this->getAdapter(['db' => ['driver' => Driver::class]]); + $adapter = $this->getAdapter([AdapterInterface::class => ['driver' => Driver::class]]); $tableGateway = new TableGateway( $table, $adapter, new MetadataFeature( new Source($adapter), - ) + ), ); - self::assertInstanceOf(TableGateway::class, $tableGateway); - self::assertSame($table, $tableGateway->getTable()); + static::assertInstanceOf(TableGateway::class, $tableGateway); + static::assertSame($table, $tableGateway->getTable()); } - /** @psalm-return array */ - public static function tableProvider(): array + #[Test] + #[Depends('insertWithExtendedCharsetFieldName')] + public function updateWithExtendedCharsetFieldName(mixed $id): void { - return [ - 'string' => ['test'], - 'aliased string' => [['foo' => 'test']], - 'TableIdentifier' => [new TableIdentifier('test')], - 'aliased TableIdentifier' => [['foo' => new TableIdentifier('test')]], + $tableGateway = new TableGateway('test_charset', $this->getAdapter([ + AdapterInterface::class => ['driver' => Driver::class], + ])); + + $data = [ + 'field$' => 'test_value3', + 'field_' => 'test_value4', ]; + $affectedRows = $tableGateway->update($data, ['id' => $id]); + static::assertSame(1, $affectedRows); + /** @var ResultSet $rowSet */ + $rowSet = $tableGateway->select(['id' => $id]); + /** @var ArrayObject $row */ + $row = $rowSet->current(); + + foreach ($data as $key => $value) { + static::assertEquals($row->$key, $value); + } } } diff --git a/test/integration/TableGatewayTest.php b/test/integration/TableGatewayTest.php index 7573fb6..18321a6 100644 --- a/test/integration/TableGatewayTest.php +++ b/test/integration/TableGatewayTest.php @@ -10,12 +10,11 @@ use PhpDb\ResultSet\AbstractResultSet; use PhpDb\TableGateway\TableGateway; use PhpDbIntegrationTest\Mysql\Container\TestAsset\SetupTrait; -use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -#[CoversMethod(AbstractResultSet::class, 'current')] -#[CoversMethod(AbstractResultSet::class, 'isBuffered')] -#[CoversMethod(TableGateway::class, 'select')] +#[CoversNothing] final class TableGatewayTest extends TestCase { use SetupTrait; @@ -23,11 +22,12 @@ final class TableGatewayTest extends TestCase /** * @see https://github.com/zendframework/zend-db/issues/330 */ - public function testSelectWithEmptyCurrentWithBufferResult(): void + #[Test] + public function selectWithEmptyCurrentWithBufferResult(): void { /** @var AdapterInterface&Adapter $adapter */ $adapter = $this->getAdapter([ - 'db' => [ + AdapterInterface::class => [ 'driver' => Driver::class, 'options' => [ 'buffer_results' => true, @@ -39,7 +39,7 @@ public function testSelectWithEmptyCurrentWithBufferResult(): void /** @var AbstractResultSet $rowset */ $rowset = $tableGateway->select('id = 0'); - $this->assertNull($rowset->current()); + static::assertNull($rowset->current()); $adapter->getDriver()->getConnection()->disconnect(); } @@ -47,11 +47,12 @@ public function testSelectWithEmptyCurrentWithBufferResult(): void /** * @see https://github.com/zendframework/zend-db/issues/330 */ - public function testSelectWithEmptyCurrentWithoutBufferResult(): void + #[Test] + public function selectWithEmptyCurrentWithoutBufferResult(): void { /** @var AdapterInterface&Adapter $adapter */ - $adapter = $this->getAdapter([ - 'db' => [ + $adapter = $this->getAdapter([ + AdapterInterface::class => [ 'driver' => Driver::class, 'options' => [ 'buffer_results' => false, @@ -61,9 +62,9 @@ public function testSelectWithEmptyCurrentWithoutBufferResult(): void $tableGateway = new TableGateway('test', $adapter); /** @var AbstractResultSet $rowset */ $rowset = $tableGateway->select('id = 0'); - $this->assertEquals(false, $rowset->isBuffered()); + static::assertFalse($rowset->isBuffered()); - $this->assertNull($rowset->current()); + static::assertNull($rowset->current()); $adapter->getDriver()->getConnection()->disconnect(); } diff --git a/test/integration/TestFixtures/mysql.sql b/test/integration/TestFixtures/mysql.sql index efeff46..1e3cf41 100644 --- a/test/integration/TestFixtures/mysql.sql +++ b/test/integration/TestFixtures/mysql.sql @@ -1,3 +1,4 @@ +SET FOREIGN_KEY_CHECKS = 0; DROP TABLE IF EXISTS test; CREATE TABLE IF NOT EXISTS test ( id INT NOT NULL AUTO_INCREMENT, @@ -53,3 +54,16 @@ CREATE TRIGGER after_test_update test_value_old = OLD.value, test_value_new = NEW.value, changed = NOW(); + +DROP TABLE IF EXISTS test_enum; +CREATE TABLE IF NOT EXISTS test_enum ( + id INT NOT NULL AUTO_INCREMENT, + status ENUM('active', 'inactive') NOT NULL DEFAULT 'active', + PRIMARY KEY (id) +) ENGINE=InnoDB; + +ALTER TABLE test_audit_trail + ADD CONSTRAINT fk_test_audit_trail_test + FOREIGN KEY (test_id) REFERENCES test(id); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/test/unit/AdapterPlatformTest.php b/test/unit/AdapterPlatformTest.php index 38f8237..8ad2fd1 100644 --- a/test/unit/AdapterPlatformTest.php +++ b/test/unit/AdapterPlatformTest.php @@ -4,230 +4,276 @@ namespace PhpDbTest\Mysql\Platform; +use mysqli; use Override; +use PDO; +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\Platform; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +#[CoversMethod(AdapterPlatform::class, '__construct')] #[CoversMethod(AdapterPlatform::class, 'getName')] -#[CoversMethod(AdapterPlatform::class, 'getQuoteIdentifierSymbol')] -#[CoversMethod(AdapterPlatform::class, 'quoteIdentifier')] +#[CoversMethod(AdapterPlatform::class, 'getSqlPlatformDecorator')] #[CoversMethod(AdapterPlatform::class, 'quoteIdentifierChain')] -#[CoversMethod(AdapterPlatform::class, 'getQuoteValueSymbol')] #[CoversMethod(AdapterPlatform::class, 'quoteValue')] #[CoversMethod(AdapterPlatform::class, 'quoteTrustedValue')] -#[CoversMethod(AdapterPlatform::class, 'quoteValueList')] -#[CoversMethod(AdapterPlatform::class, 'getIdentifierSeparator')] -#[CoversMethod(AdapterPlatform::class, 'quoteIdentifierInFragment')] +#[CoversMethod(AdapterPlatform::class, 'quoteViaDriver')] final class AdapterPlatformTest extends TestCase { protected AdapterPlatform $platform; - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - #[Override] - protected function setUp(): void + #[Test] + public function getIdentifierSeparator(): void { - $pdo = new Driver( - $this->createMock(Connection::class), - $this->createMock(Statement::class), - $this->createMock(Result::class), - ); - $this->platform = new AdapterPlatform($pdo); + static::assertSame('.', $this->platform->getIdentifierSeparator()); } - public function testGetName(): void + #[Test] + public function getName(): void { - self::assertEquals('MySQL', $this->platform->getName()); + static::assertSame('MySQL', $this->platform->getName()); } - public function testGetQuoteIdentifierSymbol(): void + #[Test] + public function getQuoteIdentifierSymbol(): void { - self::assertEquals('`', $this->platform->getQuoteIdentifierSymbol()); + static::assertSame('`', $this->platform->getQuoteIdentifierSymbol()); } - public function testQuoteIdentifier(): void + #[Test] + public function getQuoteValueSymbol(): void { - self::assertEquals('`identifier`', $this->platform->quoteIdentifier('identifier')); - self::assertEquals('`ident``ifier`', $this->platform->quoteIdentifier('ident`ifier')); - self::assertEquals('`namespace:$identifier`', $this->platform->quoteIdentifier('namespace:$identifier')); + static::assertSame("'", $this->platform->getQuoteValueSymbol()); } - public function testQuoteIdentifierChain(): void + #[Test] + public function getSqlPlatformDecorator(): void { - self::assertEquals('`identifier`', $this->platform->quoteIdentifierChain('identifier')); - self::assertEquals('`identifier`', $this->platform->quoteIdentifierChain(['identifier'])); - self::assertEquals('`schema`.`identifier`', $this->platform->quoteIdentifierChain(['schema', 'identifier'])); - - self::assertEquals('`ident``ifier`', $this->platform->quoteIdentifierChain('ident`ifier')); - self::assertEquals('`ident``ifier`', $this->platform->quoteIdentifierChain(['ident`ifier'])); - self::assertEquals( - '`schema`.`ident``ifier`', - $this->platform->quoteIdentifierChain(['schema', 'ident`ifier']) - ); - } - - public function testGetQuoteValueSymbol(): void - { - self::assertEquals("'", $this->platform->getQuoteValueSymbol()); - } - - public function testQuoteValueRaisesNoticeWithoutPlatformSupport(): void - { - /** - * todo: Determine if vulnerability warning is required during unit testing - * - * todo: This testing needs expanded to cover all possible driver types - * since using \PDO currently causes a TypeError to be raised due to the - * underlying quoteViaDriver method returning false instead of ?string - */ - //$this->expectNotice(); - //$this->expectExceptionMessage( - // 'Attempting to quote a value in PhpDb\Adapter\Platform\Mysql without extension/driver support can ' - // . 'introduce security vulnerabilities in a production environment' - //); - $this->expectNotToPerformAssertions(); - $this->platform->quoteValue('value'); + static::assertInstanceOf(Platform::class, $this->platform->getSqlPlatformDecorator()); } - public function testQuoteValue(): void + #[Test] + public function quoteIdentifier(): void { - self::assertEquals("'value'", @$this->platform->quoteValue('value')); - self::assertEquals("'Foo O\\'Bar'", @$this->platform->quoteValue("Foo O'Bar")); - self::assertEquals( - '\'\\\'; DELETE FROM some_table; -- \'', - @$this->platform->quoteValue('\'; DELETE FROM some_table; -- ') - ); - self::assertEquals( - "'\\\\\\'; DELETE FROM some_table; -- '", - @$this->platform->quoteValue('\\\'; DELETE FROM some_table; -- ') - ); + static::assertSame('`identifier`', $this->platform->quoteIdentifier('identifier')); + static::assertSame('`ident``ifier`', $this->platform->quoteIdentifier('ident`ifier')); + static::assertSame('`namespace:$identifier`', $this->platform->quoteIdentifier('namespace:$identifier')); } - public function testQuoteTrustedValue(): void + #[Test] + public function quoteIdentifierChain(): void { - self::assertEquals("'value'", $this->platform->quoteTrustedValue('value')); - self::assertEquals("'Foo O\\'Bar'", $this->platform->quoteTrustedValue("Foo O'Bar")); - self::assertEquals( - '\'\\\'; DELETE FROM some_table; -- \'', - $this->platform->quoteTrustedValue('\'; DELETE FROM some_table; -- ') - ); + static::assertSame('`identifier`', $this->platform->quoteIdentifierChain('identifier')); + static::assertSame('`identifier`', $this->platform->quoteIdentifierChain(['identifier'])); + static::assertSame('`schema`.`identifier`', $this->platform->quoteIdentifierChain(['schema', 'identifier'])); - // '\\\'; DELETE FROM some_table; -- ' <- actual below - self::assertEquals( - "'\\\\\\'; DELETE FROM some_table; -- '", - $this->platform->quoteTrustedValue('\\\'; DELETE FROM some_table; -- ') + static::assertSame('`ident``ifier`', $this->platform->quoteIdentifierChain('ident`ifier')); + static::assertSame('`ident``ifier`', $this->platform->quoteIdentifierChain(['ident`ifier'])); + static::assertSame( + '`schema`.`ident``ifier`', + $this->platform->quoteIdentifierChain(['schema', 'ident`ifier']), ); } - public function testQuoteValueList(): void + #[Test] + public function quoteIdentifierInFragment(): void { - /** - * @todo Determine if vulnerability warning is required during unit testing - */ - //$this->expectError(); - //$this->expectExceptionMessage( - // 'Attempting to quote a value in PhpDb\Adapter\Platform\Mysql without extension/driver support can ' - // . 'introduce security vulnerabilities in a production environment' - //); - self::assertEquals("'Foo O\\'Bar'", $this->platform->quoteValueList("Foo O'Bar")); - } - - public function testGetIdentifierSeparator(): void - { - self::assertEquals('.', $this->platform->getIdentifierSeparator()); - } - - public function testQuoteIdentifierInFragment(): void - { - self::assertEquals('`foo`.`bar`', $this->platform->quoteIdentifierInFragment('foo.bar')); - self::assertEquals('`foo` as `bar`', $this->platform->quoteIdentifierInFragment('foo as bar')); - self::assertEquals('`$TableName`.`bar`', $this->platform->quoteIdentifierInFragment('$TableName.bar')); - self::assertEquals( + static::assertSame('`foo`.`bar`', $this->platform->quoteIdentifierInFragment('foo.bar')); + static::assertSame('`foo` as `bar`', $this->platform->quoteIdentifierInFragment('foo as bar')); + static::assertSame('`$TableName`.`bar`', $this->platform->quoteIdentifierInFragment('$TableName.bar')); + static::assertSame( '`cmis:$TableName` as `cmis:TableAlias`', - $this->platform->quoteIdentifierInFragment('cmis:$TableName as cmis:TableAlias') + $this->platform->quoteIdentifierInFragment('cmis:$TableName as cmis:TableAlias'), ); - $this->assertEquals( + static::assertSame( '`foo-bar`.`bar-foo`', - $this->platform->quoteIdentifierInFragment('foo-bar.bar-foo') + $this->platform->quoteIdentifierInFragment('foo-bar.bar-foo'), ); - $this->assertEquals( + static::assertSame( '`foo-bar` as `bar-foo`', - $this->platform->quoteIdentifierInFragment('foo-bar as bar-foo') + $this->platform->quoteIdentifierInFragment('foo-bar as bar-foo'), ); - $this->assertEquals( + static::assertSame( '`$TableName-$ColumnName`.`bar-foo`', - $this->platform->quoteIdentifierInFragment('$TableName-$ColumnName.bar-foo') + $this->platform->quoteIdentifierInFragment('$TableName-$ColumnName.bar-foo'), ); - $this->assertEquals( + static::assertSame( '`cmis:$TableName-$ColumnName` as `cmis:TableAlias-ColumnAlias`', - $this->platform->quoteIdentifierInFragment('cmis:$TableName-$ColumnName as cmis:TableAlias-ColumnAlias') + $this->platform->quoteIdentifierInFragment('cmis:$TableName-$ColumnName as cmis:TableAlias-ColumnAlias'), ); // single char words - self::assertEquals( + static::assertSame( '(`foo`.`bar` = `boo`.`baz`)', - $this->platform->quoteIdentifierInFragment('(foo.bar = boo.baz)', ['(', ')', '=']) + $this->platform->quoteIdentifierInFragment('(foo.bar = boo.baz)', ['(', ')', '=']), ); - self::assertEquals( + static::assertSame( '(`foo`.`bar`=`boo`.`baz`)', - $this->platform->quoteIdentifierInFragment('(foo.bar=boo.baz)', ['(', ')', '=']) + $this->platform->quoteIdentifierInFragment('(foo.bar=boo.baz)', ['(', ')', '=']), ); - self::assertEquals('`foo`=`bar`', $this->platform->quoteIdentifierInFragment('foo=bar', ['='])); + static::assertSame('`foo`=`bar`', $this->platform->quoteIdentifierInFragment('foo=bar', ['='])); - $this->assertEquals( + static::assertSame( '(`foo-bar`.`bar-foo` = `boo-baz`.`baz-boo`)', - $this->platform->quoteIdentifierInFragment('(foo-bar.bar-foo = boo-baz.baz-boo)', ['(', ')', '=']) + $this->platform->quoteIdentifierInFragment('(foo-bar.bar-foo = boo-baz.baz-boo)', ['(', ')', '=']), ); - $this->assertEquals( + static::assertSame( '(`foo-bar`.`bar-foo`=`boo-baz`.`baz-boo`)', - $this->platform->quoteIdentifierInFragment('(foo-bar.bar-foo=boo-baz.baz-boo)', ['(', ')', '=']) + $this->platform->quoteIdentifierInFragment('(foo-bar.bar-foo=boo-baz.baz-boo)', ['(', ')', '=']), ); - $this->assertEquals( + static::assertSame( '`foo-bar`=`bar-foo`', - $this->platform->quoteIdentifierInFragment('foo-bar=bar-foo', ['=']) + $this->platform->quoteIdentifierInFragment('foo-bar=bar-foo', ['=']), ); // case insensitive safe words - self::assertEquals( + static::assertSame( '(`foo`.`bar` = `boo`.`baz`) AND (`foo`.`baz` = `boo`.`baz`)', $this->platform->quoteIdentifierInFragment( '(foo.bar = boo.baz) AND (foo.baz = boo.baz)', - ['(', ')', '=', 'and'] - ) + ['(', ')', '=', 'and'], + ), ); - $this->assertEquals( + static::assertSame( '(`foo-bar`.`bar-foo` = `boo-baz`.`baz-boo`) AND (`foo-baz`.`baz-foo` = `boo-baz`.`baz-boo`)', $this->platform->quoteIdentifierInFragment( '(foo-bar.bar-foo = boo-baz.baz-boo) AND (foo-baz.baz-foo = boo-baz.baz-boo)', - ['(', ')', '=', 'and'] - ) + ['(', ')', '=', 'and'], + ), ); // case insensitive safe words in field - self::assertEquals( + static::assertSame( '(`foo`.`bar` = `boo`.baz) AND (`foo`.baz = `boo`.baz)', $this->platform->quoteIdentifierInFragment( '(foo.bar = boo.baz) AND (foo.baz = boo.baz)', - ['(', ')', '=', 'and', 'bAz'] - ) + ['(', ')', '=', 'and', 'bAz'], + ), ); // case insensitive safe words in field - $this->assertEquals( + static::assertSame( '(`foo-bar`.`bar-foo` = `boo-baz`.baz-boo) AND (`foo-baz`.`baz-foo` = `boo-baz`.baz-boo)', $this->platform->quoteIdentifierInFragment( '(foo-bar.bar-foo = boo-baz.baz-boo) AND (foo-baz.baz-foo = boo-baz.baz-boo)', - ['(', ')', '=', 'and', 'bAz-BOo'] - ) + ['(', ')', '=', 'and', 'bAz-BOo'], + ), ); } + + #[Test] + public function quoteTrustedValue(): void + { + static::assertSame("'value'", $this->platform->quoteTrustedValue('value')); + static::assertSame("'Foo O\\'Bar'", $this->platform->quoteTrustedValue("Foo O'Bar")); + static::assertSame( + '\'\\\'; DELETE FROM some_table; -- \'', + $this->platform->quoteTrustedValue('\'; DELETE FROM some_table; -- '), + ); + + // '\\\'; DELETE FROM some_table; -- ' <- actual below + static::assertSame( + "'\\\\\\'; DELETE FROM some_table; -- '", + $this->platform->quoteTrustedValue('\\\'; DELETE FROM some_table; -- '), + ); + } + + #[Test] + public function quoteValue(): void + { + static::assertSame("'value'", $this->platform->quoteValue('value')); + static::assertSame("'Foo O\\'Bar'", $this->platform->quoteValue("Foo O'Bar")); + static::assertSame( + '\'\\\'; DELETE FROM some_table; -- \'', + $this->platform->quoteValue('\'; DELETE FROM some_table; -- '), + ); + static::assertSame( + "'\\\\\\'; DELETE FROM some_table; -- '", + $this->platform->quoteValue('\\\'; DELETE FROM some_table; -- '), + ); + } + + #[Test] + public function quoteValueList(): void + { + /** + * @todo Determine if vulnerability warning is required during unit testing + */ + //$this->expectError(); + //$this->expectExceptionMessage( + // 'Attempting to quote a value in PhpDb\Adapter\Platform\Mysql without extension/driver support can ' + // . 'introduce security vulnerabilities in a production environment' + //); + static::assertSame("'Foo O\\'Bar'", $this->platform->quoteValueList("Foo O'Bar")); + } + + #[Test] + public function quoteValueRaisesNoticeWithoutPlatformSupport(): void + { + /** + * todo(@tyrsson): Determine if vulnerability warning is required during unit testing + * + * todo(@tyrsson): This testing needs expanded to cover all possible driver types + * since using \PDO currently causes a TypeError to be raised due to the + * underlying quoteViaDriver method returning false instead of ?string + */ + //$this->expectNotice(); + //$this->expectExceptionMessage( + // 'Attempting to quote a value in PhpDb\Adapter\Platform\Mysql without extension/driver support can ' + // . 'introduce security vulnerabilities in a production environment' + //); + $this->expectNotToPerformAssertions(); + $this->platform->quoteValue('value'); + } + + #[Test] + public function quoteViaDriverWithMysqli(): void + { + $mysqli = $this->createMock(mysqli::class); + $mysqli->expects($this->once()) + ->method('real_escape_string') + ->with("a'b") + ->willReturn("a\\'b"); + + $platform = new AdapterPlatform($mysqli); + + static::assertSame("'a\\'b'", $platform->quoteValue("a'b")); + } + + #[Test] + public function quoteViaDriverWithPdo(): void + { + $pdo = $this->createMock(PDO::class); + $pdo->expects($this->once()) + ->method('quote') + ->with("a'b") + ->willReturn("'a''b'"); + + $platform = new AdapterPlatform($pdo); + + static::assertSame("'a''b'", $platform->quoteValue("a'b")); + } + + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + */ + #[Override] + protected function setUp(): void + { + $pdo = new Driver( + $this->createStub(AbstractPdoConnection::class), + $this->createStub(Statement::class), + $this->createStub(Result::class), + ); + $this->platform = new AdapterPlatform($pdo); + } } diff --git a/test/unit/ConfigProviderTest.php b/test/unit/ConfigProviderTest.php new file mode 100644 index 0000000..5fd24d6 --- /dev/null +++ b/test/unit/ConfigProviderTest.php @@ -0,0 +1,61 @@ +getDependencies(); + + static::assertSame(['aliases', 'factories'], array_keys($dependencies)); + + $aliases = $dependencies['aliases']; + static::assertSame(Driver::class, $aliases['Mysqli']); + static::assertSame(Pdo\Driver::class, $aliases['PDO_MySQL']); + static::assertSame(Driver::class, $aliases[DriverInterface::class]); + static::assertSame(Pdo\Driver::class, $aliases[PdoDriverInterface::class]); + static::assertSame(Source::class, $aliases[MetadataInterface::class]); + + $factories = $dependencies['factories']; + static::assertSame(Container\DriverInterfaceFactory::class, $factories[Driver::class]); + static::assertSame(Container\ConnectionInterfaceFactory::class, $factories[Connection::class]); + static::assertSame(Container\StatementInterfaceFactory::class, $factories[Statement::class]); + static::assertSame(Container\PdoDriverInterfaceFactory::class, $factories[Pdo\Driver::class]); + static::assertSame(Container\PdoConnectionInterfaceFactory::class, $factories[Pdo\Connection::class]); + static::assertSame(Container\MetadataInterfaceFactory::class, $factories[Source::class]); + static::assertSame(Container\PdoStatementFactory::class, $factories[PdoStatement::class]); + static::assertSame(Container\PlatformInterfaceFactory::class, $factories[PlatformInterface::class]); + } + + #[Test] + public function invokeWrapsDependencies(): void + { + $provider = new ConfigProvider(); + + static::assertSame(['dependencies' => $provider->getDependencies()], $provider()); + } +} diff --git a/test/unit/ConnectionTest.php b/test/unit/ConnectionTest.php index d1f7e2a..def32ff 100644 --- a/test/unit/ConnectionTest.php +++ b/test/unit/ConnectionTest.php @@ -4,68 +4,60 @@ namespace PhpDbTest\Mysql; +use Exception; use mysqli; use Override; -use PhpDb\Exception\RuntimeException; use PhpDb\Mysql\Connection; use PhpDb\Mysql\Driver; use PhpDb\Mysql\Result; use PhpDb\Mysql\Statement; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\RequiresPhpExtension; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use TypeError; use const MYSQLI_CLIENT_SSL; use const MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT; #[RequiresPhpExtension('mysqli')] #[CoversMethod(Connection::class, 'setDriver')] -#[CoversMethod(Connection::class, 'setConnectionParameters')] -#[CoversMethod(Connection::class, 'getConnectionParameters')] +#[CoversMethod(Connection::class, 'connect')] final class ConnectionTest extends TestCase { - protected Connection $connection; + // fake test-only credential, not a real secret + // @mago-expect lint:no-literal-password + private const string TEST_PASSWORD = '1234'; - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - #[Override] - protected function setUp(): void - { - // if (! (bool) getenv('TESTS_PHPDB_ADAPTER_MYSQL')) { - // $this->markTestSkipped('Mysqli test disabled'); - // } - $this->connection = new Connection([]); - } + protected Connection $connection; - /** - * Tears down the fixture, for example, closes a network connection. - * This method is called after a test is executed. - */ - protected function tearDown(): void + #[Test] + public function connectionFails(): void { - } + $mysqli = $this->getMockBuilder(mysqli::class)->getMock(); + $mysqli->expects($this->once()) + ->method('real_connect') + ->willThrowException(new Exception('simulated connection failure')); - public function testSetDriver(): void - { - $driver = new Driver($this->connection, new Statement(), new Result()); - self::assertSame($this->connection, $this->connection->setDriver($driver)); - } + $connection = $this->createMockConnection($mysqli, []); - public function testSetConnectionParameters(): void - { - self::assertEquals($this->connection, $this->connection->setConnectionParameters([])); + $this->expectException(TypeError::class); + $this->expectExceptionMessage( + 'Exception::__construct(): Argument #1 ($message) must be of type string, null given', + ); + $connection->connect(); } - public function testGetConnectionParameters(): void + #[Test] + public function getConnectionParameters(): void { $this->connection->setConnectionParameters(['foo' => 'bar']); - self::assertEquals(['foo' => 'bar'], $this->connection->getConnectionParameters()); + static::assertEquals(['foo' => 'bar'], $this->connection->getConnectionParameters()); } - public function testNonSecureConnection(): void + #[Test] + public function nonSecureConnection(): void { $mysqli = $this->createMockMysqli(0); /** @var Connection&MockObject $connection */ @@ -74,16 +66,30 @@ public function testNonSecureConnection(): void [ 'hostname' => 'localhost', 'username' => 'superuser', - 'password' => '1234', + 'password' => self::TEST_PASSWORD, 'database' => 'main', 'port' => 123, - ] + ], ); $connection->connect(); } - public function testSslConnection(): void + #[Test] + public function setConnectionParameters(): void + { + static::assertEquals($this->connection, $this->connection->setConnectionParameters([])); + } + + #[Test] + public function setDriver(): void + { + $driver = new Driver($this->connection, new Statement(), new Result()); + static::assertSame($this->connection, $this->connection->setDriver($driver)); + } + + #[Test] + public function sslConnection(): void { $mysqli = $this->createMockMysqli(MYSQLI_CLIENT_SSL); /** @var Connection&MockObject $connection */ @@ -92,17 +98,18 @@ public function testSslConnection(): void [ 'hostname' => 'localhost', 'username' => 'superuser', - 'password' => '1234', + 'password' => self::TEST_PASSWORD, 'database' => 'main', 'port' => 123, 'use_ssl' => true, - ] + ], ); $connection->connect(); } - public function testSslConnectionNoVerify(): void + #[Test] + public function sslConnectionNoVerify(): void { $mysqli = $this->createMockMysqli(MYSQLI_CLIENT_SSL | MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT); /** @var Connection&MockObject $connection */ @@ -111,26 +118,36 @@ public function testSslConnectionNoVerify(): void [ 'hostname' => 'localhost', 'username' => 'superuser', - 'password' => '1234', + 'password' => self::TEST_PASSWORD, 'database' => 'main', 'port' => 123, 'use_ssl' => true, 'driver_options' => [ MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT => true, ], - ] + ], ); $connection->connect(); } - public function testConnectionFails(): void + /** + * Create a mock connection + * + * @param MockObject $mysqli Mock mysqli object + * @param array $params Connection params + */ + protected function createMockConnection(MockObject $mysqli, array $params): MockObject { - $connection = new Connection([]); + $connection = $this->getMockBuilder(Connection::class) + ->onlyMethods(['createResource']) + ->setConstructorArgs([$params]) + ->getMock(); + $connection->expects($this->once()) + ->method('createResource') + ->willReturn($mysqli); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Connection error'); - $connection->connect(); + return $connection; } /** @@ -148,10 +165,10 @@ protected function createMockMysqli(int $flags): MockObject $this->equalTo(''), $this->equalTo(''), $this->equalTo(''), - $this->equalTo('') + $this->equalTo(''), ); - if ($flags === 0) { + if (0 === $flags) { // Do not pass $flags argument if invalid flags provided $mysqli->expects($this->once()) ->method('real_connect') @@ -161,7 +178,7 @@ protected function createMockMysqli(int $flags): MockObject $this->equalTo('1234'), $this->equalTo('main'), $this->equalTo(123), - $this->equalTo('') + $this->equalTo(''), ) ->willReturn(true); return $mysqli; @@ -176,7 +193,7 @@ protected function createMockMysqli(int $flags): MockObject $this->equalTo('main'), $this->equalTo(123), $this->equalTo(''), - $this->equalTo($flags) + $this->equalTo($flags), ) ->willReturn(true); @@ -184,21 +201,21 @@ protected function createMockMysqli(int $flags): MockObject } /** - * Create a mock connection - * - * @param MockObject $mysqli Mock mysqli object - * @param array $params Connection params + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. */ - protected function createMockConnection(MockObject $mysqli, array $params): MockObject + #[Override] + protected function setUp(): void { - $connection = $this->getMockBuilder(Connection::class) - ->onlyMethods(['createResource']) - ->setConstructorArgs([$params]) - ->getMock(); - $connection->expects($this->once()) - ->method('createResource') - ->willReturn($mysqli); - - return $connection; + // if (! (bool) getenv('TESTS_PHPDB_ADAPTER_MYSQL')) { + // $this->markTestSkipped('Mysqli test disabled'); + // } + $this->connection = new Connection([]); } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + protected function tearDown(): void {} } diff --git a/test/unit/DriverTest.php b/test/unit/DriverTest.php new file mode 100644 index 0000000..b52e36f --- /dev/null +++ b/test/unit/DriverTest.php @@ -0,0 +1,58 @@ +createStub(Connection::class); + $connection->method('getLastGeneratedValue')->willReturn(42); + + $driver = new Driver($connection); + + static::assertTrue($driver->checkEnvironment()); + static::assertSame($connection, $driver->getConnection()); + static::assertSame('?', $driver->formatParameterName('name')); + static::assertSame(DriverInterface::PARAMETERIZATION_POSITIONAL, $driver->getPrepareType()); + static::assertSame(42, $driver->getLastGeneratedValue()); + static::assertNull($driver->getProfiler()); + static::assertInstanceOf(Statement::class, $driver->getStatementPrototype()); + static::assertInstanceOf(Result::class, $driver->getResultPrototype()); + } + + #[Test] + public function setProfiler(): void + { + $driver = new Driver($this->createStub(Connection::class)); + $profiler = $this->createStub(ProfilerInterface::class); + + static::assertSame($driver, $driver->setProfiler($profiler)); + static::assertSame($profiler, $driver->getProfiler()); + } +} diff --git a/test/unit/Pdo/ConnectionTest.php b/test/unit/Pdo/ConnectionTest.php index 61be739..2ff0830 100644 --- a/test/unit/Pdo/ConnectionTest.php +++ b/test/unit/Pdo/ConnectionTest.php @@ -4,58 +4,23 @@ namespace PhpDbTest\Mysql\Pdo; -use Exception; use Override; use PhpDb\Adapter\Exception\InvalidConnectionParametersException; use PhpDb\Adapter\Exception\RuntimeException; use PhpDb\Mysql\Pdo\Connection; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -#[CoversMethod(Connection::class, 'getResource')] -#[CoversMethod(Connection::class, 'getDsn')] +#[CoversMethod(Connection::class, 'connect')] final class ConnectionTest extends TestCase { protected Connection $connection; - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - #[Override] - protected function setUp(): void - { - $this->connection = new Connection([]); - } - - /** - * Test getResource method tries to connect to the database, it should never return null - */ - public function testResource(): void - { - $this->expectException(RuntimeException::class); - $this->connection->getResource(); - } - - /** - * Test getConnectedDsn returns a DSN string if it has been set - */ - public function testGetDsn(): void - { - $dsn = "mysql:"; - $this->connection->setConnectionParameters(['dsn' => $dsn]); - try { - $this->connection->connect(); - } catch (Exception) { - } - $responseString = $this->connection->getDsn(); - - self::assertEquals($dsn, $responseString); - } - + #[Test] #[Group('2622')] - public function testArrayOfConnectionParametersCreatesCorrectDsn(): void + public function arrayOfConnectionParametersCreatesCorrectDsn(): void { $connection = new Connection([ 'driver' => 'pdo_mysql', @@ -66,22 +31,44 @@ public function testArrayOfConnectionParametersCreatesCorrectDsn(): void ]); try { $connection->connect(); - } catch (Exception) { + } catch (InvalidConnectionParametersException|RuntimeException) { + // connection failure is expected/ignored here; only dsn construction is under test + // @mago-expect lint:no-empty-catch-clause } $responseString = $connection->getDsn(); - self::assertStringStartsWith('mysql:', $responseString); - self::assertStringContainsString('charset=utf8', $responseString); - self::assertStringContainsString('dbname=foo', $responseString); - self::assertStringContainsString('port=3306', $responseString); - self::assertStringContainsString('unix_socket=/var/run/mysqld/mysqld.sock', $responseString); + static::assertStringStartsWith('mysql:', $responseString); + static::assertStringContainsString('charset=utf8', $responseString); + static::assertStringContainsString('dbname=foo', $responseString); + static::assertStringContainsString('port=3306', $responseString); + static::assertStringContainsString('unix_socket=/var/run/mysqld/mysqld.sock', $responseString); } - public function testHostnameAndUnixSocketThrowsInvalidConnectionParametersException(): void + /** + * Test getConnectedDsn returns a DSN string if it has been set + */ + #[Test] + public function getDsn(): void + { + $dsn = 'mysql:'; + $this->connection->setConnectionParameters(['dsn' => $dsn]); + try { + $this->connection->connect(); + } catch (InvalidConnectionParametersException|RuntimeException) { + // connection failure is expected/ignored here; only dsn construction is under test + // @mago-expect lint:no-empty-catch-clause + } + $responseString = $this->connection->getDsn(); + + static::assertEquals($dsn, $responseString); + } + + #[Test] + public function hostnameAndUnixSocketThrowsInvalidConnectionParametersException(): void { $this->expectException(InvalidConnectionParametersException::class); $this->expectExceptionMessage( - 'Ambiguous connection parameters, both hostname and unix_socket parameters were set' + 'Ambiguous connection parameters, both hostname and unix_socket parameters were set', ); $connection = new Connection([ @@ -93,4 +80,24 @@ public function testHostnameAndUnixSocketThrowsInvalidConnectionParametersExcept ]); $connection->connect(); } + + /** + * Test getResource method tries to connect to the database, it should never return null + */ + #[Test] + public function resource(): void + { + $this->expectException(RuntimeException::class); + $this->connection->getResource(); + } + + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + */ + #[Override] + protected function setUp(): void + { + $this->connection = new Connection([]); + } } diff --git a/test/unit/Pdo/ConnectionTransactionsTest.php b/test/unit/Pdo/ConnectionTransactionsTest.php index d09db25..8de4f54 100644 --- a/test/unit/Pdo/ConnectionTransactionsTest.php +++ b/test/unit/Pdo/ConnectionTransactionsTest.php @@ -5,118 +5,108 @@ namespace PhpDbTest\Mysql\Pdo; use Override; -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 */ #[CoversClass(Connection::class)] -#[CoversClass(AbstractConnection::class)] -#[CoversMethod(Connection::class, 'beginTransaction')] -#[CoversMethod(Connection::class, 'inTransaction')] -#[CoversMethod(Connection::class, 'commit')] -#[CoversMethod(Connection::class, 'rollback')] final class ConnectionTransactionsTest extends TestCase { - protected ConnectionWrapper $wrapper; + protected Connection $wrapper; - /** - * {@inheritDoc} - */ - #[Override] - protected function setUp(): void + #[Test] + public function beginTransactionReturnsInstanceOfConnection(): void { - $this->wrapper = new ConnectionWrapper(); + static::assertInstanceOf(Connection::class, $this->wrapper->beginTransaction()); } - public function testBeginTransactionReturnsInstanceOfConnection(): void - { - self::assertInstanceOf(Connection::class, $this->wrapper->beginTransaction()); - } - - public function testBeginTransactionSetsInTransactionAtTrue(): void + #[Test] + public function beginTransactionSetsInTransactionAtTrue(): void { $this->wrapper->beginTransaction(); - self::assertTrue($this->wrapper->inTransaction()); + static::assertTrue($this->wrapper->inTransaction()); } - public function testCommitReturnsInstanceOfConnection(): void + #[Test] + public function commitReturnsInstanceOfConnection(): void { $this->wrapper->beginTransaction(); - self::assertInstanceOf(Connection::class, $this->wrapper->commit()); + static::assertInstanceOf(Connection::class, $this->wrapper->commit()); } - public function testCommitSetsInTransactionAtFalse(): void + #[Test] + public function commitSetsInTransactionAtFalse(): void { $this->wrapper->beginTransaction(); $this->wrapper->commit(); - self::assertFalse($this->wrapper->inTransaction()); + static::assertFalse($this->wrapper->inTransaction()); } /** * Standalone commit after a SET autocommit=0; */ - public function testCommitWithoutBeginReturnsInstanceOfConnection(): void + #[Test] + public function commitWithoutBeginReturnsInstanceOfConnection(): void { - self::assertInstanceOf(Connection::class, $this->wrapper->commit()); + static::assertInstanceOf(Connection::class, $this->wrapper->commit()); } - public function testNestedTransactionsCommit(): void + #[Test] + public function nestedTransactionsCommit(): void { - $nested = 0; - - self::assertFalse($this->wrapper->inTransaction()); + static::assertFalse($this->wrapper->inTransaction()); // 1st transaction $this->wrapper->beginTransaction(); - self::assertTrue($this->wrapper->inTransaction()); - self::assertSame(++$nested, $this->wrapper->getNestedTransactionsCount()); + static::assertTrue($this->wrapper->inTransaction()); + static::assertSame(1, $this->getNestedTransactionsCount($this->wrapper)); // 2nd transaction $this->wrapper->beginTransaction(); - self::assertTrue($this->wrapper->inTransaction()); - self::assertSame(++$nested, $this->wrapper->getNestedTransactionsCount()); + static::assertTrue($this->wrapper->inTransaction()); + static::assertSame(2, $this->getNestedTransactionsCount($this->wrapper)); // 1st commit $this->wrapper->commit(); - self::assertTrue($this->wrapper->inTransaction()); - self::assertSame(--$nested, $this->wrapper->getNestedTransactionsCount()); + static::assertTrue($this->wrapper->inTransaction()); + static::assertSame(1, $this->getNestedTransactionsCount($this->wrapper)); // 2nd commit $this->wrapper->commit(); - self::assertFalse($this->wrapper->inTransaction()); - self::assertSame(--$nested, $this->wrapper->getNestedTransactionsCount()); + static::assertFalse($this->wrapper->inTransaction()); + static::assertSame(0, $this->getNestedTransactionsCount($this->wrapper)); } - public function testNestedTransactionsRollback(): void + #[Test] + public function nestedTransactionsRollback(): void { - $nested = 0; - - self::assertFalse($this->wrapper->inTransaction()); + static::assertFalse($this->wrapper->inTransaction()); // 1st transaction $this->wrapper->beginTransaction(); - self::assertTrue($this->wrapper->inTransaction()); - self::assertSame(++$nested, $this->wrapper->getNestedTransactionsCount()); + static::assertTrue($this->wrapper->inTransaction()); + static::assertSame(1, $this->getNestedTransactionsCount($this->wrapper)); // 2nd transaction $this->wrapper->beginTransaction(); - self::assertTrue($this->wrapper->inTransaction()); - self::assertSame(++$nested, $this->wrapper->getNestedTransactionsCount()); + static::assertTrue($this->wrapper->inTransaction()); + static::assertSame(2, $this->getNestedTransactionsCount($this->wrapper)); // Rollback $this->wrapper->rollback(); - self::assertFalse($this->wrapper->inTransaction()); - self::assertSame(0, $this->wrapper->getNestedTransactionsCount()); + static::assertFalse($this->wrapper->inTransaction()); + static::assertSame(0, $this->getNestedTransactionsCount($this->wrapper)); } - public function testRollbackDisconnectedThrowsException(): void + #[Test] + public function rollbackDisconnectedThrowsException(): void { $this->wrapper->disconnect(); @@ -125,20 +115,23 @@ public function testRollbackDisconnectedThrowsException(): void $this->wrapper->rollback(); } - public function testRollbackReturnsInstanceOfConnection(): void + #[Test] + public function rollbackReturnsInstanceOfConnection(): void { $this->wrapper->beginTransaction(); - self::assertInstanceOf(Connection::class, $this->wrapper->rollback()); + static::assertInstanceOf(Connection::class, $this->wrapper->rollback()); } - public function testRollbackSetsInTransactionAtFalse(): void + #[Test] + public function rollbackSetsInTransactionAtFalse(): void { $this->wrapper->beginTransaction(); $this->wrapper->rollback(); - self::assertFalse($this->wrapper->inTransaction()); + static::assertFalse($this->wrapper->inTransaction()); } - public function testRollbackWithoutBeginThrowsException(): void + #[Test] + public function rollbackWithoutBeginThrowsException(): void { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Must call beginTransaction() before you can rollback'); @@ -148,14 +141,35 @@ public function testRollbackWithoutBeginThrowsException(): void /** * Standalone commit after a SET autocommit=0; */ - public function testStandaloneCommit(): void + #[Test] + public function standaloneCommit(): void { - self::assertFalse($this->wrapper->inTransaction()); - self::assertSame(0, $this->wrapper->getNestedTransactionsCount()); + static::assertFalse($this->wrapper->inTransaction()); + static::assertSame(0, $this->getNestedTransactionsCount($this->wrapper)); $this->wrapper->commit(); - self::assertFalse($this->wrapper->inTransaction()); - self::assertSame(0, $this->wrapper->getNestedTransactionsCount()); + static::assertFalse($this->wrapper->inTransaction()); + static::assertSame(0, $this->getNestedTransactionsCount($this->wrapper)); + } + + /** + * {@inheritDoc} + */ + #[Override] + protected function setUp(): void + { + $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 fe8e188..5ec6f36 100644 --- a/test/unit/Pdo/DriverTest.php +++ b/test/unit/Pdo/DriverTest.php @@ -5,104 +5,122 @@ namespace PhpDbTest\Mysql\Pdo; use Override; +use PDO; 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; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -#[CoversMethod(Driver::class, 'getResultPrototype')] +#[CoversMethod(Driver::class, '__construct')] #[CoversMethod(Driver::class, 'createResult')] final class DriverTest extends TestCase { protected Driver $pdo; - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - #[Override] - protected function setUp(): void + /** @psalm-return array */ + public static function getInvalidParamName(): array { - $connection = $this->createMock(Connection::class); - $statement = $this->createMock(Statement::class); - $result = $this->createMock(Result::class); - $this->pdo = new Driver( - $connection, - $statement, - $result - ); + return [ + ['foo%'], + ['foo-'], + ['foo$'], + ['foo0!'], + ]; } /** @psalm-return array */ public static function getParamsAndType(): array { return [ - ['foo', null, ':foo'], - ['foo_bar', null, ':foo_bar'], - ['123foo', null, ':123foo'], - [1, null, '?'], - ['1', null, '?'], - ['foo', DriverInterface::PARAMETERIZATION_NAMED, ':foo'], + ['foo', null, ':foo'], + ['foo_bar', null, ':foo_bar'], + ['123foo', null, ':123foo'], + [1, null, '?'], + ['1', null, '?'], + ['foo', DriverInterface::PARAMETERIZATION_NAMED, ':foo'], ['foo_bar', DriverInterface::PARAMETERIZATION_NAMED, ':foo_bar'], - ['123foo', DriverInterface::PARAMETERIZATION_NAMED, ':123foo'], - [1, DriverInterface::PARAMETERIZATION_NAMED, ':1'], - ['1', DriverInterface::PARAMETERIZATION_NAMED, ':1'], - [':foo', null, ':foo'], + ['123foo', DriverInterface::PARAMETERIZATION_NAMED, ':123foo'], + [1, DriverInterface::PARAMETERIZATION_NAMED, ':1'], + ['1', DriverInterface::PARAMETERIZATION_NAMED, ':1'], + [':foo', null, ':foo'], ]; } - #[DataProvider('getParamsAndType')] - public function testFormatParameterName(int|string $name, ?string $type, string $expected): void + #[Test] + public function constructorWithPdoConnection(): void { - $result = $this->pdo->formatParameterName($name, $type); - $this->assertEquals($expected, $result); + $driver = new Driver( + $this->createStub(PDO::class), + new Statement(), + new Result(), + ); + + static::assertInstanceOf(Driver::class, $driver); } - /** @psalm-return array */ - public static function getInvalidParamName(): array + #[Test] + public function createResultPassesNullRowCount(): void { - return [ - ['foo%'], - ['foo-'], - ['foo$'], - ['foo0!'], - ]; + $pdoStatement = $this->getMockBuilder(PDOStatement::class)->getMock(); + $pdoStatement->expects($this->once()) + ->method('rowCount') + ->willReturn(4); + + $connection = $this->createStub(AbstractPdoConnection::class); + $statement = $this->createStub(Statement::class); + $driver = new Driver($connection, $statement, new Result()); + + $result = $driver->createResult($pdoStatement); + + static::assertInstanceOf(Result::class, $result); + static::assertSame(4, $result->count()); + } + + #[Test] + #[DataProvider('getParamsAndType')] + public function formatParameterName(int|string $name, ?string $type, string $expected): void + { + $result = $this->pdo->formatParameterName($name, $type); + static::assertEquals($expected, $result); } + #[Test] #[DataProvider('getInvalidParamName')] - public function testFormatParameterNameWithInvalidCharacters(string $name): void + public function formatParameterNameWithInvalidCharacters(string $name): void { $this->expectException(RuntimeException::class); $this->pdo->formatParameterName($name); } - public function testGetResultPrototype(): void + #[Test] + public function getResultPrototype(): void { $resultPrototype = $this->pdo->getResultPrototype(); - self::assertInstanceOf(Result::class, $resultPrototype); + static::assertInstanceOf(Result::class, $resultPrototype); } - public function testCreateResultPassesNullRowCount(): void + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + */ + #[Override] + protected function setUp(): void { - $pdoStatement = $this->getMockBuilder(PDOStatement::class)->getMock(); - $pdoStatement->expects($this->once()) - ->method('rowCount') - ->willReturn(4); - - $connection = $this->createMock(Connection::class); - $statement = $this->createMock(Statement::class); - $driver = new Driver($connection, $statement, new Result()); - - $result = $driver->createResult($pdoStatement); - - self::assertInstanceOf(Result::class, $result); - self::assertSame(4, $result->count()); + $connection = $this->createStub(AbstractPdoConnection::class); + $statement = $this->createStub(Statement::class); + $result = $this->createStub(Result::class); + $this->pdo = new Driver( + $connection, + $statement, + $result, + ); } } diff --git a/test/unit/Pdo/ResultTest.php b/test/unit/Pdo/ResultTest.php index 34b2685..c269a94 100644 --- a/test/unit/Pdo/ResultTest.php +++ b/test/unit/Pdo/ResultTest.php @@ -8,78 +8,47 @@ use PDOStatement; use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Exception\InvalidArgumentException; -use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use stdClass; use function assert; use function uniqid; -#[CoversMethod(Result::class, 'current')] -#[CoversMethod(Result::class, 'count')] +#[CoversNothing] #[Group('result-pdo')] final class ResultTest extends TestCase { - /** - * Tests current method returns same data on consecutive calls. - */ - public function testCurrent(): void + #[Test] + public function countWithClosureRowCountInvokesClosure(): void { $mock = $this->getMockBuilder(PDOStatement::class)->getMock(); - $mock->expects($this->any()) - ->method('fetch') - ->willReturnCallback(fn() => uniqid()); - - $result = new Result(); - $result->initialize($mock, null); - - self::assertEquals($result->current(), $result->current()); - } + $mock->expects($this->never()) + ->method('rowCount'); - public function testFetchModeException(): void - { $result = new Result(); + $result->initialize($mock, null, static fn() => 3); - $this->expectException(InvalidArgumentException::class); - $result->setFetchMode(13); + static::assertSame(3, $result->count()); } - /** - * Tests whether the fetch mode was set properly and - */ - public function testFetchModeAnonymousObject(): void + #[Test] + public function countWithIntRowCountReturnsValueWithoutQueryingPdo(): void { $mock = $this->getMockBuilder(PDOStatement::class)->getMock(); - $mock->expects($this->any()) - ->method('fetch') - ->willReturnCallback(fn() => new stdClass()); + $mock->expects($this->never()) + ->method('rowCount'); $result = new Result(); - $result->initialize($mock, null); - $result->setFetchMode(PDO::FETCH_OBJ); - - self::assertEquals(5, $result->getFetchMode()); - self::assertInstanceOf('stdClass', $result->current()); - } + $result->initialize($mock, null, 7); - /** - * Tests whether the fetch mode has a broader range - */ - public function testFetchModeRange(): void - { - $mock = $this->getMockBuilder(PDOStatement::class)->getMock(); - $mock->expects($this->any()) - ->method('fetch') - ->willReturnCallback(fn() => new stdClass()); - $result = new Result(); - $result->initialize($mock, null); - $result->setFetchMode(PDO::FETCH_NAMED); - self::assertEquals(11, $result->getFetchMode()); - self::assertInstanceOf('stdClass', $result->current()); + static::assertSame(7, $result->count()); } - public function testCountWithNullRowCountDelegatesToPdoStatement(): void + #[Test] + public function countWithNullRowCountDelegatesToPdoStatement(): void { $mock = $this->getMockBuilder(PDOStatement::class)->getMock(); $mock->expects($this->once()) @@ -89,10 +58,11 @@ public function testCountWithNullRowCountDelegatesToPdoStatement(): void $result = new Result(); $result->initialize($mock, null, null); - self::assertSame(4, $result->count()); + static::assertSame(4, $result->count()); } - public function testCountWithZeroRowCountReturnsZeroWithoutQueryingPdo(): void + #[Test] + public function countWithZeroRowCountReturnsZeroWithoutQueryingPdo(): void { $mock = $this->getMockBuilder(PDOStatement::class)->getMock(); $mock->expects($this->never()) @@ -101,46 +71,82 @@ public function testCountWithZeroRowCountReturnsZeroWithoutQueryingPdo(): void $result = new Result(); $result->initialize($mock, null, 0); - self::assertSame(0, $result->count()); + static::assertSame(0, $result->count()); } - public function testCountWithIntRowCountReturnsValueWithoutQueryingPdo(): void + /** + * Tests current method returns same data on consecutive calls. + */ + #[Test] + public function current(): void { - $mock = $this->getMockBuilder(PDOStatement::class)->getMock(); - $mock->expects($this->never()) - ->method('rowCount'); + $mock = $this->createStub(PDOStatement::class); + $mock->method('fetch') + // @mago-expect lint:prefer-first-class-callable + ->willReturnCallback(static fn() => uniqid()); $result = new Result(); - $result->initialize($mock, null, 7); + $result->initialize($mock, null); - self::assertSame(7, $result->count()); + static::assertEquals($result->current(), $result->current()); } - public function testCountWithClosureRowCountInvokesClosure(): void + /** + * Tests whether the fetch mode was set properly and + */ + #[Test] + public function fetchModeAnonymousObject(): void { - $mock = $this->getMockBuilder(PDOStatement::class)->getMock(); - $mock->expects($this->never()) - ->method('rowCount'); + $mock = $this->createStub(PDOStatement::class); + $mock->method('fetch') + ->willReturnCallback(static fn() => new stdClass()); + + $result = new Result(); + $result->initialize($mock, null); + $result->setFetchMode(PDO::FETCH_OBJ); + + static::assertSame(5, $result->getFetchMode()); + static::assertInstanceOf('stdClass', $result->current()); + } + #[Test] + public function fetchModeException(): void + { $result = new Result(); - $result->initialize($mock, null, fn() => 3); - self::assertSame(3, $result->count()); + $this->expectException(InvalidArgumentException::class); + $result->setFetchMode(13); + } + + /** + * Tests whether the fetch mode has a broader range + */ + #[Test] + public function fetchModeRange(): void + { + $mock = $this->createStub(PDOStatement::class); + $mock->method('fetch') + ->willReturnCallback(static fn() => new stdClass()); + $result = new Result(); + $result->initialize($mock, null); + $result->setFetchMode(PDO::FETCH_NAMED); + static::assertSame(11, $result->getFetchMode()); + static::assertInstanceOf('stdClass', $result->current()); } - public function testMultipleRewind(): void + #[Test] + public function multipleRewind(): void { - $data = [ + $data = [ ['test' => 1], ['test' => 2], ]; $position = 0; - $mock = $this->getMockBuilder(PDOStatement::class)->getMock(); - assert($mock instanceof PDOStatement); // to suppress IDE type warnings - $mock->expects($this->any()) - ->method('fetch') - ->willReturnCallback(function () use ($data, &$position) { + $mock = $this->createStub(PDOStatement::class); + assert($mock instanceof PDOStatement, description: 'to suppress IDE type warnings'); + $mock->method('fetch') + ->willReturnCallback(static function () use ($data, &$position) { return $data[$position++]; }); $result = new Result(); @@ -149,13 +155,13 @@ public function testMultipleRewind(): void $result->rewind(); $result->rewind(); - $this->assertEquals(0, $result->key()); - $this->assertEquals(1, $position); - $this->assertEquals($data[0], $result->current()); + static::assertSame(0, $result->key()); + static::assertSame(1, $position); + static::assertEquals($data[0], $result->current()); $result->next(); - $this->assertEquals(1, $result->key()); - $this->assertEquals(2, $position); - $this->assertEquals($data[1], $result->current()); + static::assertSame(1, $result->key()); + static::assertSame(2, $position); + static::assertEquals($data[1], $result->current()); } } diff --git a/test/unit/Pdo/StatementIntegrationTest.php b/test/unit/Pdo/StatementIntegrationTest.php index 1d3eebd..cdbd205 100644 --- a/test/unit/Pdo/StatementIntegrationTest.php +++ b/test/unit/Pdo/StatementIntegrationTest.php @@ -8,13 +8,14 @@ use PDO; use PDOStatement; use PhpDb\Adapter\Driver\Pdo\Statement; -use PhpDb\Mysql\Pdo\Driver as PdoDriver; -use PhpDbTest\Mysql\Pdo\TestAsset; -use PHPUnit\Framework\Attributes\CoversMethod; +use PhpDb\Adapter\Driver\PdoDriverInterface; +use PhpDb\Adapter\Driver\ResultInterface; +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -#[CoversMethod(Statement::class, 'execute')] +#[CoversNothing] final class StatementIntegrationTest extends TestCase { protected Statement $statement; @@ -22,6 +23,62 @@ final class StatementIntegrationTest extends TestCase /** @var MockObject */ protected PDOStatement|MockObject $pdoStatementMock; + #[Test] + public function statementExecuteWillConvertPhpBoolToPdoBoolWhenBinding(): void + { + $this->pdoStatementMock + ->expects($this->any()) + ->method('bindParam') + ->with( + static::equalTo(':foo'), + static::equalTo(false), + static::equalTo(PDO::PARAM_BOOL), + ); + $this->statement->execute(['foo' => false]); + } + + #[Test] + public function statementExecuteWillUsePdoIntForIntWhenBinding(): void + { + $this->pdoStatementMock + ->expects($this->any()) + ->method('bindParam') + ->with( + static::equalTo(':foo'), + static::equalTo(123), + static::equalTo(PDO::PARAM_INT), + ); + $this->statement->execute(['foo' => 123]); + } + + #[Test] + public function statementExecuteWillUsePdoStrByDefaultWhenBinding(): void + { + $this->pdoStatementMock + ->expects($this->any()) + ->method('bindParam') + ->with( + static::equalTo(':foo'), + static::equalTo('bar'), + static::equalTo(PDO::PARAM_STR), + ); + $this->statement->execute(['foo' => 'bar']); + } + + #[Test] + public function statementExecuteWillUsePdoStrForStringIntegerWhenBinding(): void + { + $this->pdoStatementMock + ->expects($this->any()) + ->method('bindParam') + ->with( + static::equalTo(':foo'), + static::equalTo('123'), + static::equalTo(PDO::PARAM_STR), + ); + $this->statement->execute(['foo' => '123']); + } + /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. @@ -29,65 +86,21 @@ final class StatementIntegrationTest extends TestCase #[Override] protected function setUp(): void { - $driver = $this->getMockBuilder(PdoDriver::class) - ->onlyMethods(['createResult']) - ->disableOriginalConstructor() + $driver = $this->createStub(PdoDriverInterface::class); + $driver->method('createResult')->willReturn($this->createStub(ResultInterface::class)); + + $this->pdoStatementMock = $this->getMockBuilder(PDOStatement::class) + ->onlyMethods(['execute', 'bindParam']) ->getMock(); $this->statement = new Statement(); $this->statement->setDriver($driver); - $this->statement->initialize(new TestAsset\CtorlessPdo( - $this->pdoStatementMock = $this->getMockBuilder(PDOStatement::class) - ->onlyMethods(['execute', 'bindParam']) - ->getMock() - )); + $this->statement->initialize(new TestAsset\CtorlessPdo($this->pdoStatementMock)); } /** * Tears down the fixture, for example, closes a network connection. * This method is called after a test is executed. */ - protected function tearDown(): void - { - } - - public function testStatementExecuteWillConvertPhpBoolToPdoBoolWhenBinding(): void - { - $this->pdoStatementMock->expects($this->any())->method('bindParam')->with( - $this->equalTo(':foo'), - $this->equalTo(false), - $this->equalTo(PDO::PARAM_BOOL) - ); - $this->statement->execute(['foo' => false]); - } - - public function testStatementExecuteWillUsePdoStrByDefaultWhenBinding(): void - { - $this->pdoStatementMock->expects($this->any())->method('bindParam')->with( - $this->equalTo(':foo'), - $this->equalTo('bar'), - $this->equalTo(PDO::PARAM_STR) - ); - $this->statement->execute(['foo' => 'bar']); - } - - public function testStatementExecuteWillUsePdoStrForStringIntegerWhenBinding(): void - { - $this->pdoStatementMock->expects($this->any())->method('bindParam')->with( - $this->equalTo(':foo'), - $this->equalTo('123'), - $this->equalTo(PDO::PARAM_STR) - ); - $this->statement->execute(['foo' => '123']); - } - - public function testStatementExecuteWillUsePdoIntForIntWhenBinding(): void - { - $this->pdoStatementMock->expects($this->any())->method('bindParam')->with( - $this->equalTo(':foo'), - $this->equalTo(123), - $this->equalTo(PDO::PARAM_INT) - ); - $this->statement->execute(['foo' => 123]); - } + protected function tearDown(): void {} } diff --git a/test/unit/Pdo/StatementTest.php b/test/unit/Pdo/StatementTest.php index 7c6ceb3..f415f9b 100644 --- a/test/unit/Pdo/StatementTest.php +++ b/test/unit/Pdo/StatementTest.php @@ -6,124 +6,124 @@ 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\CoversNothing; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -#[CoversMethod(Statement::class, 'setDriver')] -#[CoversMethod(Statement::class, 'setParameterContainer')] -#[CoversMethod(Statement::class, 'getParameterContainer')] -#[CoversMethod(Statement::class, 'getResource')] -#[CoversMethod(Statement::class, 'setSql')] -#[CoversMethod(Statement::class, 'getSql')] -#[CoversMethod(Statement::class, 'prepare')] -#[CoversMethod(Statement::class, 'isPrepared')] -#[CoversMethod(Statement::class, 'execute')] +#[CoversNothing] final class StatementTest extends TestCase { protected ?Driver $pdo; protected Statement $statement; - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - #[Override] - protected function setUp(): void + #[Test] + public function execute(): void { - $this->statement = new Statement(); - $this->pdo = new Driver( - $this->createMock(Connection::class), - $this->statement, - new Result(), - ); - } - - /** - * Tears down the fixture, for example, closes a network connection. - * This method is called after a test is executed. - */ - protected function tearDown(): void - { - } - - public function testSetDriver(): void - { - self::assertInstanceOf(PdoDriverInterface::class, $this->pdo); - self::assertEquals($this->statement, $this->statement->setDriver($this->pdo)); - } + $mockPdoStatement = $this->createStub(PDOStatement::class); + $pdo = new TestAsset\CtorlessPdo($mockPdoStatement); + $this->statement->initialize($pdo); + $this->statement->prepare('SELECT 1'); - public function testSetParameterContainer(): void - { - self::assertSame($this->statement, $this->statement->setParameterContainer(new ParameterContainer())); + $result = $this->statement->execute(); + static::assertInstanceOf(ResultInterface::class, $result); } /** * @todo Implement testGetParameterContainer(). */ - public function testGetParameterContainer(): void + #[Test] + public function getParameterContainer(): void { $container = new ParameterContainer(); $this->statement->setParameterContainer($container); - self::assertSame($container, $this->statement->getParameterContainer()); + static::assertSame($container, $this->statement->getParameterContainer()); } - public function testGetResource(): void + #[Test] + public function getResource(): void { - $stmt = $this->createMock(PDOStatement::class); + $stmt = $this->createStub(PDOStatement::class); $this->statement->setResource($stmt); - self::assertSame($stmt, $this->statement->getResource()); + static::assertSame($stmt, $this->statement->getResource()); } - public function testSetSql(): void + #[Test] + public function getSql(): void { $this->statement->setSql('SELECT 1'); - self::assertEquals('SELECT 1', $this->statement->getSql()); + static::assertSame('SELECT 1', $this->statement->getSql()); } - public function testGetSql(): void + #[Test] + public function isPrepared(): void { - $this->statement->setSql('SELECT 1'); - self::assertEquals('SELECT 1', $this->statement->getSql()); + static::assertFalse($this->statement->isPrepared()); + + $mockPdoStatement = $this->createStub(PDOStatement::class); + $pdo = new TestAsset\CtorlessPdo($mockPdoStatement); + $this->statement->initialize($pdo); + $this->statement->prepare('SELECT 1'); + + static::assertTrue($this->statement->isPrepared()); } - public function testPrepare(): void + #[Test] + public function prepare(): void { - $mockPdoStatement = $this->createMock(PDOStatement::class); + $mockPdoStatement = $this->createStub(PDOStatement::class); $pdo = new TestAsset\CtorlessPdo($mockPdoStatement); $this->statement->initialize($pdo); $result = $this->statement->prepare('SELECT 1'); - self::assertInstanceOf(Statement::class, $result); + static::assertInstanceOf(Statement::class, $result); } - public function testIsPrepared(): void + #[Test] + public function setDriver(): void { - self::assertFalse($this->statement->isPrepared()); - - $mockPdoStatement = $this->createMock(PDOStatement::class); - $pdo = new TestAsset\CtorlessPdo($mockPdoStatement); - $this->statement->initialize($pdo); - $this->statement->prepare('SELECT 1'); + static::assertInstanceOf(PdoDriverInterface::class, $this->pdo); + static::assertEquals($this->statement, $this->statement->setDriver($this->pdo)); + } - self::assertTrue($this->statement->isPrepared()); + #[Test] + public function setParameterContainer(): void + { + static::assertSame($this->statement, $this->statement->setParameterContainer(new ParameterContainer())); } - public function testExecute(): void + #[Test] + public function setSql(): void { - $mockPdoStatement = $this->createMock(PDOStatement::class); - $pdo = new TestAsset\CtorlessPdo($mockPdoStatement); - $this->statement->initialize($pdo); - $this->statement->prepare('SELECT 1'); + $this->statement->setSql('SELECT 1'); + static::assertSame('SELECT 1', $this->statement->getSql()); + } - $result = $this->statement->execute(); - self::assertInstanceOf(ResultInterface::class, $result); + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + */ + #[Override] + protected function setUp(): void + { + $this->statement = new Statement(); + $this->pdo = new Driver( + $this->createStub(AbstractPdoConnection::class), + $this->statement, + new Result(), + ); } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + protected function tearDown(): void {} } 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/Pdo/TestAsset/CtorlessPdo.php b/test/unit/Pdo/TestAsset/CtorlessPdo.php index 7826375..444e95e 100644 --- a/test/unit/Pdo/TestAsset/CtorlessPdo.php +++ b/test/unit/Pdo/TestAsset/CtorlessPdo.php @@ -7,13 +7,12 @@ use Override; use PDO; use PDOStatement; -use PHPUnit\Framework\MockObject\MockObject; final class CtorlessPdo extends PDO { - public function __construct(protected PDOStatement&MockObject $mockStatement) - { - } + public function __construct( + protected PDOStatement $mockStatement, + ) {} /** * @param array $options diff --git a/test/unit/Pdo/TestAsset/PdoMock.php b/test/unit/Pdo/TestAsset/PdoMock.php index 412ad28..def856c 100644 --- a/test/unit/Pdo/TestAsset/PdoMock.php +++ b/test/unit/Pdo/TestAsset/PdoMock.php @@ -11,9 +11,7 @@ */ final class PdoMock extends PDO { - public function __construct() - { - } + public function __construct() {} public function beginTransaction(): bool { diff --git a/test/unit/Pdo/TestAsset/PdoStubDriver.php b/test/unit/Pdo/TestAsset/PdoStubDriver.php index 4e3531c..35ee98f 100644 --- a/test/unit/Pdo/TestAsset/PdoStubDriver.php +++ b/test/unit/Pdo/TestAsset/PdoStubDriver.php @@ -5,9 +5,17 @@ namespace PhpDbTest\Mysql\Pdo\TestAsset; use PDO; +use SensitiveParameter; final class PdoStubDriver extends PDO { + /** + * @param string $user + * @param string $password + * @phpstan-ignore constructor.unusedParameter, constructor.unusedParameter, constructor.unusedParameter + */ + public function __construct(string $dsn, $user, #[SensitiveParameter] $password) {} + public function beginTransaction(): bool { return true; @@ -18,15 +26,6 @@ public function commit(): bool return true; } - /** - * @param string $user - * @param string $password - * @phpstan-ignore constructor.unusedParameter, constructor.unusedParameter, constructor.unusedParameter - */ - public function __construct(string $dsn, $user, $password) - { - } - public function rollBack(): bool { return true; diff --git a/test/unit/Sql/Ddl/AlterTableDecoratorTest.php b/test/unit/Sql/Ddl/AlterTableDecoratorTest.php index d9f6809..5b2f052 100644 --- a/test/unit/Sql/Ddl/AlterTableDecoratorTest.php +++ b/test/unit/Sql/Ddl/AlterTableDecoratorTest.php @@ -4,43 +4,43 @@ 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; use PhpDb\Sql\Ddl\Column; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +#[CoversMethod(AlterTableDecorator::class, 'setSubject')] #[CoversMethod(AlterTableDecorator::class, 'processAddColumns')] #[CoversMethod(AlterTableDecorator::class, 'processChangeColumns')] #[CoversMethod(AlterTableDecorator::class, 'getSqlInsertOffsets')] +#[CoversMethod(AlterTableDecorator::class, 'compareColumnOptions')] +#[CoversMethod(AlterTableDecorator::class, 'normalizeColumnOption')] final class AlterTableDecoratorTest extends TestCase { protected AdapterPlatform $platform; - protected function setUp(): void + #[Test] + public function addColumnAfter(): void { - $driver = new Driver( - $this->createMock(Connection::class), - $this->createMock(Statement::class), - $this->createMock(Result::class), - ); - $this->platform = new AdapterPlatform($driver); - } + $alter = new AlterTable('test'); + $col = new Column\Varchar('name', 255); + $col->setOption('after', 'id'); + $alter->addColumn($col); - private function buildSql(AlterTable $table): string - { - $decorator = new AlterTableDecorator(); - $decorator->setSubject($table); + $sql = $this->buildSql($alter); - return $decorator->getSqlString($this->platform); + static::assertStringContainsString('AFTER `id`', $sql); } - public function testAddColumnCharset(): void + #[Test] + public function addColumnCharset(): void { $alter = new AlterTable('test'); $col = new Column\Varchar('name', 255); @@ -49,76 +49,139 @@ public function testAddColumnCharset(): void $sql = $this->buildSql($alter); - self::assertStringContainsString('CHARACTER SET utf8mb3', $sql); + static::assertStringContainsString('CHARACTER SET utf8mb3', $sql); } - public function testAddColumnCollate(): void + #[Test] + public function addColumnCharsetAndCollate(): void { $alter = new AlterTable('test'); $col = new Column\Varchar('name', 255); + $col->setOption('charset', 'utf8mb3'); $col->setOption('collate', 'utf8mb3_unicode_ci'); $alter->addColumn($col); $sql = $this->buildSql($alter); - self::assertStringContainsString('COLLATE utf8mb3_unicode_ci', $sql); + static::assertStringContainsString('CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci', $sql); } - public function testAddColumnCharsetAndCollate(): void + #[Test] + public function addColumnCharsetBeforeNotNull(): void { $alter = new AlterTable('test'); $col = new Column\Varchar('name', 255); + $col->setNullable(false); $col->setOption('charset', 'utf8mb3'); $col->setOption('collate', 'utf8mb3_unicode_ci'); $alter->addColumn($col); $sql = $this->buildSql($alter); - self::assertStringContainsString('CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci', $sql); + static::assertMatchesRegularExpression( + '/CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL/', + $sql, + ); } - public function testAddColumnCharsetBeforeNotNull(): void + #[Test] + public function addColumnCollate(): void { $alter = new AlterTable('test'); $col = new Column\Varchar('name', 255); - $col->setNullable(false); - $col->setOption('charset', 'utf8mb3'); $col->setOption('collate', 'utf8mb3_unicode_ci'); $alter->addColumn($col); $sql = $this->buildSql($alter); - self::assertMatchesRegularExpression( - '/CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL/', - $sql, - ); + static::assertStringContainsString('COLLATE utf8mb3_unicode_ci', $sql); } - public function testChangeColumnCharset(): void + #[Test] + public function addColumnComment(): void { $alter = new AlterTable('test'); $col = new Column\Varchar('name', 255); - $col->setOption('charset', 'utf8mb3'); - $alter->changeColumn('name', $col); + $col->setOption('comment', 'A comment'); + $alter->addColumn($col); + + static::assertStringContainsString('COMMENT', $this->buildSql($alter)); + } + + #[Test] + public function addColumnFalsyOptionSkipped(): void + { + $alter = new AlterTable('test'); + $col = new Column\Integer('id'); + $col->setOption('unsigned', false); + $alter->addColumn($col); + + static::assertStringNotContainsString('UNSIGNED', $this->buildSql($alter)); + } + + #[Test] + public function addColumnFormat(): void + { + $alter = new AlterTable('test'); + $col = new Column\Varchar('name', 255); + $col->setOption('columnformat', 'fixed'); + $alter->addColumn($col); + + static::assertStringContainsString('COLUMN_FORMAT FIXED', $this->buildSql($alter)); + } + + #[Test] + public function addColumnStorage(): void + { + $alter = new AlterTable('test'); + $col = new Column\Varchar('name', 255); + $col->setOption('storage', 'disk'); + $alter->addColumn($col); + + static::assertStringContainsString('STORAGE DISK', $this->buildSql($alter)); + } + + #[Test] + public function addColumnUnsigned(): void + { + $alter = new AlterTable('test'); + $col = new Column\Integer('id'); + $col->setOption('unsigned', true); + $col->setOption('auto_increment', true); + $alter->addColumn($col); $sql = $this->buildSql($alter); - self::assertStringContainsString('CHARACTER SET utf8mb3', $sql); + static::assertStringContainsString('UNSIGNED', $sql); + static::assertStringContainsString('AUTO_INCREMENT', $sql); } - public function testChangeColumnCollate(): void + #[Test] + public function addColumnZerofill(): void + { + $alter = new AlterTable('test'); + $col = new Column\Integer('id'); + $col->setOption('zerofill', true); + $alter->addColumn($col); + + static::assertStringContainsString('ZEROFILL', $this->buildSql($alter)); + } + + #[Test] + public function changeColumnCharset(): void { $alter = new AlterTable('test'); $col = new Column\Varchar('name', 255); - $col->setOption('collate', 'utf8mb3_unicode_ci'); + $col->setOption('charset', 'utf8mb3'); $alter->changeColumn('name', $col); $sql = $this->buildSql($alter); - self::assertStringContainsString('COLLATE utf8mb3_unicode_ci', $sql); + static::assertStringContainsString('CHARACTER SET utf8mb3', $sql); } - public function testChangeColumnCharsetAndCollate(): void + #[Test] + public function changeColumnCharsetAndCollate(): void { $alter = new AlterTable('test'); $col = new Column\Varchar('name', 255); @@ -129,35 +192,117 @@ public function testChangeColumnCharsetAndCollate(): void $sql = $this->buildSql($alter); - self::assertMatchesRegularExpression( + static::assertMatchesRegularExpression( '/CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL/', $sql, ); } - public function testAddColumnAfter(): void + #[Test] + public function changeColumnCollate(): void { $alter = new AlterTable('test'); $col = new Column\Varchar('name', 255); - $col->setOption('after', 'id'); - $alter->addColumn($col); + $col->setOption('collate', 'utf8mb3_unicode_ci'); + $alter->changeColumn('name', $col); $sql = $this->buildSql($alter); - self::assertStringContainsString('AFTER `id`', $sql); + static::assertStringContainsString('COLLATE utf8mb3_unicode_ci', $sql); + } + + #[Test] + public function changeColumnComment(): void + { + $alter = new AlterTable('test'); + $col = new Column\Varchar('name', 255); + $col->setOption('comment', 'A comment'); + $alter->changeColumn('name', $col); + + static::assertStringContainsString('COMMENT', $this->buildSql($alter)); + } + + #[Test] + public function changeColumnFalsyOptionSkipped(): void + { + $alter = new AlterTable('test'); + $col = new Column\Integer('id'); + $col->setOption('unsigned', false); + $alter->changeColumn('id', $col); + + static::assertStringNotContainsString('UNSIGNED', $this->buildSql($alter)); + } + + #[Test] + public function changeColumnFormat(): void + { + $alter = new AlterTable('test'); + $col = new Column\Varchar('name', 255); + $col->setOption('columnformat', 'fixed'); + $alter->changeColumn('name', $col); + + static::assertStringContainsString('COLUMN_FORMAT FIXED', $this->buildSql($alter)); + } + + #[Test] + public function changeColumnIdentity(): void + { + $alter = new AlterTable('test'); + $col = new Column\Integer('id'); + $col->setOption('identity', true); + $alter->changeColumn('id', $col); + + static::assertStringContainsString('AUTO_INCREMENT', $this->buildSql($alter)); + } + + #[Test] + public function changeColumnStorage(): void + { + $alter = new AlterTable('test'); + $col = new Column\Varchar('name', 255); + $col->setOption('storage', 'disk'); + $alter->changeColumn('name', $col); + + static::assertStringContainsString('STORAGE DISK', $this->buildSql($alter)); } - public function testAddColumnUnsigned(): void + #[Test] + public function changeColumnUnsigned(): void { $alter = new AlterTable('test'); $col = new Column\Integer('id'); $col->setOption('unsigned', true); - $col->setOption('auto_increment', true); - $alter->addColumn($col); + $alter->changeColumn('id', $col); - $sql = $this->buildSql($alter); + static::assertStringContainsString('UNSIGNED', $this->buildSql($alter)); + } - self::assertStringContainsString('UNSIGNED', $sql); - self::assertStringContainsString('AUTO_INCREMENT', $sql); + #[Test] + public function changeColumnZerofill(): void + { + $alter = new AlterTable('test'); + $col = new Column\Integer('id'); + $col->setOption('zerofill', true); + $alter->changeColumn('id', $col); + + static::assertStringContainsString('ZEROFILL', $this->buildSql($alter)); + } + + protected function setUp(): void + { + $driver = new Driver( + $this->createStub(AbstractPdoConnection::class), + $this->createStub(Statement::class), + $this->createStub(Result::class), + ); + $this->platform = new AdapterPlatform($driver); + } + + private function buildSql(AlterTable $table): string + { + $decorator = new AlterTableDecorator(); + $decorator->setSubject($table); + + return $decorator->getSqlString($this->platform); } } diff --git a/test/unit/Sql/Ddl/CreateTableDecoratorTest.php b/test/unit/Sql/Ddl/CreateTableDecoratorTest.php index 1aa25da..c048910 100644 --- a/test/unit/Sql/Ddl/CreateTableDecoratorTest.php +++ b/test/unit/Sql/Ddl/CreateTableDecoratorTest.php @@ -4,67 +4,75 @@ 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; use PhpDb\Sql\Ddl\Constraint; use PhpDb\Sql\Ddl\CreateTable; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +#[CoversMethod(CreateTableDecorator::class, 'setSubject')] #[CoversMethod(CreateTableDecorator::class, 'processColumns')] #[CoversMethod(CreateTableDecorator::class, 'getSqlInsertOffsets')] +#[CoversMethod(CreateTableDecorator::class, 'compareColumnOptions')] +#[CoversMethod(CreateTableDecorator::class, 'normalizeColumnOption')] final class CreateTableDecoratorTest extends TestCase { protected AdapterPlatform $platform; - protected function setUp(): void + #[Test] + public function charsetAppearsAfterUnsigned(): void { - $driver = new Driver( - $this->createMock(Connection::class), - $this->createMock(Statement::class), - $this->createMock(Result::class), - ); - $this->platform = new AdapterPlatform($driver); - } + $table = new CreateTable('test'); + $col = new Column\Integer('id'); + $col->setOption('unsigned', true); + $col->setOption('charset', 'utf8mb3'); + $table->addColumn($col); - private function buildSql(CreateTable $table): string - { - $decorator = new CreateTableDecorator(); - $decorator->setSubject($table); + $sql = $this->buildSql($table); - return $decorator->getSqlString($this->platform); + static::assertMatchesRegularExpression('/UNSIGNED CHARACTER SET utf8mb3/', $sql); } - public function testColumnCharset(): void + #[Test] + public function charsetAppearsBeforeNotNull(): void { $table = new CreateTable('test'); $col = new Column\Varchar('name', 255); + $col->setNullable(false); $col->setOption('charset', 'utf8mb3'); + $col->setOption('collate', 'utf8mb3_unicode_ci'); $table->addColumn($col); $sql = $this->buildSql($table); - self::assertStringContainsString('CHARACTER SET utf8mb3', $sql); + static::assertMatchesRegularExpression( + '/CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL/', + $sql, + ); } - public function testColumnCollate(): void + #[Test] + public function columnCharset(): void { $table = new CreateTable('test'); $col = new Column\Varchar('name', 255); - $col->setOption('collate', 'utf8mb3_unicode_ci'); + $col->setOption('charset', 'utf8mb3'); $table->addColumn($col); $sql = $this->buildSql($table); - self::assertStringContainsString('COLLATE utf8mb3_unicode_ci', $sql); + static::assertStringContainsString('CHARACTER SET utf8mb3', $sql); } - public function testColumnCharsetAndCollate(): void + #[Test] + public function columnCharsetAndCollate(): void { $table = new CreateTable('test'); $col = new Column\Varchar('name', 255); @@ -74,66 +82,59 @@ public function testColumnCharsetAndCollate(): void $sql = $this->buildSql($table); - self::assertStringContainsString('CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci', $sql); + static::assertStringContainsString('CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci', $sql); } - public function testCharsetAppearsBeforeNotNull(): void + #[Test] + public function columnCollate(): void { $table = new CreateTable('test'); $col = new Column\Varchar('name', 255); - $col->setNullable(false); - $col->setOption('charset', 'utf8mb3'); $col->setOption('collate', 'utf8mb3_unicode_ci'); $table->addColumn($col); $sql = $this->buildSql($table); - self::assertMatchesRegularExpression( - '/CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL/', - $sql, - ); + static::assertStringContainsString('COLLATE utf8mb3_unicode_ci', $sql); } - public function testCharsetAppearsAfterUnsigned(): void + #[Test] + public function columnFormatOption(): void { $table = new CreateTable('test'); - $col = new Column\Integer('id'); - $col->setOption('unsigned', true); - $col->setOption('charset', 'utf8mb3'); + $col = new Column\Varchar('name', 255); + $col->setOption('columnformat', 'fixed'); $table->addColumn($col); - $sql = $this->buildSql($table); - - self::assertMatchesRegularExpression('/UNSIGNED CHARACTER SET utf8mb3/', $sql); + static::assertStringContainsString('COLUMN_FORMAT FIXED', $this->buildSql($table)); } - public function testUnsignedOption(): void + #[Test] + public function commentOption(): void { $table = new CreateTable('test'); - $col = new Column\Integer('id'); - $col->setOption('unsigned', true); - $col->setOption('auto_increment', true); + $col = new Column\Varchar('name', 255); + $col->setOption('comment', 'A test column'); $table->addColumn($col); $sql = $this->buildSql($table); - self::assertStringContainsString('UNSIGNED', $sql); - self::assertStringContainsString('AUTO_INCREMENT', $sql); + static::assertStringContainsString('COMMENT', $sql); } - public function testCommentOption(): void + #[Test] + public function falsyOptionIsSkipped(): void { $table = new CreateTable('test'); - $col = new Column\Varchar('name', 255); - $col->setOption('comment', 'A test column'); + $col = new Column\Integer('id'); + $col->setOption('unsigned', false); $table->addColumn($col); - $sql = $this->buildSql($table); - - self::assertStringContainsString('COMMENT', $sql); + static::assertStringNotContainsString('UNSIGNED', $this->buildSql($table)); } - public function testFullColumnDefinition(): void + #[Test] + public function fullColumnDefinition(): void { $table = new CreateTable('test'); @@ -152,8 +153,63 @@ public function testFullColumnDefinition(): void $sql = $this->buildSql($table); - self::assertStringContainsString('UNSIGNED', $sql); - self::assertStringContainsString('AUTO_INCREMENT', $sql); - self::assertStringContainsString('CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL', $sql); + static::assertStringContainsString('UNSIGNED', $sql); + static::assertStringContainsString('AUTO_INCREMENT', $sql); + static::assertStringContainsString('CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL', $sql); + } + + #[Test] + public function storageOption(): void + { + $table = new CreateTable('test'); + $col = new Column\Varchar('name', 255); + $col->setOption('storage', 'disk'); + $table->addColumn($col); + + static::assertStringContainsString('STORAGE DISK', $this->buildSql($table)); + } + + #[Test] + public function unsignedOption(): void + { + $table = new CreateTable('test'); + $col = new Column\Integer('id'); + $col->setOption('unsigned', true); + $col->setOption('auto_increment', true); + $table->addColumn($col); + + $sql = $this->buildSql($table); + + static::assertStringContainsString('UNSIGNED', $sql); + static::assertStringContainsString('AUTO_INCREMENT', $sql); + } + + #[Test] + public function zerofillOption(): void + { + $table = new CreateTable('test'); + $col = new Column\Integer('id'); + $col->setOption('zerofill', true); + $table->addColumn($col); + + static::assertStringContainsString('ZEROFILL', $this->buildSql($table)); + } + + protected function setUp(): void + { + $driver = new Driver( + $this->createStub(AbstractPdoConnection::class), + $this->createStub(Statement::class), + $this->createStub(Result::class), + ); + $this->platform = new AdapterPlatform($driver); + } + + private function buildSql(CreateTable $table): string + { + $decorator = new CreateTableDecorator(); + $decorator->setSubject($table); + + return $decorator->getSqlString($this->platform); } } diff --git a/test/unit/Sql/PlatformTest.php b/test/unit/Sql/PlatformTest.php new file mode 100644 index 0000000..8f457d0 --- /dev/null +++ b/test/unit/Sql/PlatformTest.php @@ -0,0 +1,44 @@ +getTypeDecorator(new AlterTable('test'))); + } + + #[Test] + public function registersCreateTableDecorator(): void + { + $platform = new Platform(); + + static::assertInstanceOf(CreateTableDecorator::class, $platform->getTypeDecorator(new CreateTable('test'))); + } + + #[Test] + public function registersSelectDecorator(): void + { + $platform = new Platform(); + + static::assertInstanceOf(SelectDecorator::class, $platform->getTypeDecorator(new Select('test'))); + } +} diff --git a/test/unit/Sql/SelectDecoratorTest.php b/test/unit/Sql/SelectDecoratorTest.php new file mode 100644 index 0000000..65cd091 --- /dev/null +++ b/test/unit/Sql/SelectDecoratorTest.php @@ -0,0 +1,130 @@ +limit(10) + ->offset(5); + $sql = $this->decorate($select)->getSqlString($this->platform); + + static::assertStringContainsString('LIMIT 10', $sql); + static::assertStringContainsString('OFFSET 5', $sql); + } + + #[Test] + public function limitOnly(): void + { + $select = (new Select('test'))->limit(10); + $sql = $this->decorate($select)->getSqlString($this->platform); + + static::assertStringContainsString('LIMIT 10', $sql); + static::assertStringNotContainsString('OFFSET', $sql); + } + + #[Test] + public function noLimitOrOffset(): void + { + $select = new Select('test'); + $sql = $this->decorate($select)->getSqlString($this->platform); + + static::assertStringNotContainsString('LIMIT', $sql); + static::assertStringNotContainsString('OFFSET', $sql); + } + + #[Test] + public function offsetWithoutLimit(): void + { + $select = (new Select('test'))->offset(5); + $sql = $this->decorate($select)->getSqlString($this->platform); + + static::assertStringContainsString('LIMIT 18446744073709551615', $sql); + static::assertStringContainsString('OFFSET 5', $sql); + } + + #[Test] + public function prepareStatementBindsLimitAndOffsetParameters(): void + { + $decorator = $this->decorate( + (new Select('test'))->limit(10) + ->offset(5), + ); + + $parameterContainer = new ParameterContainer(); + + $statementContainer = $this->createMock(StatementContainerInterface::class); + $statementContainer->method('getParameterContainer')->willReturn($parameterContainer); + $statementContainer->expects($this->once()) + ->method('setSql') + ->with($this->isString()); + + $driver = $this->createStub(DriverInterface::class); + $driver->method('formatParameterName') + ->willReturnCallback(static fn(string $name): string => ":{$name}"); + + $adapter = $this->createStub(AdapterInterface::class); + $adapter->method('getPlatform')->willReturn($this->platform); + $adapter->method('getDriver')->willReturn($driver); + + $result = $decorator->prepareStatement($adapter, $statementContainer); + + static::assertSame($statementContainer, $result); + static::assertSame(10, $parameterContainer->offsetGet('limit')); + static::assertSame(5, $parameterContainer->offsetGet('offset')); + } + + #[Test] + public function setSubjectReturnsSelf(): void + { + $decorator = new SelectDecorator(); + + static::assertSame($decorator, $decorator->setSubject(new Select('test'))); + static::assertSame($decorator, $decorator->setSubject(null)); + } + + #[Override] + protected function setUp(): void + { + $driver = new Driver( + $this->createStub(AbstractPdoConnection::class), + $this->createStub(Statement::class), + $this->createStub(Result::class), + ); + $this->platform = new AdapterPlatform($driver); + } + + private function decorate(Select $select): SelectDecorator + { + $decorator = new SelectDecorator(); + $decorator->setSubject($select); + + return $decorator; + } +} diff --git a/test/unit/StatementTest.php b/test/unit/StatementTest.php new file mode 100644 index 0000000..3eb5956 --- /dev/null +++ b/test/unit/StatementTest.php @@ -0,0 +1,41 @@ +getProfiler()); + + $profiler = $this->createStub(ProfilerInterface::class); + + static::assertSame($statement, $statement->setProfiler($profiler)); + static::assertSame($profiler, $statement->getProfiler()); + } + + #[Test] + public function setDriverRejectsNonMysqlDriver(): void + { + $statement = new Statement(); + + $this->expectException(InvalidArgumentException::class); + $statement->setDriver($this->createStub(DriverInterface::class)); + } +}