diff --git a/composer.json b/composer.json index 92da69c..9d9557a 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ }, "require": { "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", - "php-db/phpdb": "^0.3.2" + "php-db/phpdb": "^0.4.0" }, "require-dev": { "ext-mysqli": "*", @@ -62,7 +62,7 @@ "scripts": { "check": [ "@cs-check", - "@static-analysis", + "@sa", "@test", "@test-integration" ], @@ -71,8 +71,8 @@ "test": "phpunit --colors=always --testsuite \"unit test\"", "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", "test-integration": "phpunit --colors=always --testsuite \"integration test\"", - "static-analysis": "vendor/bin/phpstan analyse --memory-limit=256M", - "sa-generate-baseline": "vendor/bin/phpstan analyse --memory-limit=256M --generate-baseline", + "sa": "vendor/bin/phpstan analyse --memory-limit=256M", + "sa-gen-baseline": "vendor/bin/phpstan analyse --memory-limit=256M --generate-baseline", "upload-coverage": "coveralls -v" } } diff --git a/composer.lock b/composer.lock index f93b74b..f819017 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": "3d68e25b79f19fd20226d1d4b0571a6b", + "content-hash": "b2525dc152013de8e9aa5e870852bcaf", "packages": [ { "name": "brick/varexporter", @@ -201,16 +201,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -253,22 +253,22 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "php-db/phpdb", - "version": "0.3.2", + "version": "0.4.0", "source": { "type": "git", "url": "https://github.com/php-db/phpdb.git", - "reference": "b3df2ff1f3d776f42f8d926b23535e4143a93996" + "reference": "3cb7531b36bf37c42843bbe9dd05583d8d8f130e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-db/phpdb/zipball/b3df2ff1f3d776f42f8d926b23535e4143a93996", - "reference": "b3df2ff1f3d776f42f8d926b23535e4143a93996", + "url": "https://api.github.com/repos/php-db/phpdb/zipball/3cb7531b36bf37c42843bbe9dd05583d8d8f130e", + "reference": "3cb7531b36bf37c42843bbe9dd05583d8d8f130e", "shasum": "" }, "require": { @@ -284,6 +284,7 @@ "laminas/laminas-coding-standard": "^3.0.1", "laminas/laminas-eventmanager": "^3.14.0", "laminas/laminas-hydrator": "^4.6.0", + "phpbench/phpbench": "^1.4", "phpstan/phpstan": "^2.1", "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^11.5.15", @@ -322,7 +323,7 @@ "issues": "https://github.com/php-db/phpdb/issues", "source": "https://github.com/php-db/phpdb" }, - "time": "2025-12-01T04:51:41+00:00" + "time": "2025-12-05T01:52:04+00:00" }, { "name": "psr/container", @@ -755,11 +756,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.32", + "version": "2.1.33", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e126cad1e30a99b137b8ed75a85a676450ebb227", - "reference": "e126cad1e30a99b137b8ed75a85a676450ebb227", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f", + "reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f", "shasum": "" }, "require": { @@ -804,20 +805,20 @@ "type": "github" } ], - "time": "2025-11-11T15:18:17+00:00" + "time": "2025-12-05T10:24:31+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "2.0.8", + "version": "2.0.10", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "2fe9fbeceaf76dd1ebaa7bbbb25e2fb5e59db2fe" + "reference": "8d61a5854e7497d95bc85188e13537e99bd7aae7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/2fe9fbeceaf76dd1ebaa7bbbb25e2fb5e59db2fe", - "reference": "2fe9fbeceaf76dd1ebaa7bbbb25e2fb5e59db2fe", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/8d61a5854e7497d95bc85188e13537e99bd7aae7", + "reference": "8d61a5854e7497d95bc85188e13537e99bd7aae7", "shasum": "" }, "require": { @@ -855,9 +856,9 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.8" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.10" }, - "time": "2025-11-11T07:55:22+00:00" + "time": "2025-12-06T11:15:39+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1196,16 +1197,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.44", + "version": "11.5.46", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c346885c95423eda3f65d85a194aaa24873cda82" + "reference": "75dfe79a2aa30085b7132bb84377c24062193f33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c346885c95423eda3f65d85a194aaa24873cda82", - "reference": "c346885c95423eda3f65d85a194aaa24873cda82", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/75dfe79a2aa30085b7132bb84377c24062193f33", + "reference": "75dfe79a2aa30085b7132bb84377c24062193f33", "shasum": "" }, "require": { @@ -1277,7 +1278,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.44" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.46" }, "funding": [ { @@ -1301,7 +1302,7 @@ "type": "tidelift" } ], - "time": "2025-11-13T07:17:35+00:00" + "time": "2025-12-06T08:01:15+00:00" }, { "name": "sebastian/cli-parser", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ea4169d..35df4a1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -162,12 +162,6 @@ parameters: count: 2 path: src/Driver/Pdo/Connection.php - - - message: '#^Parameter \#2 \$parameters of class PhpDb\\Adapter\\Exception\\InvalidConnectionParametersException constructor expects int, array given\.$#' - identifier: argument.type - count: 2 - path: src/Driver/Pdo/Connection.php - - message: '#^Parameter \#1 \$resource \(PDOStatement\) of method PhpDb\\Adapter\\Mysql\\Driver\\Pdo\\Pdo\:\:createResult\(\) should be compatible with parameter \$resource \(resource\) of method PhpDb\\Adapter\\Driver\\DriverInterface\:\:createResult\(\)$#' identifier: method.childParameterType @@ -198,30 +192,6 @@ parameters: count: 2 path: src/Metadata/Source/MysqlMetadata.php - - - message: '#^Method PhpDb\\Adapter\\Mysql\\Sql\\Platform\\Mysql\\Ddl\\CreateTableDecorator\:\:processColumns\(\) never returns null so it can be removed from the return type\.$#' - identifier: return.unusedType - count: 1 - path: src/Sql/Platform/Mysql/Ddl/CreateTableDecorator.php - - - - message: '#^Method PhpDb\\Adapter\\Mysql\\Sql\\Platform\\Mysql\\Ddl\\CreateTableDecorator\:\:processColumns\(\) should return array\\>\|null but empty return statement found\.$#' - identifier: return.empty - count: 1 - path: src/Sql/Platform/Mysql/Ddl/CreateTableDecorator.php - - - - message: '#^Method PhpDb\\Adapter\\Mysql\\Sql\\Platform\\Mysql\\SelectDecorator\:\:processLimit\(\) should return array\\|null but returns array\\.$#' - identifier: return.type - count: 1 - path: src/Sql/Platform/Mysql/SelectDecorator.php - - - - message: '#^Offset ''paramPrefix'' does not exist on string\.$#' - identifier: offsetAccess.notFound - count: 2 - path: src/Sql/Platform/Mysql/SelectDecorator.php - - message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertIsInt\(\) with int will always evaluate to true\.$#' identifier: staticMethod.alreadyNarrowedType diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 9356f41..ba14d84 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -4,23 +4,20 @@ namespace PhpDb\Adapter\Mysql; -use Laminas\ServiceManager\Factory\InvokableFactory; use PhpDb\Adapter\AdapterInterface; use PhpDb\Adapter\Driver\DriverInterface; -use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\Pdo\Statement as PdoStatement; use PhpDb\Adapter\Driver\PdoDriverInterface; use PhpDb\Adapter\Mysql\Driver; use PhpDb\Adapter\Mysql\Metadata\Source\MysqlMetadata; use PhpDb\Adapter\Platform\PlatformInterface; -use PhpDb\Adapter\Profiler; use PhpDb\Container\AdapterAbstractServiceFactory; use PhpDb\Container\ConnectionInterfaceFactoryFactoryInterface; use PhpDb\Container\DriverInterfaceFactoryFactoryInterface; use PhpDb\Container\PlatformInterfaceFactoryFactoryInterface; use PhpDb\Metadata\MetadataInterface; -use PhpDb\ResultSet; +/** @internal */ final class ConfigProvider { public function __invoke(): array @@ -50,35 +47,41 @@ public function getDependencies(): array 'pdo' => Driver\Pdo\Pdo::class, DriverInterface::class => Driver\Mysqli\Mysqli::class, PdoDriverInterface::class => Driver\Pdo\Pdo::class, - Profiler\ProfilerInterface::class => Profiler\Profiler::class, - ResultSet\ResultSetInterface::class => ResultSet\ResultSet::class, ConnectionInterfaceFactoryFactoryInterface::class => Container\ConnectionInterfaceFactoryFactory::class, DriverInterfaceFactoryFactoryInterface::class => Container\DriverInterfaceFactoryFactory::class, MetadataInterface::class => MysqlMetadata::class, PlatformInterfaceFactoryFactoryInterface::class => Container\PlatformInterfaceFactoryFactory::class, + // Uncomment to override the default implementations + //Profiler\ProfilerInterface::class => Profiler\Profiler::class, + //ResultSet\ResultSetInterface::class => ResultSet\ResultSet::class, ], 'factories' => [ AdapterInterface::class => Container\AdapterFactory::class, Driver\Mysqli\Mysqli::class => Container\MysqliDriverFactory::class, Driver\Mysqli\Connection::class => Container\MysqliConnectionFactory::class, - Driver\Mysqli\Result::class => Container\MysqliResultFactory::class, - Driver\Mysqli\Statement::class => Container\MysqliStatementFactory::class, + Driver\Mysqli\Statement::class => Container\StatementInterfaceFactory::class, Driver\Pdo\Pdo::class => Container\PdoDriverFactory::class, Driver\Pdo\Connection::class => Container\PdoConnectionFactory::class, + PdoStatement::class => Container\StatementInterfaceFactory::class, MysqlMetadata::class => Container\MetadataInterfaceFactory::class, - PdoStatement::class => Container\PdoStatementFactory::class, PlatformInterface::class => Container\PlatformInterfaceFactory::class, - Profiler\Profiler::class => InvokableFactory::class, - Result::class => Container\PdoResultFactory::class, - ResultSet\ResultSet::class => InvokableFactory::class, ], 'invokables' => [ Container\ConnectionInterfaceFactoryFactory::class - => Container\ConnectionInterfaceFactoryFactory::class, + => Container\ConnectionInterfaceFactoryFactory::class, Container\DriverInterfaceFactoryFactory::class - => Container\DriverInterfaceFactoryFactory::class, + => Container\DriverInterfaceFactoryFactory::class, Container\PlatformInterfaceFactoryFactory::class - => Container\PlatformInterfaceFactoryFactory::class, + => Container\PlatformInterfaceFactoryFactory::class, + // Uncomment to override the default implementations + // Driver\Mysqli\Result::class + // => Driver\Mysqli\Result::class, + // Profiler\Profiler::class + // => Profiler\Profiler::class, + // Result::class + // => Result::class, + // ResultSet\ResultSet::class + // => ResultSet\ResultSet::class, ], ]; } diff --git a/src/Container/AdapterFactory.php b/src/Container/AdapterFactory.php index 7670dfc..4368343 100644 --- a/src/Container/AdapterFactory.php +++ b/src/Container/AdapterFactory.php @@ -17,6 +17,7 @@ use function sprintf; +/** @internal */ final class AdapterFactory { public function __invoke(ContainerInterface $container): AdapterInterface @@ -36,7 +37,7 @@ public function __invoke(ContainerInterface $container): AdapterInterface if (! $container->has($driver)) { throw new ServiceNotFoundException(sprintf( - 'Database driver "%s" is not registered in the adapter manager.', + 'Database driver "%s" is not registered in the container.', $driver )); } @@ -46,7 +47,7 @@ public function __invoke(ContainerInterface $container): AdapterInterface if (! $container->has(PlatformInterface::class)) { throw new ServiceNotFoundException(sprintf( - 'Database platform "%s" is not registered in the adapter manager.', + 'Database platform "%s" is not registered in the container.', PlatformInterface::class )); } @@ -54,26 +55,25 @@ public function __invoke(ContainerInterface $container): AdapterInterface /** @var PlatformInterface $platformInstance */ $platformInstance = $container->get(PlatformInterface::class); - if (! $container->has(ResultSetInterface::class)) { - throw new ServiceNotFoundException(sprintf( - 'ResultSet "%s" is not registered in the adapter manager.', - ResultSetInterface::class - )); - } - - /** @var ResultSetInterface $resultSetInstance */ - $resultSetInstance = $container->get(ResultSetInterface::class); - /** @var ProfilerInterface|null $profilerInstanceOrNull */ $profilerInstanceOrNull = $container->has(ProfilerInterface::class) ? $container->get(ProfilerInterface::class) : null; + // If its not provided, use the default ResultSet + if (! $container->has(ResultSetInterface::class)) { + return new Adapter( + driver: $driverInstance, + platform: $platformInstance, + profiler: $profilerInstanceOrNull + ); + } + return new Adapter( - $driverInstance, - $platformInstance, - $resultSetInstance, - $profilerInstanceOrNull + driver: $driverInstance, + platform: $platformInstance, + queryResultSetPrototype: $container->get(ResultSetInterface::class), + profiler: $profilerInstanceOrNull ); } } diff --git a/src/Container/ConnectionInterfaceFactoryFactory.php b/src/Container/ConnectionInterfaceFactoryFactory.php index 001daa7..e1ea552 100644 --- a/src/Container/ConnectionInterfaceFactoryFactory.php +++ b/src/Container/ConnectionInterfaceFactoryFactory.php @@ -15,6 +15,7 @@ use function array_key_exists; use function sprintf; +/** @internal */ final class ConnectionInterfaceFactoryFactory implements FactoryFactoryInterface { public function __invoke( diff --git a/src/Container/DriverInterfaceFactoryFactory.php b/src/Container/DriverInterfaceFactoryFactory.php index e4ce956..92dedca 100644 --- a/src/Container/DriverInterfaceFactoryFactory.php +++ b/src/Container/DriverInterfaceFactoryFactory.php @@ -10,6 +10,7 @@ use function sprintf; +/** @internal */ final class DriverInterfaceFactoryFactory implements FactoryFactoryInterface { public function __invoke( diff --git a/src/Container/MetadataInterfaceFactory.php b/src/Container/MetadataInterfaceFactory.php index 6080713..f6a1a59 100644 --- a/src/Container/MetadataInterfaceFactory.php +++ b/src/Container/MetadataInterfaceFactory.php @@ -9,6 +9,7 @@ use PhpDb\Metadata\MetadataInterface; use Psr\Container\ContainerInterface; +/** @internal */ final class MetadataInterfaceFactory { public function __invoke(ContainerInterface $container): MetadataInterface diff --git a/src/Container/MysqlMetadataFactory.php b/src/Container/MysqlMetadataFactory.php index 9d826d8..b82bda2 100644 --- a/src/Container/MysqlMetadataFactory.php +++ b/src/Container/MysqlMetadataFactory.php @@ -10,6 +10,7 @@ use PhpDb\Metadata\MetadataInterface; use Psr\Container\ContainerInterface; +/** @internal */ final class MysqlMetadataFactory { public function __invoke(ContainerInterface $container): MetadataInterface&MysqlMetadata diff --git a/src/Container/MysqliConnectionFactory.php b/src/Container/MysqliConnectionFactory.php index a47bb5d..384ac26 100644 --- a/src/Container/MysqliConnectionFactory.php +++ b/src/Container/MysqliConnectionFactory.php @@ -8,6 +8,7 @@ use PhpDb\Adapter\Mysql\Driver\Mysqli\Connection; use Psr\Container\ContainerInterface; +/** @internal */ final class MysqliConnectionFactory { public function __invoke( diff --git a/src/Container/MysqliDriverFactory.php b/src/Container/MysqliDriverFactory.php index 4351254..3a141c8 100644 --- a/src/Container/MysqliDriverFactory.php +++ b/src/Container/MysqliDriverFactory.php @@ -7,7 +7,9 @@ use PhpDb\Adapter\Driver; use PhpDb\Adapter\Mysql\Driver\Mysqli; use Psr\Container\ContainerInterface; +use RuntimeException; +/** @internal */ final class MysqliDriverFactory { public function __invoke(ContainerInterface $container): Driver\DriverInterface&Mysqli\Mysqli @@ -24,18 +26,32 @@ public function __invoke(ContainerInterface $container): Driver\DriverInterface& /** @var Driver\ConnectionInterface&Mysqli\Connection $connectionInstance */ $connectionInstance = $container->get(Mysqli\Connection::class); - /** @var Driver\StatementInterface&Mysqli\Statement $statementInstance */ - $statementInstance = $container->get(Mysqli\Statement::class); + $hasStatement = $container->has(Mysqli\Statement::class); + $hasResult = $container->has(Mysqli\Result::class); - /** @var Driver\ResultInterface&Mysqli\Result $resultInstance */ - $resultInstance = $container->get(Mysqli\Result::class); - - return new Mysqli\Mysqli( - $connectionInstance, - $statementInstance, - $resultInstance, - $options - ); + return match (true) { + ! $hasStatement && ! $hasResult => new Mysqli\Mysqli( + connection: $connectionInstance, + options: $options, + ), + $hasStatement && ! $hasResult => new Mysqli\Mysqli( + connection: $connectionInstance, + statementPrototype: $container->get(Mysqli\Statement::class), + options: $options, + ), + ! $hasStatement && $hasResult => new Mysqli\Mysqli( + connection: $connectionInstance, + resultPrototype: $container->get(Mysqli\Result::class), + options: $options, + ), + $hasStatement && $hasResult => new Mysqli\Mysqli( + connection: $connectionInstance, + statementPrototype: $container->get(Mysqli\Statement::class), + resultPrototype: $container->get(Mysqli\Result::class), + options: $options, + ), + default => throw new RuntimeException('Unable to create PdoDriver from configuration.'), + }; } public static function createFromConfig( @@ -52,11 +68,34 @@ public static function createFromConfig( /** @var array $adapterConfig */ $adapterConfig = $dbConfig['adapters'][$requestedName] ?? []; - return new Mysqli\Mysqli( - $connectionFactory::createFromConfig($container, $requestedName), - $container->get(Mysqli\Statement::class), - $container->get(Mysqli\Result::class), - $adapterConfig['options'] ?? [] - ); + /** @var Driver\ConnectionInterface&Mysqli\Connection $connectionInstance */ + $connectionInstance = $connectionFactory::createFromConfig($container, $requestedName); + + $hasStatement = $container->has(Mysqli\Statement::class); + $hasResult = $container->has(Mysqli\Result::class); + + return match (true) { + ! $hasStatement && ! $hasResult => new Mysqli\Mysqli( + connection: $connectionInstance, + options: $adapterConfig['options'] ?? [], + ), + $hasStatement && ! $hasResult => new Mysqli\Mysqli( + connection: $connectionInstance, + statementPrototype: $container->get(Mysqli\Statement::class), + options: $adapterConfig['options'] ?? [], + ), + ! $hasStatement && $hasResult => new Mysqli\Mysqli( + connection: $connectionInstance, + resultPrototype: $container->get(Mysqli\Result::class), + options: $adapterConfig['options'] ?? [], + ), + $hasStatement && $hasResult => new Mysqli\Mysqli( + connection: $connectionInstance, + statementPrototype: $container->get(Mysqli\Statement::class), + resultPrototype: $container->get(Mysqli\Result::class), + options: $adapterConfig['options'] ?? [], + ), + default => throw new RuntimeException('Unable to create PdoDriver from configuration.'), + }; } } diff --git a/src/Container/MysqliResultFactory.php b/src/Container/MysqliResultFactory.php deleted file mode 100644 index bb1f9cb..0000000 --- a/src/Container/MysqliResultFactory.php +++ /dev/null @@ -1,17 +0,0 @@ -get('config'); - - /** @var array $dbConfig */ - $dbConfig = $config['db'] ?? []; - - /** @var array $options */ - $options = $dbConfig['options'] ?? []; - - /** @var bool $bufferResults */ - $bufferResults = $options['buffer_results'] ?? false; - - return new Statement(bufferResults: $bufferResults); - } -} diff --git a/src/Container/PdoConnectionFactory.php b/src/Container/PdoConnectionFactory.php index 1370281..17cc432 100644 --- a/src/Container/PdoConnectionFactory.php +++ b/src/Container/PdoConnectionFactory.php @@ -8,6 +8,7 @@ use PhpDb\Adapter\Mysql\Driver\Pdo\Connection; use Psr\Container\ContainerInterface; +/** @internal */ final class PdoConnectionFactory { public function __invoke(ContainerInterface $container): ConnectionInterface&Connection diff --git a/src/Container/PdoDriverFactory.php b/src/Container/PdoDriverFactory.php index e245ca2..daa130f 100644 --- a/src/Container/PdoDriverFactory.php +++ b/src/Container/PdoDriverFactory.php @@ -8,11 +8,10 @@ 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\Driver\StatementInterface; use PhpDb\Adapter\Mysql\Driver\Pdo\Connection; use PhpDb\Adapter\Mysql\Driver\Pdo\Pdo as PdoDriver; use Psr\Container\ContainerInterface; +use RuntimeException; final class PdoDriverFactory { @@ -21,16 +20,28 @@ public function __invoke(ContainerInterface $container): PdoDriverInterface&PdoD /** @var ConnectionInterface&Connection $connectionInstance */ $connectionInstance = $container->get(Connection::class); - /** @var StatementInterface&Statement $statementInstance */ - $statementInstance = $container->get(Statement::class); + $hasStatement = $container->has(Statement::class); + $hasResult = $container->has(Result::class); - /** @var ResultInterface&Result $resultInstance */ - $resultInstance = $container->get(Result::class); - return new PdoDriver( - $connectionInstance, - $statementInstance, - $resultInstance - ); + return match (true) { + ! $hasStatement && ! $hasResult => new PdoDriver( + connection: $connectionInstance, + ), + $hasStatement && ! $hasResult => new PdoDriver( + connection: $connectionInstance, + statementPrototype: $container->get(Statement::class), + ), + ! $hasStatement && $hasResult => new PdoDriver( + connection: $connectionInstance, + resultPrototype: $container->get(Result::class), + ), + $hasStatement && $hasResult => new PdoDriver( + connection: $connectionInstance, + statementPrototype: $container->get(Statement::class), + resultPrototype: $container->get(Result::class), + ), + default => throw new RuntimeException('Unable to create PdoDriver'), + }; } public static function createFromConfig( @@ -44,25 +55,31 @@ public static function createFromConfig( $config = $container->get('config'); /** @var array $dbConfig */ $dbConfig = $config['db'] ?? []; - /** @var array $adapterConfig */ - $adapterConfig = $dbConfig['adapters'][$requestedName] ?? []; - /** @var array $options */ - $options = $adapterConfig['options'] ?? []; /** @var ConnectionInterface&Connection $connectionInstance */ $connectionInstance = $connectionFactory::createFromConfig($container, $requestedName); - /** @var StatementInterface&Statement $statementInstance */ - $statementInstance = $container->get(Statement::class); + $hasStatement = $container->has(Statement::class); + $hasResult = $container->has(Result::class); - /** @var ResultInterface&Result $resultInstance */ - $resultInstance = $container->get(Result::class); - - return new PdoDriver( - $connectionInstance, - $statementInstance, - $resultInstance, - $options - ); + return match (true) { + ! $hasStatement && ! $hasResult => new PdoDriver( + connection: $connectionInstance, + ), + $hasStatement && ! $hasResult => new PdoDriver( + connection: $connectionInstance, + statementPrototype: $container->get(Statement::class), + ), + ! $hasStatement && $hasResult => new PdoDriver( + connection: $connectionInstance, + resultPrototype: $container->get(Result::class), + ), + $hasStatement && $hasResult => new PdoDriver( + connection: $connectionInstance, + statementPrototype: $container->get(Statement::class), + resultPrototype: $container->get(Result::class), + ), + default => throw new RuntimeException('Unable to create PdoDriver from configuration.'), + }; } } diff --git a/src/Container/PdoResultFactory.php b/src/Container/PdoResultFactory.php deleted file mode 100644 index 86f1ea3..0000000 --- a/src/Container/PdoResultFactory.php +++ /dev/null @@ -1,17 +0,0 @@ -get('config'); - - /** @var array $dbConfig */ - $dbConfig = $config['db'] ?? []; - - /** @var array $options */ - $options = $dbConfig['options'] ?? []; - - return new Statement(options: $options); - } -} diff --git a/src/Container/PlatformInterfaceFactory.php b/src/Container/PlatformInterfaceFactory.php index 9a7b170..db7b377 100644 --- a/src/Container/PlatformInterfaceFactory.php +++ b/src/Container/PlatformInterfaceFactory.php @@ -11,6 +11,7 @@ use PhpDb\Adapter\Platform\PlatformInterface; use Psr\Container\ContainerInterface; +/** @internal */ final class PlatformInterfaceFactory { public function __invoke(ContainerInterface $container): PlatformInterface&Mysql diff --git a/src/Container/PlatformInterfaceFactoryFactory.php b/src/Container/PlatformInterfaceFactoryFactory.php index 01b4a36..2ac99e7 100644 --- a/src/Container/PlatformInterfaceFactoryFactory.php +++ b/src/Container/PlatformInterfaceFactoryFactory.php @@ -7,6 +7,7 @@ use PhpDb\Adapter\Mysql\Container\PlatformInterfaceFactory; use PhpDb\Container\PlatformInterfaceFactoryFactoryInterface as FactoryFactoryInterface; +/** @internal */ final class PlatformInterfaceFactoryFactory implements FactoryFactoryInterface { public function __invoke(): callable diff --git a/src/Container/StatementInterfaceFactory.php b/src/Container/StatementInterfaceFactory.php new file mode 100644 index 0000000..77a6484 --- /dev/null +++ b/src/Container/StatementInterfaceFactory.php @@ -0,0 +1,34 @@ +get('config'); + + /** @var array $dbConfig */ + $dbConfig = $config['db'] ?? []; + + /** @var array $options */ + $options = $dbConfig['options'] ?? []; + + return new $requestedName(options: $options); + } +} diff --git a/src/Driver/Mysqli/Mysqli.php b/src/Driver/Mysqli/Mysqli.php index e9ee6d9..56db6d4 100644 --- a/src/Driver/Mysqli/Mysqli.php +++ b/src/Driver/Mysqli/Mysqli.php @@ -32,9 +32,9 @@ final class Mysqli implements DriverInterface, ProfilerAwareInterface ]; public function __construct( - protected readonly ConnectionInterface&Connection $connection, - protected readonly StatementInterface&Statement $statementPrototype, - protected readonly ResultInterface $resultPrototype, + protected ConnectionInterface&Connection $connection, + protected StatementInterface&Statement $statementPrototype = new Statement(), + protected ResultInterface $resultPrototype = new Result(), array $options = [] ) { $this->checkEnvironment(); diff --git a/src/Driver/Mysqli/Result.php b/src/Driver/Mysqli/Result.php index ce8732d..fef5d40 100644 --- a/src/Driver/Mysqli/Result.php +++ b/src/Driver/Mysqli/Result.php @@ -275,12 +275,10 @@ public function rewind() /** * Valid - * - * @return bool */ #[ReturnTypeWillChange] #[Override] - public function valid() + public function valid(): bool { if ($this->currentComplete) { return true; @@ -320,11 +318,9 @@ public function getFieldCount(): int /** * Get generated value - * - * @return mixed|null */ #[Override] - public function getGeneratedValue() + public function getGeneratedValue(): mixed { return $this->generatedValue; } diff --git a/src/Driver/Mysqli/Statement.php b/src/Driver/Mysqli/Statement.php index 730a068..0f4e530 100644 --- a/src/Driver/Mysqli/Statement.php +++ b/src/Driver/Mysqli/Statement.php @@ -22,11 +22,12 @@ final class Statement implements StatementInterface, DriverAwareInterface, ProfilerAwareInterface { + protected bool $bufferResults; protected \mysqli $mysqli; protected Mysqli $driver; - protected ?ProfilerInterface $profiler; + protected ?ProfilerInterface $profiler = null; protected string $sql = ''; @@ -36,8 +37,14 @@ final class Statement implements StatementInterface, DriverAwareInterface, Profi public function __construct( protected ParameterContainer $parameterContainer = new ParameterContainer(), - protected bool $bufferResults = false + array|bool $options = false ) { + if (is_array($options)) { + $this->bufferResults = $options['buffer_results'] ?? false; + } else { + // Provide backward compatibility for boolean parameter + $this->bufferResults = $options; + } } #[Override] diff --git a/src/Driver/Pdo/Pdo.php b/src/Driver/Pdo/Pdo.php index fa89fe7..79b98ac 100644 --- a/src/Driver/Pdo/Pdo.php +++ b/src/Driver/Pdo/Pdo.php @@ -7,14 +7,32 @@ use Override; use PDOStatement; use PhpDb\Adapter\Driver\Pdo\AbstractPdo; +use PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection; use PhpDb\Adapter\Driver\Pdo\Result; +use PhpDb\Adapter\Driver\Pdo\Statement; +use PhpDb\Adapter\Driver\PdoDriverAwareInterface; use PhpDb\Adapter\Driver\ResultInterface; +use PhpDb\Adapter\Driver\StatementInterface; use PhpDb\Adapter\Mysql\DatabasePlatformNameTrait; class Pdo extends AbstractPdo { use DatabasePlatformNameTrait; + public function __construct( + protected AbstractPdoConnection|\PDO $connection, + protected StatementInterface&PdoDriverAwareInterface $statementPrototype = new Statement(), + protected ResultInterface $resultPrototype = new Result(), + array $features = [], + ) { + parent::__construct( + $connection, + $statementPrototype, + $resultPrototype, + $features + ); + } + /** * @param PDOStatement $resource */ diff --git a/src/Metadata/Source/MysqlMetadata.php b/src/Metadata/Source/MysqlMetadata.php index 05e1bf8..7e771b3 100644 --- a/src/Metadata/Source/MysqlMetadata.php +++ b/src/Metadata/Source/MysqlMetadata.php @@ -20,6 +20,7 @@ use const CASE_LOWER; use const PREG_PATTERN_ORDER; +/** @internal */ final class MysqlMetadata extends AbstractSource { /** diff --git a/src/Platform/Mysql.php b/src/Platform/Mysql.php index c87a870..d076599 100644 --- a/src/Platform/Mysql.php +++ b/src/Platform/Mysql.php @@ -99,6 +99,7 @@ protected function quoteViaDriver(string $value): ?string $resource = $this->driver; } + // todo: Refactor using match if ($resource instanceof mysqli) { return '\'' . $resource->real_escape_string($value) . '\''; } diff --git a/src/Sql/Platform/Mysql/Ddl/AlterTableDecorator.php b/src/Sql/Platform/Mysql/Ddl/AlterTableDecorator.php index 94e9d1a..b5dbe9a 100644 --- a/src/Sql/Platform/Mysql/Ddl/AlterTableDecorator.php +++ b/src/Sql/Platform/Mysql/Ddl/AlterTableDecorator.php @@ -4,9 +4,12 @@ namespace PhpDb\Adapter\Mysql\Sql\Platform\Mysql\Ddl; +use Override; use PhpDb\Adapter\Platform\PlatformInterface; use PhpDb\Sql\Ddl\AlterTable; use PhpDb\Sql\Platform\PlatformDecoratorInterface; +use PhpDb\Sql\PreparableSqlInterface; +use PhpDb\Sql\SqlInterface; use function count; use function range; @@ -18,10 +21,10 @@ use function substr_replace; use function uksort; +/** @internal */ final class AlterTableDecorator extends AlterTable implements PlatformDecoratorInterface { - /** @var AlterTable */ - protected $subject; + protected SqlInterface|PreparableSqlInterface|null $subject = null; /** @var array{ * unsigned: int, @@ -49,22 +52,16 @@ final class AlterTableDecorator extends AlterTable implements PlatformDecoratorI 'after' => 6, ]; - /** - * @param AlterTable $subject - * @return $this Provides a fluent interface - */ - public function setSubject($subject) - { + #[Override] + public function setSubject( + SqlInterface|PreparableSqlInterface|null $subject + ): PlatformDecoratorInterface { $this->subject = $subject; return $this; } - /** - * @param string $sql - * @return array - */ - protected function getSqlInsertOffsets($sql) + protected function getSqlInsertOffsets(string $sql): array { $sqlLength = strlen($sql); $insertStart = []; @@ -94,10 +91,8 @@ protected function getSqlInsertOffsets($sql) return $insertStart; } - /** - * @return array - */ - protected function processAddColumns(?PlatformInterface $adapterPlatform = null) + #[Override] + protected function processAddColumns(?PlatformInterface $adapterPlatform = null): array { $sqls = []; @@ -162,10 +157,8 @@ protected function processAddColumns(?PlatformInterface $adapterPlatform = null) return [$sqls]; } - /** - * @return array - */ - protected function processChangeColumns(?PlatformInterface $adapterPlatform = null) + #[Override] + protected function processChangeColumns(?PlatformInterface $adapterPlatform = null): array { $sqls = []; foreach ($this->changeColumns as $name => $column) { @@ -230,11 +223,7 @@ protected function processChangeColumns(?PlatformInterface $adapterPlatform = nu return [$sqls]; } - /** - * @param string $name - * @return string - */ - private function normalizeColumnOption($name) + private function normalizeColumnOption(string $name): string { return strtolower(str_replace(['-', '_', ' '], '', $name)); } diff --git a/src/Sql/Platform/Mysql/Ddl/CreateTableDecorator.php b/src/Sql/Platform/Mysql/Ddl/CreateTableDecorator.php index 2e7ae46..b2c69ac 100644 --- a/src/Sql/Platform/Mysql/Ddl/CreateTableDecorator.php +++ b/src/Sql/Platform/Mysql/Ddl/CreateTableDecorator.php @@ -4,9 +4,12 @@ namespace PhpDb\Adapter\Mysql\Sql\Platform\Mysql\Ddl; +use Override; use PhpDb\Adapter\Platform\PlatformInterface; use PhpDb\Sql\Ddl\CreateTable; use PhpDb\Sql\Platform\PlatformDecoratorInterface; +use PhpDb\Sql\PreparableSqlInterface; +use PhpDb\Sql\SqlInterface; use function count; use function range; @@ -18,10 +21,10 @@ use function substr_replace; use function uksort; +/** @internal */ final class CreateTableDecorator extends CreateTable implements PlatformDecoratorInterface { - /** @var CreateTable */ - protected $subject; + protected SqlInterface|PreparableSqlInterface|null $subject = null; /** @var int[] */ protected $columnOptionSortOrder = [ @@ -40,18 +43,15 @@ final class CreateTableDecorator extends CreateTable implements PlatformDecorato * @param CreateTable $subject * @return $this Provides a fluent interface */ - public function setSubject($subject) - { + public function setSubject( + SqlInterface|PreparableSqlInterface|null $subject + ): PlatformDecoratorInterface { $this->subject = $subject; return $this; } - /** - * @param string $sql - * @return array - */ - protected function getSqlInsertOffsets($sql) + protected function getSqlInsertOffsets(string $sql): array { $sqlLength = strlen($sql); $insertStart = []; @@ -81,13 +81,11 @@ protected function getSqlInsertOffsets($sql) return $insertStart; } - /** - * {@inheritDoc} - */ - protected function processColumns(?PlatformInterface $platform = null) + #[Override] + protected function processColumns(?PlatformInterface $platform = null): array { if (! $this->columns) { - return; + return []; } $sqls = []; @@ -152,11 +150,7 @@ protected function processColumns(?PlatformInterface $platform = null) return [$sqls]; } - /** - * @param string $name - * @return string - */ - private function normalizeColumnOption($name) + private function normalizeColumnOption(string $name): string { return strtolower(str_replace(['-', '_', ' '], '', $name)); } @@ -164,10 +158,9 @@ private function normalizeColumnOption($name) /** * @param string $columnA * @param string $columnB - * @return int */ // phpcs:ignore SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedMethod - private function compareColumnOptions($columnA, $columnB) + private function compareColumnOptions($columnA, $columnB): int { $columnA = $this->normalizeColumnOption($columnA); $columnA = $this->columnOptionSortOrder[$columnA] ?? count($this->columnOptionSortOrder); diff --git a/src/Sql/Platform/Mysql/SelectDecorator.php b/src/Sql/Platform/Mysql/SelectDecorator.php index dbedd20..c781336 100644 --- a/src/Sql/Platform/Mysql/SelectDecorator.php +++ b/src/Sql/Platform/Mysql/SelectDecorator.php @@ -8,24 +8,26 @@ use PhpDb\Adapter\ParameterContainer; use PhpDb\Adapter\Platform\PlatformInterface; use PhpDb\Sql\Platform\PlatformDecoratorInterface; +use PhpDb\Sql\PreparableSqlInterface; use PhpDb\Sql\Select; +use PhpDb\Sql\SqlInterface; +/** @internal */ final class SelectDecorator extends Select implements PlatformDecoratorInterface { - /** @var Select */ - protected $subject; + protected SqlInterface|PreparableSqlInterface|null $subject = null; /** - * @param Select $subject * @return $this */ - public function setSubject($subject) - { + public function setSubject( + SqlInterface|PreparableSqlInterface|null $subject + ): PlatformDecoratorInterface { $this->subject = $subject; return $this; } - protected function localizeVariables() + protected function localizeVariables(): void { parent::localizeVariables(); if ($this->limit === null && $this->offset !== null) { diff --git a/test/integration/Container/AdapterAbstractServiceFactoryTest.php b/test/integration/Container/AdapterAbstractServiceFactoryTest.php new file mode 100644 index 0000000..0de7502 --- /dev/null +++ b/test/integration/Container/AdapterAbstractServiceFactoryTest.php @@ -0,0 +1,154 @@ +serviceManager = $this->buildContainer(); + parent::setUp(); + } + + protected function buildContainer(array $config = []): ContainerInterface + { + $readAdapterConfig = [ + 'db' => [ + 'adapters' => [ + 'PhpDb\Adapter\Reader' => [ + 'driver' => Pdo::class, + 'connection' => [ + 'hostname' => (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_HOSTNAME') ?: 'localhost', + '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', + 'charset' => 'utf8', + 'driver_options' => [], + ], + 'options' => [ + 'buffer_results' => false, + ], + ], + ], + ], + ]; + + $writeAdapterConfig = [ + 'db' => [ + 'adapters' => [ + 'PhpDb\Adapter\Writer' => [ + 'driver' => Mysqli::class, + 'connection' => [ + 'hostname' => (string) getenv('TESTS_PHPDB_ADAPTER_MYSQL_HOSTNAME') ?: 'localhost', + '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', + 'charset' => 'utf8', + 'driver_options' => [], + ], + 'options' => [ + 'buffer_results' => false, + ], + ], + ], + ], + ]; + + $adapterConfig = ArrayUtils::merge($readAdapterConfig, $writeAdapterConfig); + + // merge service config from both PhpDb and PhpDb\Adapter\Mysql + $serviceManagerConfig = ArrayUtils::merge( + (new LaminasDbConfigProvider())()['dependencies'], + (new ConfigProvider())()['dependencies'] + ); + + $serviceManagerConfig = ArrayUtils::merge( + $serviceManagerConfig, + $adapterConfig + ); + + // prefer passed config over environment variables + if ($config !== []) { + $serviceManagerConfig = ArrayUtils::merge($serviceManagerConfig, $config); + } + + $serviceManagerConfig = ArrayUtils::merge( + $serviceManagerConfig, + [ + 'services' => [ + 'config' => $serviceManagerConfig, + ], + ] + ); + + $config = $serviceManagerConfig; + return new ServiceManager($config); + } + + public static function providerValidService(): array + { + return [ + ['PhpDb\Adapter\Writer'], + ['PhpDb\Adapter\Reader'], + ]; + } + + public static function providerInvalidService(): array + { + return [ + ['PhpDb\Adapter\Unknown'], + ]; + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + #[DataProvider('providerValidService')] + public function testValidService(string $service): void + { + $actual = $this->serviceManager->get($service); + self::assertInstanceOf(AdapterInterface::class, $actual); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + #[DataProvider('providerInvalidService')] + public function testInvalidService(string $service): void + { + $this->expectException(ServiceNotFoundException::class); + $this->serviceManager->get($service); + } +} diff --git a/test/integration/Container/MysqliResultFactoryTest.php b/test/integration/Container/MysqliResultFactoryTest.php deleted file mode 100644 index 30ed58a..0000000 --- a/test/integration/Container/MysqliResultFactoryTest.php +++ /dev/null @@ -1,30 +0,0 @@ -container); - - self::assertInstanceOf(ResultInterface::class, $result); - self::assertInstanceOf(Result::class, $result); - } -} diff --git a/test/integration/Container/MysqliStatementFactoryTest.php b/test/integration/Container/MysqliStatementFactoryTest.php deleted file mode 100644 index 419a0c1..0000000 --- a/test/integration/Container/MysqliStatementFactoryTest.php +++ /dev/null @@ -1,39 +0,0 @@ -getAdapter([ - 'db' => [ - 'driver' => 'Mysqli', - 'options' => [ - 'buffer_results' => false, - ], - ], - ]); - - $factory = new MysqliStatementFactory(); - $statement = $factory($this->container); - - self::assertInstanceOf(StatementInterface::class, $statement); - self::assertInstanceOf(Statement::class, $statement); - } -} diff --git a/test/integration/Container/PdoResultFactoryTest.php b/test/integration/Container/PdoResultFactoryTest.php deleted file mode 100644 index 81d3177..0000000 --- a/test/integration/Container/PdoResultFactoryTest.php +++ /dev/null @@ -1,31 +0,0 @@ -container); - - self::assertInstanceOf(ResultInterface::class, $result); - self::assertInstanceOf(Result::class, $result); - } -} diff --git a/test/integration/Container/PdoStatementFactoryTest.php b/test/integration/Container/PdoStatementFactoryTest.php deleted file mode 100644 index 21e8712..0000000 --- a/test/integration/Container/PdoStatementFactoryTest.php +++ /dev/null @@ -1,30 +0,0 @@ -container); - self::assertInstanceOf(StatementInterface::class, $statement); - self::assertInstanceOf(Statement::class, $statement); - } -} diff --git a/test/integration/Container/StatementInterfaceFactoryTest.php b/test/integration/Container/StatementInterfaceFactoryTest.php new file mode 100644 index 0000000..3ddcec5 --- /dev/null +++ b/test/integration/Container/StatementInterfaceFactoryTest.php @@ -0,0 +1,39 @@ +container, PdoStatement::class); + self::assertInstanceOf(StatementInterface::class, $statement); + self::assertInstanceOf(PdoStatement::class, $statement); + } + + public function testInvokeReturnsMysqliStatement(): void + { + $factory = new StatementInterfaceFactory(); + $statement = $factory($this->container, MysqliStatement::class); + self::assertInstanceOf(StatementInterface::class, $statement); + self::assertInstanceOf(MysqliStatement::class, $statement); + } +}