From 583033bc90e6be598669550a3b2b33684a57c61d Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Mon, 28 Sep 2026 16:50:59 +1000 Subject: [PATCH] Route the Adapter throw sites through named constructors and remove VunerablePlatformQuoteException All 26 throw sites in src/Adapter now call a named constructor. Message text is byte-identical, verified template by template against the originals. - Add 13 templates and factories to Adapter\Exception\RuntimeException, 6 to its InvalidArgumentException and 2 to InvalidQueryException - Converge the drifted PDO param message on the quoted form, per #189 - Collapse the four "Data does not exist for this name/position" copies in ParameterContainer onto one template - Remove VunerablePlatformQuoteException. It held no state, implemented no external contract and reported a fail-fast setup error, so it met none of the three tests for a condition earning its own class. Its two throw sites now call RuntimeException::forVulnerablePlatformQuote(), which renders the same message, and the misspelling goes with the class - Drop the class from ExceptionHierarchyTest's EXCEPTION_CLASSES map and retire the guard's exemption, which existed only for that class, so no exception class is exempt from the constant naming rule any more - Give the two driver-error passthroughs a DRIVER_ERROR template of '%s', documented as carrying the driver's own wording verbatim - Cover every factory with a template test, and derive the 16 Adapter message assertions from the templates rather than restating them - Declare CoversMethod for Adapter::executeQuery(), which four tests exercise but none declared, so PHPUnit recorded no coverage for the method - Re-attribute three baselined entries to the factories and class that now carry them, exclude src/Adapter/Exception/ from too-many-methods, and baseline the halstead reading Pdo\Statement shifts InvalidConnectionParametersException keeps its constructor rather than gaining a named constructor: its message is supplied by the adapter that throws it, so there is no template for core to own. It earns its own class on the extra state it carries. BC: removing VunerablePlatformQuoteException breaks any catch of that exact type. It extended Adapter\Exception\RuntimeException, so a catch of that is unaffected. The tests in phpdb-pgsql and phpdb-sqlite that name the class need updating in their own repositories. Signed-off-by: Simon Mundy --- analyzer-baseline.toml | 6 +- lint-baseline.toml | 6 + mago.toml | 7 + src/Adapter/Adapter.php | 8 +- .../Feature/DriverFeatureProviderTrait.php | 8 +- src/Adapter/Driver/Pdo/AbstractPdo.php | 12 +- .../Driver/Pdo/AbstractPdoConnection.php | 10 +- src/Adapter/Driver/Pdo/Result.php | 17 +-- src/Adapter/Driver/Pdo/Statement.php | 15 +-- .../Exception/InvalidArgumentException.php | 47 ++++++- .../Exception/InvalidQueryException.php | 22 ++- src/Adapter/Exception/RuntimeException.php | 112 +++++++++++++++- .../VunerablePlatformQuoteException.php | 22 --- src/Adapter/ParameterContainer.php | 10 +- src/Adapter/Platform/AbstractPlatform.php | 7 +- src/Adapter/Platform/Sql92.php | 7 +- src/Adapter/Profiler/Profiler.php | 4 +- test/unit/Adapter/AdapterTest.php | 18 +-- .../DriverFeatureProviderTraitTest.php | 11 +- .../Adapter/Driver/Pdo/ConnectionTest.php | 6 +- .../Driver/Pdo/ConnectionTransactionsTest.php | 8 +- test/unit/Adapter/Driver/Pdo/ResultTest.php | 12 +- .../unit/Adapter/Driver/Pdo/StatementTest.php | 16 ++- .../InvalidArgumentExceptionTest.php | 80 +++++++++++ .../Exception/InvalidQueryExceptionTest.php | 59 +++++++++ .../Exception/RuntimeExceptionTest.php | 125 ++++++++++++++++++ test/unit/Adapter/ParameterContainerTest.php | 20 +-- test/unit/Adapter/Platform/Sql92Test.php | 10 +- test/unit/Adapter/Profiler/ProfilerTest.php | 6 +- .../unit/Exception/ExceptionHierarchyTest.php | 11 -- test/unit/Sql/Predicate/PredicateTest.php | 4 +- 31 files changed, 548 insertions(+), 158 deletions(-) delete mode 100644 src/Adapter/Exception/VunerablePlatformQuoteException.php create mode 100644 test/unit/Adapter/Exception/InvalidArgumentExceptionTest.php create mode 100644 test/unit/Adapter/Exception/InvalidQueryExceptionTest.php create mode 100644 test/unit/Adapter/Exception/RuntimeExceptionTest.php diff --git a/analyzer-baseline.toml b/analyzer-baseline.toml index 93a74a29..57e0f127 100644 --- a/analyzer-baseline.toml +++ b/analyzer-baseline.toml @@ -441,7 +441,7 @@ count = 1 [[issues]] file = "src/Adapter/Driver/Pdo/AbstractPdoConnection.php" code = "mixed-argument" -message = "Invalid argument type for argument #1 of `Exception::__construct`: expected `string`, but found `mixed`." +message = 'Invalid argument type for argument #1 of `PhpDb\Adapter\Exception\InvalidQueryException::forDriverError`: expected `string`, but found `mixed`.' count = 1 [[issues]] @@ -1125,7 +1125,7 @@ count = 2 [[issues]] file = "src/Adapter/Platform/AbstractPlatform.php" code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PhpDb\Adapter\Exception\VunerablePlatformQuoteException` in `PhpDb\Adapter\Platform\AbstractPlatform::quoteValue`.' +message = 'Potentially unhandled exception `PhpDb\Adapter\Exception\RuntimeException` in `PhpDb\Adapter\Platform\AbstractPlatform::quoteValue`.' count = 1 [[issues]] @@ -1155,7 +1155,7 @@ count = 1 [[issues]] file = "src/Adapter/Platform/Sql92.php" code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PhpDb\Adapter\Exception\VunerablePlatformQuoteException` in `PhpDb\Adapter\Platform\Sql92::quoteValue`.' +message = 'Potentially unhandled exception `PhpDb\Adapter\Exception\RuntimeException` in `PhpDb\Adapter\Platform\Sql92::quoteValue`.' count = 1 [[issues]] diff --git a/lint-baseline.toml b/lint-baseline.toml index f4f30de4..a6844e3f 100644 --- a/lint-baseline.toml +++ b/lint-baseline.toml @@ -8471,3 +8471,9 @@ file = "test/unit/TableGateway/TableGatewayTest.php" code = "strict-assertions" message = "Use strict assertions in PHPUnit tests." count = 2 + +[[issues]] +file = "src/Adapter/Driver/Pdo/Statement.php" +code = "halstead" +message = "Method has a high halstead difficulty" +count = 1 diff --git a/mago.toml b/mago.toml index 5ef92248..7fc4485d 100644 --- a/mago.toml +++ b/mago.toml @@ -11,6 +11,13 @@ parentheses-around-new-in-member-access = true [linter] baseline = "lint-baseline.toml" +[linter.rules] +# Exception classes are message catalogues: one template and one named constructor per +# condition, per the convention in docs/book/exceptions.md. The method count tracks the +# number of distinct failures a component can report, so the maintainability signal does +# not apply. "test/" repeats the phpdb-qa-tools default, which this key replaces. +too-many-methods = { exclude = ["test/", "src/Adapter/Exception/"] } + [analyzer] baseline = "analyzer-baseline.toml" class-initializers = [ diff --git a/src/Adapter/Adapter.php b/src/Adapter/Adapter.php index 6d0a1961..7e2a42b8 100644 --- a/src/Adapter/Adapter.php +++ b/src/Adapter/Adapter.php @@ -63,7 +63,7 @@ public function executeQuery(Driver\StatementInterface|string $sql): Driver\Resu : $this->driver->getConnection()->execute($sql); if (! $result instanceof Driver\ResultInterface) { - throw new Exception\RuntimeException('Query execution did not produce a result'); + throw Exception\RuntimeException::forMissingQueryResult(); } return $result; @@ -171,9 +171,7 @@ public function query( $parametersOrQueryMode instanceof ParameterContainer, is_array($parametersOrQueryMode), => $this->prepareQuery($sql, $parametersOrQueryMode), - default => throw new Exception\InvalidArgumentException( - 'Flag incorrectly set', - ), + default => throw Exception\InvalidArgumentException::forIncorrectFlag(), }; $result = $this->executeQuery($sql); @@ -199,7 +197,7 @@ public function __get(string $name): Driver\DriverInterface|Platform\PlatformInt return match (strtolower($name)) { 'driver' => $this->driver, 'platform' => $this->platform, - default => throw new Exception\InvalidArgumentException('Invalid magic property on adapter'), + default => throw Exception\InvalidArgumentException::forInvalidMagicProperty(), }; } } diff --git a/src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php b/src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php index d7e77a27..665e0559 100644 --- a/src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php +++ b/src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php @@ -8,8 +8,6 @@ use PhpDb\Adapter\Driver\DriverInterface; use PhpDb\Adapter\Exception\RuntimeException; -use function sprintf; - /** * Trait implementation of DriverFeatureProviderInterface. * @@ -27,11 +25,7 @@ trait DriverFeatureProviderTrait public function addFeature(DriverFeatureInterface $feature): DriverFeatureProviderInterface { if (! $this instanceof DriverInterface) { - throw new RuntimeException(sprintf( - '%s can only be composed into %s', - __TRAIT__, - DriverInterface::class, - )); + throw RuntimeException::forUncomposableTrait(__TRAIT__, DriverInterface::class); } $feature->setDriver($this); diff --git a/src/Adapter/Driver/Pdo/AbstractPdo.php b/src/Adapter/Driver/Pdo/AbstractPdo.php index 925fff9a..a73ade04 100644 --- a/src/Adapter/Driver/Pdo/AbstractPdo.php +++ b/src/Adapter/Driver/Pdo/AbstractPdo.php @@ -23,7 +23,6 @@ use function is_string; use function ltrim; use function preg_match; -use function sprintf; abstract class AbstractPdo implements PdoDriverInterface, ProfilerAwareInterface { @@ -44,9 +43,7 @@ public function checkEnvironment(): bool { if (! extension_loaded('PDO')) { // @codeCoverageIgnoreStart - throw new Exception\RuntimeException( - 'The PDO extension is required for this adapter but the extension is not loaded', - ); + throw Exception\RuntimeException::forMissingPdoExtension(); // @codeCoverageIgnoreEnd } @@ -94,12 +91,7 @@ public function formatParameterName(string|int $name, ?string $type = null): str $name = ltrim($name, ':'); // @see https://bugs.php.net/bug.php?id=43130 if (preg_match('/[^a-zA-Z0-9_]/', $name)) { - throw new Exception\RuntimeException(sprintf( - 'The PDO param %s contains invalid characters.' - . ' Only alphabetic characters, digits, and underscores (_)' - . ' are allowed.', - $name, - )); + throw Exception\RuntimeException::forInvalidPdoParam($name); } return ":{$name}"; } diff --git a/src/Adapter/Driver/Pdo/AbstractPdoConnection.php b/src/Adapter/Driver/Pdo/AbstractPdoConnection.php index 42fde323..1e3f314e 100644 --- a/src/Adapter/Driver/Pdo/AbstractPdoConnection.php +++ b/src/Adapter/Driver/Pdo/AbstractPdoConnection.php @@ -91,7 +91,7 @@ public function execute($sql): ?ResultInterface if (false === $resultResource) { $errorInfo = $this->resource->errorInfo(); - throw new Exception\InvalidQueryException($errorInfo[2]); + throw Exception\InvalidQueryException::forDriverError($errorInfo[2]); } /** @phpstan-ignore arguments.count */ @@ -107,9 +107,7 @@ public function execute($sql): ?ResultInterface final public function getDsn(): string { if (! $this->dsn) { - throw new Exception\RuntimeException( - 'The DSN has not been set or constructed from parameters in connect() for this Connection', - ); + throw Exception\RuntimeException::forMissingDsn(); } return $this->dsn; @@ -141,11 +139,11 @@ public function prepare(?string $sql = null): StatementInterface public function rollback(): ConnectionInterface { if (! $this->isConnected()) { - throw new Exception\RuntimeException('Must be connected before you can rollback'); + throw Exception\RuntimeException::forDisconnectedRollback(); } if (! $this->inTransaction()) { - throw new Exception\RuntimeException('Must call beginTransaction() before you can rollback'); + throw Exception\RuntimeException::forRollbackWithoutTransaction(); } $this->resource->rollBack(); diff --git a/src/Adapter/Driver/Pdo/Result.php b/src/Adapter/Driver/Pdo/Result.php index b87fb0de..41460dd4 100644 --- a/src/Adapter/Driver/Pdo/Result.php +++ b/src/Adapter/Driver/Pdo/Result.php @@ -167,10 +167,7 @@ public function getGeneratedValue(): string|int|false|null public function getQueryResult(?ResultSetInterface $resultPrototype = null): ResultSetInterface { if (! $this->isQueryResult()) { - throw new Exception\RuntimeException( - 'Cannot produce a query result set from a result that is not a query result;' - . ' check isQueryResult() first', - ); + throw Exception\RuntimeException::forNonQueryResult(); } $resultPrototype ??= new ResultSet(); @@ -265,9 +262,7 @@ public function next() public function rewind() { if (self::STATEMENT_MODE_FORWARD === $this->statementMode && $this->position > 0) { - throw new Exception\RuntimeException( - 'This result is a forward only result set, calling rewind() after moving forward is not supported', - ); + throw Exception\RuntimeException::forForwardOnlyRewind(); } if (! $this->currentComplete) { $this->currentData = $this->resource->fetch($this->fetchMode); @@ -282,9 +277,7 @@ public function rewind() public function setFetchMode(int $fetchMode): void { if (! in_array($fetchMode, self::VALID_FETCH_MODES, true)) { - throw new Exception\InvalidArgumentException( - 'The fetch mode must be one of the PDO::FETCH_* constants.', - ); + throw Exception\InvalidArgumentException::forInvalidFetchMode(); } $this->fetchMode = (int) $fetchMode; @@ -293,9 +286,7 @@ public function setFetchMode(int $fetchMode): void public function setStatementMode(string $statementMode = self::STATEMENT_MODE_FORWARD): void { if (! in_array($statementMode, [self::STATEMENT_MODE_SCROLLABLE, self::STATEMENT_MODE_FORWARD], true)) { - throw new Exception\InvalidArgumentException( - 'The statement mode must be one of the defined constants.', - ); + throw Exception\InvalidArgumentException::forInvalidStatementMode(); } $this->statementMode = $statementMode; diff --git a/src/Adapter/Driver/Pdo/Statement.php b/src/Adapter/Driver/Pdo/Statement.php index dea6152d..794acd37 100644 --- a/src/Adapter/Driver/Pdo/Statement.php +++ b/src/Adapter/Driver/Pdo/Statement.php @@ -23,7 +23,6 @@ use function is_int; use function ltrim; use function preg_match; -use function sprintf; class Statement implements StatementInterface, PdoDriverAwareInterface, ProfilerAwareInterface { @@ -83,8 +82,8 @@ public function execute(array|ParameterContainer|null $parameters = null): ?Resu $code = 0; } - throw new Exception\InvalidQueryException( - 'Statement could not be executed (' . implode(' - ', $this->resource->errorInfo()) . ')', + throw Exception\InvalidQueryException::forFailedStatement( + implode(' - ', $this->resource->errorInfo()), $code, $e, ); @@ -138,7 +137,7 @@ public function isPrepared(): bool public function prepare(?string $sql = null): StatementInterface { if ($this->isPrepared) { - throw new Exception\RuntimeException('This statement has been prepared already'); + throw Exception\RuntimeException::forAlreadyPrepared(); } if (null === $sql) { @@ -149,7 +148,7 @@ public function prepare(?string $sql = null): StatementInterface if (false === $this->resource) { $error = $this->pdo->errorInfo(); - throw new Exception\RuntimeException($error[2]); + throw Exception\RuntimeException::forDriverError($error[2]); } $this->isPrepared = true; @@ -224,11 +223,7 @@ protected function bindParametersFromContainer(): void $parameter = $name + 1; } else { if (! preg_match('/^:?[a-zA-Z0-9_]+$/', $name)) { - throw new Exception\RuntimeException(sprintf( - 'The PDO param "%s" contains invalid characters.' - . ' Only alphabetic characters, digits, and underscores (_) are allowed.', - $name, - )); + throw Exception\RuntimeException::forInvalidPdoParam($name); } $parameter = ':' . ltrim($name, ':'); } diff --git a/src/Adapter/Exception/InvalidArgumentException.php b/src/Adapter/Exception/InvalidArgumentException.php index e459554c..d7267f77 100644 --- a/src/Adapter/Exception/InvalidArgumentException.php +++ b/src/Adapter/Exception/InvalidArgumentException.php @@ -1,7 +1,52 @@ positions[$name]; } if (! array_key_exists($name, $this->data)) { - throw new Exception\InvalidArgumentException('Data does not exist for this name/position'); + throw Exception\InvalidArgumentException::forMissingData(); } return $this->errata[$name]; } @@ -212,7 +212,7 @@ public function offsetGetMaxLength(string|int $name): mixed $name = $this->positions[$name]; } if (! array_key_exists($name, $this->data)) { - throw new Exception\InvalidArgumentException('Data does not exist for this name/position'); + throw Exception\InvalidArgumentException::forMissingData(); } return $this->maxLength[$name]; } @@ -276,7 +276,7 @@ public function offsetSet(mixed $name, mixed $value, mixed $errata = null, mixed } elseif (null === $name) { $name = (string) count($this->data); } else { - throw new Exception\InvalidArgumentException('Keys must be string, integer or null'); + throw Exception\InvalidArgumentException::forInvalidKeyType(); } if ($isNewPosition) { @@ -345,7 +345,7 @@ public function offsetUnsetErrata(string|int $name): void $name = $this->positions[$name]; } if (! array_key_exists($name, $this->errata)) { - throw new Exception\InvalidArgumentException('Data does not exist for this name/position'); + throw Exception\InvalidArgumentException::forMissingData(); } $this->errata[$name] = null; } @@ -361,7 +361,7 @@ public function offsetUnsetMaxLength(string|int $name): void $name = $this->positions[$name]; } if (! array_key_exists($name, $this->maxLength)) { - throw new Exception\InvalidArgumentException('Data does not exist for this name/position'); + throw Exception\InvalidArgumentException::forMissingData(); } $this->maxLength[$name] = null; } diff --git a/src/Adapter/Platform/AbstractPlatform.php b/src/Adapter/Platform/AbstractPlatform.php index d2159216..dd09a77b 100644 --- a/src/Adapter/Platform/AbstractPlatform.php +++ b/src/Adapter/Platform/AbstractPlatform.php @@ -7,7 +7,7 @@ use Override; use PDO; use PhpDb\Adapter\Driver; -use PhpDb\Adapter\Exception\VunerablePlatformQuoteException; +use PhpDb\Adapter\Exception; use function addcslashes; use function array_map; @@ -144,10 +144,7 @@ public function quoteTrustedValue(int|float|string|bool $value): ?string public function quoteValue(string $value): string { if (! isset($this->driver)) { - throw VunerablePlatformQuoteException::forPlatformAndMethod( - static::class, - __METHOD__, - ); + throw Exception\RuntimeException::forVulnerablePlatformQuote(static::class, __METHOD__); } return '\'' . addcslashes($value, "\x00\n\r\\'\"\x1a") . '\''; } diff --git a/src/Adapter/Platform/Sql92.php b/src/Adapter/Platform/Sql92.php index 7bade002..61ab0608 100644 --- a/src/Adapter/Platform/Sql92.php +++ b/src/Adapter/Platform/Sql92.php @@ -5,7 +5,7 @@ namespace PhpDb\Adapter\Platform; use Override; -use PhpDb\Adapter\Exception\VunerablePlatformQuoteException; +use PhpDb\Adapter\Exception; use PhpDb\Sql\Platform\Platform; use PhpDb\Sql\Platform\PlatformDecoratorInterface; @@ -40,10 +40,7 @@ public function getSqlPlatformDecorator(): PlatformDecoratorInterface public function quoteValue(string $value): string { if (! isset($this->driver)) { - throw VunerablePlatformQuoteException::forPlatformAndMethod( - static::class, - __METHOD__, - ); + throw Exception\RuntimeException::forVulnerablePlatformQuote(static::class, __METHOD__); } return '\'' . addcslashes($value, "\x00\n\r\\'\"\x1a") . '\''; } diff --git a/src/Adapter/Profiler/Profiler.php b/src/Adapter/Profiler/Profiler.php index 573520c1..da67fecb 100644 --- a/src/Adapter/Profiler/Profiler.php +++ b/src/Adapter/Profiler/Profiler.php @@ -55,9 +55,7 @@ public function getProfiles(): array public function profilerFinish(): ProfilerInterface { if (! isset($this->profiles[$this->currentIndex])) { - throw new Exception\RuntimeException( - 'A profile must be started before ' . __FUNCTION__ . ' can be called.', - ); + throw Exception\RuntimeException::forUnstartedProfile(__FUNCTION__); } $current = &$this->profiles[$this->currentIndex]; $current['end'] = microtime(true); diff --git a/test/unit/Adapter/AdapterTest.php b/test/unit/Adapter/AdapterTest.php index 14efdba6..1a1f94fd 100644 --- a/test/unit/Adapter/AdapterTest.php +++ b/test/unit/Adapter/AdapterTest.php @@ -13,7 +13,6 @@ use PhpDb\Adapter\Driver\StatementInterface; use PhpDb\Adapter\Exception\InvalidArgumentException; use PhpDb\Adapter\Exception\RuntimeException; -use PhpDb\Adapter\Exception\VunerablePlatformQuoteException; use PhpDb\Adapter\ParameterContainer; use PhpDb\Adapter\Platform\PlatformInterface; use PhpDb\Adapter\Profiler; @@ -32,6 +31,7 @@ #[CoversMethod(Adapter::class, 'getDriver')] #[CoversMethod(Adapter::class, 'getPlatform')] #[CoversMethod(Adapter::class, 'getQueryResultSetPrototype')] +#[CoversMethod(Adapter::class, 'executeQuery')] #[CoversMethod(Adapter::class, 'getCurrentSchema')] #[CoversMethod(Adapter::class, 'query')] #[CoversMethod(Adapter::class, 'createStatement')] @@ -91,8 +91,8 @@ public function testExecuteQueryThrowsWhenExecutionDoesNotProduceAResult(): void $sql = 'SELECT foo'; $this->mockConnection->method('execute')->willReturn(null); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Query execution did not produce a result'); + self::expectException(RuntimeException::class); + self::expectExceptionMessage(RuntimeException::MISSING_QUERY_RESULT); $this->adapter->executeQuery($sql); } @@ -155,11 +155,11 @@ public function testGetHelpersQuoteValueClosureCallsPlatform(): void $this->mockPlatform ->method('quoteValue') ->with('test') - ->willThrowException(VunerablePlatformQuoteException::forPlatformAndMethod('test', 'test')); + ->willThrowException(RuntimeException::forVulnerablePlatformQuote('test', 'test')); $functions = $this->adapter->getHelpers(Adapter::FUNCTION_QUOTE_VALUE); - $this->expectException(VunerablePlatformQuoteException::class); + self::expectException(RuntimeException::class); $functions[0]('test'); } @@ -226,8 +226,8 @@ public function testMagicGetReturnsDriverAndPlatformCaseInsensitively(): void self::assertSame($this->mockPlatform, $this->adapter->PlatForm); self::assertSame($this->mockPlatform, $this->adapter->platform); - $this->expectException('InvalidArgumentException'); - $this->expectExceptionMessage('Invalid magic'); + self::expectException('InvalidArgumentException'); + self::expectExceptionMessage(InvalidArgumentException::INVALID_MAGIC_PROPERTY); /** @phpstan-ignore property.notFound, expr.resultUnused */ $this->adapter->foo; } @@ -294,8 +294,8 @@ public function testProducedResultSetPrototypeIsDifferentForEachQuery(): void public function testQueryThrowsOnInvalidParameterType(): void { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Flag incorrectly set'); + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage(InvalidArgumentException::INCORRECT_FLAG); $this->adapter->query('SELECT 1', 'invalid_mode'); } diff --git a/test/unit/Adapter/Driver/Feature/DriverFeatureProviderTraitTest.php b/test/unit/Adapter/Driver/Feature/DriverFeatureProviderTraitTest.php index 09d9862e..ef473075 100644 --- a/test/unit/Adapter/Driver/Feature/DriverFeatureProviderTraitTest.php +++ b/test/unit/Adapter/Driver/Feature/DriverFeatureProviderTraitTest.php @@ -4,6 +4,7 @@ namespace PhpDbTest\Adapter\Driver\Feature; +use PhpDb\Adapter\Driver\DriverInterface; use PhpDb\Adapter\Driver\Feature\DriverFeatureInterface; use PhpDb\Adapter\Driver\Feature\DriverFeatureProviderInterface; use PhpDb\Adapter\Driver\Feature\DriverFeatureProviderTrait; @@ -13,6 +14,8 @@ use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; +use function sprintf; + #[Group('unit')] #[CoversMethod(DriverFeatureProviderTrait::class, 'addFeature')] #[CoversMethod(DriverFeatureProviderTrait::class, 'addFeatures')] @@ -49,8 +52,12 @@ public function testAddFeatureThrowsWhenUsedOutsideDriverInterface(): void $feature = $this->createMock(DriverFeatureInterface::class); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('can only be composed into'); + self::expectException(RuntimeException::class); + self::expectExceptionMessage(sprintf( + RuntimeException::UNCOMPOSABLE_TRAIT, + DriverFeatureProviderTrait::class, + DriverInterface::class, + )); $nonDriver->addFeature($feature); } diff --git a/test/unit/Adapter/Driver/Pdo/ConnectionTest.php b/test/unit/Adapter/Driver/Pdo/ConnectionTest.php index 1de9bcb3..8f876d09 100644 --- a/test/unit/Adapter/Driver/Pdo/ConnectionTest.php +++ b/test/unit/Adapter/Driver/Pdo/ConnectionTest.php @@ -101,7 +101,7 @@ public function testExecuteCallsProfilerFinishBeforeThrowingOnError(): void $driver = new TestPdo($connection); $connection->setProfiler($profiler); - $this->expectException(InvalidQueryException::class); + self::expectException(InvalidQueryException::class); @$connection->execute('INVALID SQL STATEMENT HERE %%%'); } @@ -152,8 +152,8 @@ public function testGetDsnThrowsWhenDsnIsNull(): void $reflection = new ReflectionProperty($connection, 'dsn'); $reflection->setValue($connection, null); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('The DSN has not been set'); + self::expectException(RuntimeException::class); + self::expectExceptionMessage(RuntimeException::MISSING_DSN); $connection->getDsn(); } diff --git a/test/unit/Adapter/Driver/Pdo/ConnectionTransactionsTest.php b/test/unit/Adapter/Driver/Pdo/ConnectionTransactionsTest.php index 73ea607e..65d9cc27 100644 --- a/test/unit/Adapter/Driver/Pdo/ConnectionTransactionsTest.php +++ b/test/unit/Adapter/Driver/Pdo/ConnectionTransactionsTest.php @@ -112,8 +112,8 @@ public function testRollbackDisconnectedThrowsException(): void { $this->wrapper->disconnect(); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Must be connected before you can rollback'); + self::expectException(RuntimeException::class); + self::expectExceptionMessage(RuntimeException::DISCONNECTED_ROLLBACK); $this->wrapper->rollback(); } @@ -132,8 +132,8 @@ public function testRollbackSetsInTransactionAtFalse(): void public function testRollbackWithoutBeginThrowsException(): void { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Must call beginTransaction() before you can rollback'); + self::expectException(RuntimeException::class); + self::expectExceptionMessage(RuntimeException::ROLLBACK_WITHOUT_TRANSACTION); $this->wrapper->rollback(); } diff --git a/test/unit/Adapter/Driver/Pdo/ResultTest.php b/test/unit/Adapter/Driver/Pdo/ResultTest.php index 1f9a6656..73f5b277 100644 --- a/test/unit/Adapter/Driver/Pdo/ResultTest.php +++ b/test/unit/Adapter/Driver/Pdo/ResultTest.php @@ -260,7 +260,7 @@ public function testGetQueryResultThrowsWhenResultIsNotAQueryResult(): void $result = new Result(); $result->initialize($stub, null); - $this->expectException(RuntimeException::class); + self::expectException(RuntimeException::class); $result->getQueryResult(); } @@ -371,7 +371,7 @@ public function testRewindThrowsExceptionOnForwardOnlyAfterAdvancing(): void $result->rewind(); $result->next(); - $this->expectException(RuntimeException::class); + self::expectException(RuntimeException::class); $result->rewind(); } @@ -387,8 +387,8 @@ public function testSetFetchModeThrowsOnInvalidFetchMode(): void { $result = new Result(); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The fetch mode must be one of the PDO::FETCH_* constants.'); + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage(InvalidArgumentException::INVALID_FETCH_MODE); $result->setFetchMode(9999); } @@ -397,7 +397,7 @@ public function testSetFetchModeThrowsOnInvalidMode(): void { $result = new Result(); - $this->expectException(InvalidArgumentException::class); + self::expectException(InvalidArgumentException::class); $result->setFetchMode(13); } @@ -405,7 +405,7 @@ public function testSetStatementModeThrowsOnInvalidMode(): void { $result = new Result(); - $this->expectException(InvalidArgumentException::class); + self::expectException(InvalidArgumentException::class); $result->setStatementMode('invalid'); } diff --git a/test/unit/Adapter/Driver/Pdo/StatementTest.php b/test/unit/Adapter/Driver/Pdo/StatementTest.php index 53a5be69..5b3eb200 100644 --- a/test/unit/Adapter/Driver/Pdo/StatementTest.php +++ b/test/unit/Adapter/Driver/Pdo/StatementTest.php @@ -23,6 +23,8 @@ use PHPUnit\Framework\TestCase; use ReflectionProperty; +use function sprintf; + #[CoversMethod(Statement::class, 'setDriver')] #[CoversMethod(Statement::class, 'setParameterContainer')] #[CoversMethod(Statement::class, 'getParameterContainer')] @@ -255,7 +257,7 @@ public function testExecuteCallsProfilerFinishOnFailure(): void $reflection = new ReflectionProperty($this->statement, 'resource'); $reflection->setValue($this->statement, $pdoStmt); - $this->expectException(InvalidQueryException::class); + self::expectException(InvalidQueryException::class); $this->statement->execute(); } @@ -324,7 +326,7 @@ public function testExecuteThrowsInvalidQueryExceptionOnPdoException(): void $reflection = new ReflectionProperty($this->statement, 'resource'); $reflection->setValue($this->statement, $pdoStmt); - $this->expectException(InvalidQueryException::class); + self::expectException(InvalidQueryException::class); $this->statement->execute(); } @@ -335,8 +337,8 @@ public function testExecuteThrowsOnInvalidParameterName(string $name): void $this->statement->initialize($pdo); $this->statement->prepare('SELECT 1'); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('contains invalid characters'); + self::expectException(RuntimeException::class); + self::expectExceptionMessage(sprintf(RuntimeException::INVALID_PDO_PARAM, $name)); $this->statement->execute([$name => 'value']); } @@ -443,7 +445,7 @@ public function testPrepareThrowsRuntimeExceptionOnPdoFailure(): void $this->statement->initialize($pdo); $this->statement->setSql('INVALID SQL'); - $this->expectException(RuntimeException::class); + self::expectException(RuntimeException::class); $this->statement->prepare(); } @@ -452,8 +454,8 @@ public function testPrepareThrowsWhenAlreadyPrepared(): void $this->statement->initialize(new SqliteMemoryPdo()); $this->statement->prepare('SELECT 1'); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('This statement has been prepared already'); + self::expectException(RuntimeException::class); + self::expectExceptionMessage(RuntimeException::ALREADY_PREPARED); $this->statement->prepare('SELECT 2'); } diff --git a/test/unit/Adapter/Exception/InvalidArgumentExceptionTest.php b/test/unit/Adapter/Exception/InvalidArgumentExceptionTest.php new file mode 100644 index 00000000..fb178ab8 --- /dev/null +++ b/test/unit/Adapter/Exception/InvalidArgumentExceptionTest.php @@ -0,0 +1,80 @@ +, string}> */ + public static function namedConstructorProvider(): array + { + return [ + 'incorrect flag' => [ + 'forIncorrectFlag', + [], + InvalidArgumentException::INCORRECT_FLAG, + ], + 'invalid fetch mode' => [ + 'forInvalidFetchMode', + [], + InvalidArgumentException::INVALID_FETCH_MODE, + ], + 'invalid key type' => [ + 'forInvalidKeyType', + [], + InvalidArgumentException::INVALID_KEY_TYPE, + ], + 'invalid magic property' => [ + 'forInvalidMagicProperty', + [], + InvalidArgumentException::INVALID_MAGIC_PROPERTY, + ], + 'invalid statement mode' => [ + 'forInvalidStatementMode', + [], + InvalidArgumentException::INVALID_STATEMENT_MODE, + ], + 'missing data' => [ + 'forMissingData', + [], + InvalidArgumentException::MISSING_DATA, + ], + ]; + } + + /** @param list $arguments */ + #[Test] + #[DataProvider('namedConstructorProvider')] + public function namedConstructorRendersItsTemplate(string $method, array $arguments, string $template): void + { + $exception = InvalidArgumentException::{$method}(...$arguments); + + self::assertSame(sprintf($template, ...$arguments), $exception->getMessage()); + } + + /** @param list $arguments */ + #[Test] + #[DataProvider('namedConstructorProvider')] + public function namedConstructorReturnsTheComponentExceptionType(string $method, array $arguments): void + { + self::assertInstanceOf(ExceptionInterface::class, InvalidArgumentException::{$method}(...$arguments)); + } +} diff --git a/test/unit/Adapter/Exception/InvalidQueryExceptionTest.php b/test/unit/Adapter/Exception/InvalidQueryExceptionTest.php new file mode 100644 index 00000000..0a9b1b11 --- /dev/null +++ b/test/unit/Adapter/Exception/InvalidQueryExceptionTest.php @@ -0,0 +1,59 @@ +getMessage(), + ); + } + + #[Test] + public function forFailedStatementKeepsTheCodeAndCause(): void + { + $cause = new RuntimeException(); + $exception = InvalidQueryException::forFailedStatement('HY000', 42, $cause); + + self::assertSame(42, $exception->getCode()); + self::assertSame($cause, $exception->getPrevious()); + } + + #[Test] + public function forFailedStatementRendersItsTemplate(): void + { + $exception = InvalidQueryException::forFailedStatement('HY000 - 1 - near "x"', 42, new RuntimeException()); + + self::assertSame( + sprintf(InvalidQueryException::FAILED_STATEMENT, 'HY000 - 1 - near "x"'), + $exception->getMessage(), + ); + } + + #[Test] + public function isReachableThroughTheComponentExceptionInterface(): void + { + self::assertInstanceOf(ExceptionInterface::class, InvalidQueryException::forDriverError('x')); + } +} diff --git a/test/unit/Adapter/Exception/RuntimeExceptionTest.php b/test/unit/Adapter/Exception/RuntimeExceptionTest.php new file mode 100644 index 00000000..78ece44b --- /dev/null +++ b/test/unit/Adapter/Exception/RuntimeExceptionTest.php @@ -0,0 +1,125 @@ +, string}> */ + public static function namedConstructorProvider(): array + { + return [ + 'already prepared' => [ + 'forAlreadyPrepared', + [], + RuntimeException::ALREADY_PREPARED, + ], + 'disconnected rollback' => [ + 'forDisconnectedRollback', + [], + RuntimeException::DISCONNECTED_ROLLBACK, + ], + 'driver error' => [ + 'forDriverError', + ['SQLSTATE[42S02]: Base table not found'], + RuntimeException::DRIVER_ERROR, + ], + 'forward only rewind' => [ + 'forForwardOnlyRewind', + [], + RuntimeException::FORWARD_ONLY_REWIND, + ], + 'invalid pdo param' => [ + 'forInvalidPdoParam', + ['bad-name'], + RuntimeException::INVALID_PDO_PARAM, + ], + 'missing dsn' => [ + 'forMissingDsn', + [], + RuntimeException::MISSING_DSN, + ], + 'missing pdo extension' => [ + 'forMissingPdoExtension', + [], + RuntimeException::MISSING_PDO_EXTENSION, + ], + 'missing query result' => [ + 'forMissingQueryResult', + [], + RuntimeException::MISSING_QUERY_RESULT, + ], + 'non query result' => [ + 'forNonQueryResult', + [], + RuntimeException::NON_QUERY_RESULT, + ], + 'rollback without transaction' => [ + 'forRollbackWithoutTransaction', + [], + RuntimeException::ROLLBACK_WITHOUT_TRANSACTION, + ], + 'uncomposable trait' => [ + 'forUncomposableTrait', + [DriverFeatureProviderTrait::class, DriverInterface::class], + RuntimeException::UNCOMPOSABLE_TRAIT, + ], + 'unstarted profile' => [ + 'forUnstartedProfile', + ['profilerFinish'], + RuntimeException::UNSTARTED_PROFILE, + ], + 'vulnerable platform quote' => [ + 'forVulnerablePlatformQuote', + [Sql92::class, 'Sql92::quoteValue'], + RuntimeException::VULNERABLE_PLATFORM_QUOTE, + ], + ]; + } + + /** @param list $arguments */ + #[Test] + #[DataProvider('namedConstructorProvider')] + public function namedConstructorRendersItsTemplate(string $method, array $arguments, string $template): void + { + $exception = RuntimeException::{$method}(...$arguments); + + self::assertSame(sprintf($template, ...$arguments), $exception->getMessage()); + } + + /** @param list $arguments */ + #[Test] + #[DataProvider('namedConstructorProvider')] + public function namedConstructorReturnsTheComponentExceptionType(string $method, array $arguments): void + { + self::assertInstanceOf(ExceptionInterface::class, RuntimeException::{$method}(...$arguments)); + } +} diff --git a/test/unit/Adapter/ParameterContainerTest.php b/test/unit/Adapter/ParameterContainerTest.php index 2e43c62e..4c391040 100644 --- a/test/unit/Adapter/ParameterContainerTest.php +++ b/test/unit/Adapter/ParameterContainerTest.php @@ -147,8 +147,8 @@ public function testOffsetGetErrataThrowsWhenNameDoesNotExist(): void { $container = new ParameterContainer(); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Data does not exist for this name/position'); + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage(InvalidArgumentException::MISSING_DATA); $container->offsetGetErrata('nonexistent'); } @@ -165,8 +165,8 @@ public function testOffsetGetMaxLengthThrowsWhenNameDoesNotExist(): void { $container = new ParameterContainer(); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Data does not exist for this name/position'); + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage(InvalidArgumentException::MISSING_DATA); $container->offsetGetMaxLength('nonexistent'); } @@ -294,8 +294,8 @@ public function testOffsetSetThrowsOnInvalidKeyType(): void { $container = new ParameterContainer(); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Keys must be string, integer or null'); + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage(InvalidArgumentException::INVALID_KEY_TYPE); $container->offsetSet(1.5, 'value'); } @@ -357,8 +357,8 @@ public function testOffsetUnsetErrataThrowsWhenNameDoesNotExist(): void { $container = new ParameterContainer(['foo' => 'bar']); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Data does not exist for this name/position'); + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage(InvalidArgumentException::MISSING_DATA); $container->offsetUnsetErrata('foo'); } @@ -384,8 +384,8 @@ public function testOffsetUnsetMaxLengthThrowsWhenNameDoesNotExist(): void { $container = new ParameterContainer(['foo' => 'bar']); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Data does not exist for this name/position'); + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage(InvalidArgumentException::MISSING_DATA); $container->offsetUnsetMaxLength('foo'); } diff --git a/test/unit/Adapter/Platform/Sql92Test.php b/test/unit/Adapter/Platform/Sql92Test.php index b19279bd..1b11b20e 100644 --- a/test/unit/Adapter/Platform/Sql92Test.php +++ b/test/unit/Adapter/Platform/Sql92Test.php @@ -6,7 +6,7 @@ use Override; use PhpDb\Adapter\Driver\DriverInterface; -use PhpDb\Adapter\Exception\VunerablePlatformQuoteException; +use PhpDb\Adapter\Exception\RuntimeException; use PhpDb\Adapter\Platform\AbstractPlatform; use PhpDb\Adapter\Platform\Sql92; use PhpDbTest\Adapter\Platform\TestAsset\TestPlatform; @@ -50,7 +50,7 @@ public function testAbstractPlatformQuoteValueThrowsWithoutDriver(): void { $platform = new TestPlatform(); - $this->expectException(VunerablePlatformQuoteException::class); + $this->expectException(RuntimeException::class); $platform->quoteValue('value'); } @@ -159,19 +159,19 @@ public function testQuoteValueEscapesSpecialCharacters(): void public function testQuoteValueListThrowsWithoutDriver(): void { - $this->expectException(VunerablePlatformQuoteException::class); + $this->expectException(RuntimeException::class); self::assertEquals("'Foo O\\'Bar'", $this->platform->quoteValueList("Foo O'Bar")); } public function testQuoteValueRaisesNoticeWithoutPlatformSupport(): void { - $this->expectException(VunerablePlatformQuoteException::class); + $this->expectException(RuntimeException::class); $this->platform->quoteValue('value'); } public function testQuoteValueThrowsWithoutDriver(): void { - $this->expectException(VunerablePlatformQuoteException::class); + $this->expectException(RuntimeException::class); self::assertEquals("'value'", @$this->platform->quoteValue('value')); self::assertEquals("'Foo O\\'Bar'", @$this->platform->quoteValue("Foo O'Bar")); self::assertEquals( diff --git a/test/unit/Adapter/Profiler/ProfilerTest.php b/test/unit/Adapter/Profiler/ProfilerTest.php index 38724686..ff1d6117 100644 --- a/test/unit/Adapter/Profiler/ProfilerTest.php +++ b/test/unit/Adapter/Profiler/ProfilerTest.php @@ -13,6 +13,8 @@ use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; +use function sprintf; + #[CoversMethod(Profiler::class, 'profilerStart')] #[CoversMethod(Profiler::class, 'profilerFinish')] #[CoversMethod(Profiler::class, 'getLastProfile')] @@ -51,8 +53,8 @@ public function testProfilerFinishThrowsWithoutStart(): void self::assertSame($this->profiler, $ret); $profiler = new Profiler(); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('A profile must be started before profilerFinish can be called'); + self::expectException(RuntimeException::class); + self::expectExceptionMessage(sprintf(RuntimeException::UNSTARTED_PROFILE, 'profilerFinish')); $profiler->profilerFinish(); } diff --git a/test/unit/Exception/ExceptionHierarchyTest.php b/test/unit/Exception/ExceptionHierarchyTest.php index 6593f630..1a6fe49e 100644 --- a/test/unit/Exception/ExceptionHierarchyTest.php +++ b/test/unit/Exception/ExceptionHierarchyTest.php @@ -60,7 +60,6 @@ final class ExceptionHierarchyTest extends TestCase AdapterException\InvalidQueryException::class => UnexpectedValueException::class, AdapterException\RuntimeException::class => RuntimeException::class, AdapterException\UnexpectedValueException::class => UnexpectedValueException::class, - AdapterException\VunerablePlatformQuoteException::class => RuntimeException::class, DbException\ContainerException::class => RuntimeException::class, DbException\ErrorException::class => Exception::class, DbException\InvalidArgumentException::class => InvalidArgumentException::class, @@ -76,12 +75,6 @@ final class ExceptionHierarchyTest extends TestCase TableGatewayException\RuntimeException::class => RuntimeException::class, ]; - /** - * Exempt from the naming guard: its factory is named for its arguments rather than the - * fault, and holds its message inline. - */ - private const string UNCONVERTED_CLASS = AdapterException\VunerablePlatformQuoteException::class; - /** @return array */ public static function exceptionClassProvider(): array { @@ -100,10 +93,6 @@ public static function namedConstructorProvider(): array { $cases = []; foreach (array_keys(self::EXCEPTION_CLASSES) as $class) { - if (self::UNCONVERTED_CLASS === $class) { - continue; - } - foreach (self::namedConstructors($class) as $method) { $cases["{$class}::{$method}()"] = [ $class, diff --git a/test/unit/Sql/Predicate/PredicateTest.php b/test/unit/Sql/Predicate/PredicateTest.php index 80f31cbd..aad7bf52 100644 --- a/test/unit/Sql/Predicate/PredicateTest.php +++ b/test/unit/Sql/Predicate/PredicateTest.php @@ -5,7 +5,7 @@ namespace PhpDbTest\Sql\Predicate; use ErrorException; -use PhpDb\Adapter\Exception\VunerablePlatformQuoteException; +use PhpDb\Adapter\Exception\RuntimeException as AdapterRuntimeException; use PhpDb\Adapter\Platform\Sql92; use PhpDb\Sql\Argument; use PhpDb\Sql\Exception\RuntimeException; @@ -469,7 +469,7 @@ private function makeSqlString(Predicate $where): string // } finally { // ErrorHandler::stop(); // } - $this->expectException(VunerablePlatformQuoteException::class); + $this->expectException(AdapterRuntimeException::class); return $select->getSqlString(new Sql92()); } }