From 88b5bf5198d93641a35775d17649a6f83e71aa43 Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Mon, 28 Sep 2026 14:51:14 +1000 Subject: [PATCH] Add the Metadata exception namespace Metadata was the only component with no exception namespace, so its failures were invisible to catch (PhpDb\Exception\ExceptionInterface). - Add Metadata\Exception\InvalidArgumentException and RuntimeException, matching the other components. The namespace declares no ExceptionInterface of its own: both classes extend their PhpDb\Exception counterparts, where the single marker is declared, so they implement it by inheritance - Register both concrete classes in ExceptionHierarchyTest's EXCEPTION_CLASSES map, so they come under the structural guards - Replace the seven bare \Exception throws in AbstractSource with named constructors and repoint the eleven @throws annotations at them - Converge the two "table does not exist" messages on 'Table "%s" does not exist', matching the Trigger and View wording already in the class - Derive the AbstractSource message assertions from the templates rather than restating them, and call the factories in the test body so they record coverage, which a data provider does not Refs #189 Signed-off-by: Simon Mundy --- analyzer-baseline.toml | 18 +++++ .../Exception/InvalidArgumentException.php | 9 +++ src/Metadata/Exception/RuntimeException.php | 54 +++++++++++++ src/Metadata/Source/AbstractSource.php | 40 +++++----- .../unit/Exception/ExceptionHierarchyTest.php | 3 + .../Exception/RuntimeExceptionTest.php | 80 +++++++++++++++++++ .../Metadata/Source/AbstractSourceTest.php | 38 +++++---- 7 files changed, 204 insertions(+), 38 deletions(-) create mode 100644 src/Metadata/Exception/InvalidArgumentException.php create mode 100644 src/Metadata/Exception/RuntimeException.php create mode 100644 test/unit/Metadata/Exception/RuntimeExceptionTest.php diff --git a/analyzer-baseline.toml b/analyzer-baseline.toml index 93a74a29..7c072fde 100644 --- a/analyzer-baseline.toml +++ b/analyzer-baseline.toml @@ -1464,6 +1464,24 @@ code = "missing-api-or-internal" message = 'Interface `PhpDb\Exception\ExceptionInterface` is missing an `@api` or `@internal` annotation.' count = 1 +[[issues]] +file = "src/Metadata/Exception/ExceptionInterface.php" +code = "missing-api-or-internal" +message = 'Interface `PhpDb\Metadata\Exception\ExceptionInterface` is missing an `@api` or `@internal` annotation.' +count = 1 + +[[issues]] +file = "src/Metadata/Exception/InvalidArgumentException.php" +code = "class-must-be-final" +message = 'Class `PhpDb\Metadata\Exception\InvalidArgumentException` should be declared `final`.' +count = 1 + +[[issues]] +file = "src/Metadata/Exception/RuntimeException.php" +code = "class-must-be-final" +message = 'Class `PhpDb\Metadata\Exception\RuntimeException` should be declared `final`.' +count = 1 + [[issues]] file = "src/Metadata/Object/ConstraintKeyObject.php" code = "missing-constant-type" diff --git a/src/Metadata/Exception/InvalidArgumentException.php b/src/Metadata/Exception/InvalidArgumentException.php new file mode 100644 index 00000000..6fdac4db --- /dev/null +++ b/src/Metadata/Exception/InvalidArgumentException.php @@ -0,0 +1,9 @@ +data['columns'][$schema][$table][$columnName] ?? null; if (null === $info) { - throw new Exception('A column by that name was not found.'); + throw Exception\RuntimeException::forUnknownColumn(); } $column = new ColumnObject($columnName, $table, $schema); @@ -157,7 +157,7 @@ public function getColumn(string $columnName, string $table, ?string $schema = n /** * @return list * - * @throws Exception If the table does not exist. + * @throws Exception\RuntimeException If the table does not exist. */ #[Override] public function getColumnNames(string $table, ?string $schema = null): array @@ -170,7 +170,7 @@ public function getColumnNames(string $table, ?string $schema = null): array $columns = $this->data['columns'][$schema][$table] ?? null; if (null === $columns) { - throw new Exception("\"{$table}\" does not exist"); + throw Exception\RuntimeException::forUnknownTable($table); } return array_keys($columns); @@ -181,7 +181,7 @@ public function getColumnNames(string $table, ?string $schema = null): array * * @return list * - * @throws Exception If the table does not exist. + * @throws Exception\RuntimeException If the table does not exist. */ #[Override] public function getColumns(string $table, ?string $schema = null): array @@ -201,7 +201,7 @@ public function getColumns(string $table, ?string $schema = null): array } /** - * @throws Exception If the constraint does not exist. + * @throws Exception\RuntimeException If the constraint does not exist. */ #[Override] public function getConstraint( @@ -217,7 +217,7 @@ public function getConstraint( $info = $this->data['constraints'][$schema][$table][$constraintName] ?? null; if (null === $info) { - throw new Exception('Cannot find a constraint by that name in this table'); + throw Exception\RuntimeException::forUnknownConstraint(); } $constraint = new ConstraintObject($constraintName, $table, $schema); @@ -331,7 +331,7 @@ public function getConstraintKeys(string $constraint, string $table, ?string $sc * * @return list * - * @throws Exception If a constraint cannot be loaded. + * @throws Exception\RuntimeException If a constraint cannot be loaded. */ #[Override] public function getConstraints(string $table, ?string $schema = null): array @@ -364,7 +364,7 @@ public function getSchemas(): array } /** - * @throws Exception If the table does not exist or is of an unsupported type. + * @throws Exception\RuntimeException If the table does not exist or is of an unsupported type. */ #[Override] public function getTable(string $tableName, ?string $schema = null): TableObject|ViewObject @@ -377,7 +377,7 @@ public function getTable(string $tableName, ?string $schema = null): TableObject $data = $this->data['table_names'][$schema][$tableName] ?? null; if (null === $data) { - throw new Exception("Table \"{$tableName}\" does not exist"); + throw Exception\RuntimeException::forUnknownTable($tableName); } switch ($data['table_type']) { @@ -391,9 +391,7 @@ public function getTable(string $tableName, ?string $schema = null): TableObject $table->setIsUpdatable($data['is_updatable'] ?? null); break; default: - throw new Exception( - "Table \"{$tableName}\" is of an unsupported type \"{$data['table_type']}\"", - ); + throw Exception\RuntimeException::forUnsupportedTableType($tableName, $data['table_type']); } $table->setColumns($this->getColumns($tableName, $schema)); @@ -440,7 +438,7 @@ public function getTableNames(?string $schema = null, bool $includeViews = false * * @return list * - * @throws Exception If a table cannot be loaded. + * @throws Exception\RuntimeException If a table cannot be loaded. */ #[Override] public function getTables(?string $schema = null, bool $includeViews = false): array @@ -458,7 +456,7 @@ public function getTables(?string $schema = null, bool $includeViews = false): a } /** - * @throws Exception If the trigger does not exist. + * @throws Exception\RuntimeException If the trigger does not exist. */ #[Override] public function getTrigger(string $triggerName, ?string $schema = null): TriggerObject @@ -471,7 +469,7 @@ public function getTrigger(string $triggerName, ?string $schema = null): Trigger $info = $this->data['triggers'][$schema][$triggerName] ?? null; if (null === $info) { - throw new Exception("Trigger \"{$triggerName}\" does not exist"); + throw Exception\RuntimeException::forUnknownTrigger($triggerName); } $trigger = new TriggerObject(); @@ -517,7 +515,7 @@ public function getTriggerNames(?string $schema = null): array * * @return list * - * @throws Exception If a trigger cannot be loaded. + * @throws Exception\RuntimeException If a trigger cannot be loaded. */ #[Override] public function getTriggers(?string $schema = null): array @@ -535,7 +533,7 @@ public function getTriggers(?string $schema = null): array } /** - * @throws Exception If the view does not exist. + * @throws Exception\RuntimeException If the view does not exist. */ #[Override] public function getView(string $viewName, ?string $schema = null): ViewObject|TableObject @@ -551,7 +549,7 @@ public function getView(string $viewName, ?string $schema = null): ViewObject|Ta return $this->getTable($viewName, $schema); } - throw new Exception("View \"{$viewName}\" does not exist"); + throw Exception\RuntimeException::forUnknownView($viewName); } /** @@ -585,7 +583,7 @@ public function getViewNames(?string $schema = null): array * * @return list * - * @throws Exception If a view cannot be loaded. + * @throws Exception\RuntimeException If a view cannot be loaded. */ #[Override] public function getViews(?string $schema = null): array diff --git a/test/unit/Exception/ExceptionHierarchyTest.php b/test/unit/Exception/ExceptionHierarchyTest.php index 6593f630..b096016a 100644 --- a/test/unit/Exception/ExceptionHierarchyTest.php +++ b/test/unit/Exception/ExceptionHierarchyTest.php @@ -8,6 +8,7 @@ use InvalidArgumentException; use PhpDb\Adapter\Exception as AdapterException; use PhpDb\Exception as DbException; +use PhpDb\Metadata\Exception as MetadataException; use PhpDb\ResultSet\Exception as ResultSetException; use PhpDb\RowGateway\Exception as RowGatewayException; use PhpDb\Sql\Exception as SqlException; @@ -66,6 +67,8 @@ final class ExceptionHierarchyTest extends TestCase DbException\InvalidArgumentException::class => InvalidArgumentException::class, DbException\RuntimeException::class => RuntimeException::class, DbException\UnexpectedValueException::class => UnexpectedValueException::class, + MetadataException\InvalidArgumentException::class => InvalidArgumentException::class, + MetadataException\RuntimeException::class => RuntimeException::class, ResultSetException\InvalidArgumentException::class => InvalidArgumentException::class, ResultSetException\RuntimeException::class => RuntimeException::class, RowGatewayException\InvalidArgumentException::class => InvalidArgumentException::class, diff --git a/test/unit/Metadata/Exception/RuntimeExceptionTest.php b/test/unit/Metadata/Exception/RuntimeExceptionTest.php new file mode 100644 index 00000000..d0b42f4f --- /dev/null +++ b/test/unit/Metadata/Exception/RuntimeExceptionTest.php @@ -0,0 +1,80 @@ +, string}> */ + public static function namedConstructorProvider(): array + { + return [ + 'unknown column' => [ + 'forUnknownColumn', + [], + RuntimeException::UNKNOWN_COLUMN, + ], + 'unknown constraint' => [ + 'forUnknownConstraint', + [], + RuntimeException::UNKNOWN_CONSTRAINT, + ], + 'unknown table' => [ + 'forUnknownTable', + ['users'], + RuntimeException::UNKNOWN_TABLE, + ], + 'unknown trigger' => [ + 'forUnknownTrigger', + ['after_insert'], + RuntimeException::UNKNOWN_TRIGGER, + ], + 'unknown view' => [ + 'forUnknownView', + ['active_users'], + RuntimeException::UNKNOWN_VIEW, + ], + 'unsupported table type' => [ + 'forUnsupportedTableType', + ['users', 'SYSTEM VIEW'], + RuntimeException::UNSUPPORTED_TABLE_TYPE, + ], + ]; + } + + /** @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/Metadata/Source/AbstractSourceTest.php b/test/unit/Metadata/Source/AbstractSourceTest.php index 0dc82b30..646e6faf 100644 --- a/test/unit/Metadata/Source/AbstractSourceTest.php +++ b/test/unit/Metadata/Source/AbstractSourceTest.php @@ -4,10 +4,10 @@ namespace PhpDbTest\Metadata\Source; -use Exception; use Override; use PhpDb\Adapter\AdapterInterface; use PhpDb\Adapter\SchemaAwareInterface; +use PhpDb\Metadata\Exception\RuntimeException; use PhpDb\Metadata\Object\ColumnObject; use PhpDb\Metadata\Object\ConstraintKeyObject; use PhpDb\Metadata\Object\ConstraintObject; @@ -27,6 +27,8 @@ use ReflectionMethod; use ReflectionProperty; +use function sprintf; + #[IgnoreDeprecations] #[RequiresPhp('<= 8.6')] #[CoversMethod(AbstractSource::class, '__construct')] @@ -181,8 +183,8 @@ public function getColumnNamesThrowsWhenLoadColumnDataDoesNotPopulate(): void $source = new IncompleteSource($adapter); - $this->expectException(Exception::class); - $this->expectExceptionMessage('"nonexistent" does not exist'); + self::expectException(RuntimeException::class); + self::expectExceptionMessage(sprintf(RuntimeException::UNKNOWN_TABLE, 'nonexistent')); $source->getColumnNames('nonexistent', 'public'); } @@ -296,8 +298,8 @@ public function getColumnThrowsExceptionForNonExistentColumn(): void ], ]); - $this->expectException(Exception::class); - $this->expectExceptionMessage('A column by that name was not found.'); + self::expectException(RuntimeException::class); + self::expectExceptionMessage(RuntimeException::UNKNOWN_COLUMN); $this->abstractSourceMock->getColumn('non_existent', 'users', 'public'); @@ -614,8 +616,8 @@ public function getConstraintThrowsExceptionForNonExistent(): void ], ]); - $this->expectException(Exception::class); - $this->expectExceptionMessage('Cannot find a constraint by that name in this table'); + self::expectException(RuntimeException::class); + self::expectExceptionMessage(RuntimeException::UNKNOWN_CONSTRAINT); $this->abstractSourceMock->getConstraint('non_existent', 'users', 'public'); @@ -937,8 +939,8 @@ public function getTableThrowsExceptionForNonExistentTable(): void ], ]); - $this->expectException(Exception::class); - $this->expectExceptionMessage('Table "non_existent" does not exist'); + self::expectException(RuntimeException::class); + self::expectExceptionMessage(sprintf(RuntimeException::UNKNOWN_TABLE, 'non_existent')); $this->abstractSourceMock->getTable('non_existent', 'public'); @@ -959,8 +961,10 @@ public function getTableThrowsExceptionForUnsupportedTableType(): void ], ]); - $this->expectException(Exception::class); - $this->expectExceptionMessage('Table "special_table" is of an unsupported type "UNSUPPORTED_TYPE"'); + self::expectException(RuntimeException::class); + self::expectExceptionMessage( + sprintf(RuntimeException::UNSUPPORTED_TABLE_TYPE, 'special_table', 'UNSUPPORTED_TYPE'), + ); $this->abstractSourceMock->getTable('special_table', 'public'); @@ -1181,8 +1185,8 @@ public function getTriggerThrowsExceptionForNonExistent(): void ], ]); - $this->expectException(Exception::class); - $this->expectExceptionMessage('Trigger "non_existent" does not exist'); + self::expectException(RuntimeException::class); + self::expectExceptionMessage(sprintf(RuntimeException::UNKNOWN_TRIGGER, 'non_existent')); $this->abstractSourceMock->getTrigger('non_existent', 'public'); @@ -1387,8 +1391,8 @@ public function getViewThrowsExceptionForNonExistentView(): void ], ]); - $this->expectException(Exception::class); - $this->expectExceptionMessage('View "non_existent_view" does not exist'); + self::expectException(RuntimeException::class); + self::expectExceptionMessage(sprintf(RuntimeException::UNKNOWN_VIEW, 'non_existent_view')); $this->abstractSourceMock->getView('non_existent_view', 'public'); @@ -1409,8 +1413,8 @@ public function getViewThrowsExceptionForTable(): void ], ]); - $this->expectException(Exception::class); - $this->expectExceptionMessage('View "users" does not exist'); + self::expectException(RuntimeException::class); + self::expectExceptionMessage(sprintf(RuntimeException::UNKNOWN_VIEW, 'users')); $this->abstractSourceMock->getView('users', 'public');