From 5c6e701f42e8a97c6e3a8132a13fe284f68669a7 Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Mon, 24 Aug 2026 10:20:11 +1000 Subject: [PATCH 1/2] Annotate Feature classes and types - Add @api to the Feature interfaces, both AbstractFeature bases, EventFeatureEventsInterface, EventFeature, GlobalAdapterFeature, MetadataFeature, SequenceFeature and TableGateway\Feature\FeatureSet - Mark RowGatewayFeature, MasterSlaveFeature and TableGatewayEvent final - Type the EventFeatureEventsInterface constants and both APPLY_HALT - Add array shape docblocks to the FeatureSets, AbstractFeature::$sharedData, RowGatewayFeature::$constructorArguments and TableGatewayEvent::$params - Declare TableGatewayEvent as an EventInterface implementation - Correct AbstractTableGateway's @property $table to the declared union and narrow TableGateway::__construct's $features docblock - Register the AbstractFeature gateway setters as class initializers - Drop the Feature entries from the analysis baselines --- analyzer-baseline.toml | 632 +----------------- lint-baseline.toml | 102 +-- mago.toml | 2 + src/Feature/FeatureInterface.php | 3 + src/RowGateway/Feature/AbstractFeature.php | 4 + src/RowGateway/Feature/FeatureInterface.php | 3 + src/RowGateway/Feature/FeatureSet.php | 31 +- src/TableGateway/AbstractTableGateway.php | 2 +- src/TableGateway/Feature/AbstractFeature.php | 4 + src/TableGateway/Feature/EventFeature.php | 3 + .../EventFeature/TableGatewayEvent.php | 15 +- .../Feature/EventFeatureEventsInterface.php | 22 +- src/TableGateway/Feature/FeatureInterface.php | 3 + src/TableGateway/Feature/FeatureSet.php | 30 +- .../Feature/GlobalAdapterFeature.php | 3 + .../Feature/MasterSlaveFeature.php | 2 +- src/TableGateway/Feature/MetadataFeature.php | 3 + .../Feature/RowGatewayFeature.php | 3 +- src/TableGateway/Feature/SequenceFeature.php | 10 + src/TableGateway/TableGateway.php | 2 + 20 files changed, 168 insertions(+), 711 deletions(-) diff --git a/analyzer-baseline.toml b/analyzer-baseline.toml index 3e000633..423ba43d 100644 --- a/analyzer-baseline.toml +++ b/analyzer-baseline.toml @@ -1482,12 +1482,6 @@ code = "missing-api-or-internal" message = 'Interface `PhpDb\Exception\ExceptionInterface` is missing an `@api` or `@internal` annotation.' count = 1 -[[issues]] -file = "src/Feature/FeatureInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Feature\FeatureInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - [[issues]] file = "src/Metadata/Object/ConstraintKeyObject.php" code = "missing-constant-type" @@ -2022,102 +2016,6 @@ code = "class-must-be-final" message = 'Class `PhpDb\RowGateway\Exception\RuntimeException` should be declared `final`.' count = 1 -[[issues]] -file = "src/RowGateway/Feature/AbstractFeature.php" -code = "imprecise-type" -message = "Type `array` in property `$sharedData` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/RowGateway/Feature/AbstractFeature.php" -code = "missing-api-or-internal" -message = 'Abstract class `PhpDb\RowGateway\Feature\AbstractFeature` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/RowGateway/Feature/FeatureInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\RowGateway\Feature\FeatureInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/RowGateway/Feature/FeatureSet.php" -code = "imprecise-type" -message = "Type `array` in parameter `$args` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/RowGateway/Feature/FeatureSet.php" -code = "imprecise-type" -message = "Type `array` in parameter `$arguments` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/RowGateway/Feature/FeatureSet.php" -code = "imprecise-type" -message = "Type `array` in parameter `$features` is imprecise, equivalent to `array`." -count = 2 - -[[issues]] -file = "src/RowGateway/Feature/FeatureSet.php" -code = "imprecise-type" -message = "Type `array` in property `$magicSpecifications` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/RowGateway/Feature/FeatureSet.php" -code = "missing-constant-type" -message = "Class constant `APPLY_HALT` is missing a type hint." -count = 1 - -[[issues]] -file = "src/RowGateway/Feature/FeatureSet.php" -code = "mixed-argument" -message = 'Invalid argument type for argument #1 of `PhpDb\RowGateway\Feature\FeatureSet::addFeature`: expected `PhpDb\RowGateway\Feature\FeatureInterface`, but found `mixed`.' -count = 1 - -[[issues]] -file = "src/RowGateway/Feature/FeatureSet.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 2 - -[[issues]] -file = "src/RowGateway/Feature/FeatureSet.php" -code = "string-member-selector" -message = "This member selector uses a non-literal string type (`string`); its specific value cannot be statically determined." -count = 1 - -[[issues]] -file = "src/RowGateway/Feature/FeatureSet.php" -code = "unused-parameter" -message = "Parameter `$arguments` is never used." -count = 1 - -[[issues]] -file = "src/RowGateway/Feature/FeatureSet.php" -code = "unused-parameter" -message = "Parameter `$method` is never used." -count = 2 - -[[issues]] -file = "src/RowGateway/Feature/FeatureSet.php" -code = "unused-parameter" -message = "Parameter `$property` is never used." -count = 4 - -[[issues]] -file = "src/RowGateway/Feature/FeatureSet.php" -code = "unused-parameter" -message = "Parameter `$value` is never used." -count = 1 - -[[issues]] -file = "src/RowGateway/Feature/FeatureSet.php" -code = "unused-property" -message = "Property `$magicSpecifications` is never used." -count = 1 - [[issues]] file = "src/RowGateway/RowGateway.php" code = "class-must-be-final" @@ -5344,13 +5242,13 @@ count = 1 file = "src/TableGateway/AbstractTableGateway.php" code = "invalid-array-access" message = 'Cannot access array index on object `PhpDb\Sql\TableIdentifier` that does not implement `ArrayAccess`.' -count = 2 +count = 3 [[issues]] file = "src/TableGateway/AbstractTableGateway.php" code = "invalid-clone" message = "Invalid `clone` operation on non-cloneable type `never`." -count = 1 +count = 2 [[issues]] file = "src/TableGateway/AbstractTableGateway.php" @@ -5554,7 +5452,7 @@ count = 1 file = "src/TableGateway/AbstractTableGateway.php" code = "possibly-undefined-string-array-index" message = "Possibly undefined array key `string('table')` accessed on `array`." -count = 2 +count = 3 [[issues]] file = "src/TableGateway/AbstractTableGateway.php" @@ -5572,7 +5470,7 @@ count = 2 file = "src/TableGateway/Exception/ExceptionInterface.php" code = "missing-api-or-internal" message = 'Interface `PhpDb\TableGateway\Exception\ExceptionInterface` is missing an `@api` or `@internal` annotation.' -count = 1 +count = 2 [[issues]] file = "src/TableGateway/Exception/InvalidArgumentException.php" @@ -5586,516 +5484,6 @@ code = "class-must-be-final" message = 'Class `PhpDb\TableGateway\Exception\RuntimeException` should be declared `final`.' count = 1 -[[issues]] -file = "src/TableGateway/Feature/AbstractFeature.php" -code = "imprecise-type" -message = "Type `array` in property `$sharedData` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/AbstractFeature.php" -code = "missing-api-or-internal" -message = 'Abstract class `PhpDb\TableGateway\Feature\AbstractFeature` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/AbstractFeature.php" -code = "uninitialized-property" -message = 'Property `$tableGateway` is not initialized in the constructor of class `PhpDb\TableGateway\Feature\EventFeature`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/AbstractFeature.php" -code = "uninitialized-property" -message = 'Property `$tableGateway` is not initialized in the constructor of class `PhpDb\TableGateway\Feature\MasterSlaveFeature`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/AbstractFeature.php" -code = "uninitialized-property" -message = 'Property `$tableGateway` is not initialized in the constructor of class `PhpDb\TableGateway\Feature\MetadataFeature`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/AbstractFeature.php" -code = "uninitialized-property" -message = 'Property `$tableGateway` is not initialized in the constructor of class `PhpDb\TableGateway\Feature\RowGatewayFeature`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/AbstractFeature.php" -code = "uninitialized-property" -message = 'Property `$tableGateway` is not initialized in the constructor of class `PhpDb\TableGateway\Feature\SequenceFeature`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeature.php" -code = "class-must-be-final" -message = 'Class `PhpDb\TableGateway\Feature\EventFeature` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeature.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\TableGateway\Feature\EventFeature::getEvent`: expected `PhpDb\TableGateway\Feature\EventFeature\TableGatewayEvent`, but found `PhpDb\TableGateway\Feature\EventFeature\TableGatewayEvent|null`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeature.php" -code = "nullable-return-statement" -message = 'Function `PhpDb\TableGateway\Feature\EventFeature::getEvent` is declared to return `PhpDb\TableGateway\Feature\EventFeature\TableGatewayEvent` but possibly returns a nullable value (inferred as `PhpDb\TableGateway\Feature\EventFeature\TableGatewayEvent|null`).' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeature.php" -code = "possible-method-access-on-null" -message = "Attempting to call a method on `null`." -count = 19 - -[[issues]] -file = "src/TableGateway/Feature/EventFeature.php" -code = "possibly-null-argument" -message = 'Argument #1 of method `Laminas\EventManager\EventManagerInterface::triggerEvent` is possibly `null`, but parameter type `Laminas\EventManager\EventInterface|array|object>` does not accept it.' -count = 10 - -[[issues]] -file = "src/TableGateway/Feature/EventFeature/TableGatewayEvent.php" -code = "class-must-be-final" -message = 'Class `PhpDb\TableGateway\Feature\EventFeature\TableGatewayEvent` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeature/TableGatewayEvent.php" -code = "imprecise-type" -message = "Type `array` in property `$params` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeature/TableGatewayEvent.php" -code = "missing-template-parameter" -message = 'Too few template arguments for `Laminas\EventManager\EventInterface`: expected at least 2, but found 0.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeature/TableGatewayEvent.php" -code = "possibly-invalid-array-access" -message = "Cannot perform array access on value of type `object`." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeature/TableGatewayEvent.php" -code = "property-type-coercion" -message = 'A value of a less specific type `null|object|string` is being assigned to property `$target` (PhpDb\TableGateway\AbstractTableGateway|null).' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeature/TableGatewayEvent.php" -code = "unused-template-parameter" -message = 'Template parameter `NewTParams` is never used in method `PhpDb\TableGateway\Feature\EventFeature\TableGatewayEvent::setParams`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeature/TableGatewayEvent.php" -code = "unused-template-parameter" -message = 'Template parameter `NewTTarget` is never used in method `PhpDb\TableGateway\Feature\EventFeature\TableGatewayEvent::setTarget`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeatureEventsInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\TableGateway\Feature\EventFeatureEventsInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeatureEventsInterface.php" -code = "missing-constant-type" -message = "Class constant `EVENT_POST_DELETE` is missing a type hint." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeatureEventsInterface.php" -code = "missing-constant-type" -message = "Class constant `EVENT_POST_INITIALIZE` is missing a type hint." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeatureEventsInterface.php" -code = "missing-constant-type" -message = "Class constant `EVENT_POST_INSERT` is missing a type hint." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeatureEventsInterface.php" -code = "missing-constant-type" -message = "Class constant `EVENT_POST_SELECT` is missing a type hint." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeatureEventsInterface.php" -code = "missing-constant-type" -message = "Class constant `EVENT_POST_UPDATE` is missing a type hint." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeatureEventsInterface.php" -code = "missing-constant-type" -message = "Class constant `EVENT_PRE_DELETE` is missing a type hint." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeatureEventsInterface.php" -code = "missing-constant-type" -message = "Class constant `EVENT_PRE_INITIALIZE` is missing a type hint." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeatureEventsInterface.php" -code = "missing-constant-type" -message = "Class constant `EVENT_PRE_INSERT` is missing a type hint." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeatureEventsInterface.php" -code = "missing-constant-type" -message = "Class constant `EVENT_PRE_SELECT` is missing a type hint." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeatureEventsInterface.php" -code = "missing-constant-type" -message = "Class constant `EVENT_PRE_UPDATE` is missing a type hint." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/FeatureInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\TableGateway\Feature\FeatureInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/FeatureSet.php" -code = "class-must-be-final" -message = 'Class `PhpDb\TableGateway\Feature\FeatureSet` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/FeatureSet.php" -code = "imprecise-type" -message = "Type `array` in parameter `$args` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/FeatureSet.php" -code = "imprecise-type" -message = "Type `array` in parameter `$arguments` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/FeatureSet.php" -code = "imprecise-type" -message = "Type `array` in parameter `$features` is imprecise, equivalent to `array`." -count = 2 - -[[issues]] -file = "src/TableGateway/Feature/FeatureSet.php" -code = "imprecise-type" -message = "Type `array` in property `$magicSpecifications` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/FeatureSet.php" -code = "missing-constant-type" -message = "Class constant `APPLY_HALT` is missing a type hint." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/FeatureSet.php" -code = "mixed-argument" -message = 'Invalid argument type for argument #1 of `PhpDb\TableGateway\Feature\FeatureSet::addFeature`: expected `PhpDb\TableGateway\Feature\FeatureInterface`, but found `mixed`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/FeatureSet.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 2 - -[[issues]] -file = "src/TableGateway/Feature/FeatureSet.php" -code = "string-member-selector" -message = "This member selector uses a non-literal string type (`string`); its specific value cannot be statically determined." -count = 2 - -[[issues]] -file = "src/TableGateway/Feature/FeatureSet.php" -code = "unused-parameter" -message = "Parameter `$property` is never used." -count = 4 - -[[issues]] -file = "src/TableGateway/Feature/FeatureSet.php" -code = "unused-parameter" -message = "Parameter `$value` is never used." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/GlobalAdapterFeature.php" -code = "class-must-be-final" -message = 'Class `PhpDb\TableGateway\Feature\GlobalAdapterFeature` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/GlobalAdapterFeature.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\TableGateway\Feature\GlobalAdapterFeature::getStaticAdapter`: expected `PhpDb\Adapter\AdapterInterface`, but found `PhpDb\Adapter\AdapterInterface|null`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/GlobalAdapterFeature.php" -code = "missing-constructor" -message = 'Class `PhpDb\TableGateway\Feature\GlobalAdapterFeature` has typed properties without default values but no constructor to initialize them.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/GlobalAdapterFeature.php" -code = "nullable-return-statement" -message = 'Function `PhpDb\TableGateway\Feature\GlobalAdapterFeature::getStaticAdapter` is declared to return `PhpDb\Adapter\AdapterInterface` but possibly returns a nullable value (inferred as `PhpDb\Adapter\AdapterInterface|null`).' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/GlobalAdapterFeature.php" -code = "possibly-undefined-string-array-index" -message = '''Possibly undefined array key `class-string('PhpDb\TableGateway\Feature\GlobalAdapterFeature')` accessed on `array`.''' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MasterSlaveFeature.php" -code = "class-must-be-final" -message = 'Class `PhpDb\TableGateway\Feature\MasterSlaveFeature` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MasterSlaveFeature.php" -code = "invalid-property-assignment-value" -message = 'Invalid type for property `$masterSql`: expected `PhpDb\Sql\Sql`, but got `PhpDb\Sql\Sql|null`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MasterSlaveFeature.php" -code = "mixed-argument" -message = 'Invalid argument type for argument #2 of `PhpDb\Sql\Sql::__construct`: expected `PhpDb\Sql\TableIdentifier|array|null|string`, but found `mixed`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MasterSlaveFeature.php" -code = "possible-method-access-on-null" -message = "Attempting to call a method on `null`." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MasterSlaveFeature.php" -code = "uninitialized-property" -message = 'Property `$masterSql` is not initialized in the constructor of class `PhpDb\TableGateway\Feature\MasterSlaveFeature`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MetadataFeature.php" -code = "class-must-be-final" -message = 'Class `PhpDb\TableGateway\Feature\MetadataFeature` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MetadataFeature.php" -code = "impossible-condition" -message = "Redundant ternary operator: condition is always falsy." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MetadataFeature.php" -code = "impossible-condition" -message = "This condition (type `false`) will always evaluate to false." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MetadataFeature.php" -code = "impossible-type-comparison" -message = "Impossible type assertion: `$t->table` of type `string` can never be `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MetadataFeature.php" -code = "mixed-argument" -message = 'Invalid argument type for argument #1 of `PhpDb\Metadata\MetadataInterface::getColumnNames`: expected `string`, but found `mixed`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MetadataFeature.php" -code = "mixed-argument" -message = 'Invalid argument type for argument #1 of `PhpDb\Metadata\MetadataInterface::getConstraints`: expected `string`, but found `mixed`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MetadataFeature.php" -code = "mixed-argument" -message = 'Invalid argument type for argument #1 of `PhpDb\Metadata\MetadataInterface::getTable`: expected `string`, but found `mixed`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MetadataFeature.php" -code = "mixed-argument" -message = 'Invalid argument type for argument #2 of `PhpDb\Metadata\MetadataInterface::getColumnNames`: expected `null|string`, but found `mixed`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MetadataFeature.php" -code = "mixed-argument" -message = 'Invalid argument type for argument #2 of `PhpDb\Metadata\MetadataInterface::getConstraints`: expected `null|string`, but found `mixed`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MetadataFeature.php" -code = "mixed-argument" -message = 'Invalid argument type for argument #2 of `PhpDb\Metadata\MetadataInterface::getTable`: expected `null|string`, but found `mixed`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MetadataFeature.php" -code = "mixed-array-assignment" -message = "Unsafe array assignment on type `mixed`." -count = 2 - -[[issues]] -file = "src/TableGateway/Feature/MetadataFeature.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 2 - -[[issues]] -file = "src/TableGateway/Feature/MetadataFeature.php" -code = "no-value" -message = "Argument #1 passed to function `current` has type `never`, meaning it cannot produce a value." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/MetadataFeature.php" -code = "possibly-undefined-int-array-index" -message = "Possibly undefined array key `int(0)` accessed on `non-empty-array`." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/RowGatewayFeature.php" -code = "class-must-be-final" -message = 'Class `PhpDb\TableGateway\Feature\RowGatewayFeature` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/RowGatewayFeature.php" -code = "deprecated-method" -message = 'Call to deprecated method: `PhpDb\ResultSet\ResultSet::setArrayObjectPrototype`.' -count = 3 - -[[issues]] -file = "src/TableGateway/Feature/RowGatewayFeature.php" -code = "imprecise-type" -message = "Type `array` in property `$constructorArguments` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/RowGatewayFeature.php" -code = "mixed-argument" -message = 'Invalid argument type for argument #1 of `PhpDb\RowGateway\RowGateway::__construct`: expected `array|null|string`, but found `mixed`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/RowGatewayFeature.php" -code = "mixed-array-access" -message = "Unsafe array access on type `nonnull`." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/RowGatewayFeature.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 2 - -[[issues]] -file = "src/TableGateway/Feature/RowGatewayFeature.php" -code = "possible-method-access-on-null" -message = "Attempting to call a method on `null`." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/SequenceFeature.php" -code = "class-must-be-final" -message = 'Class `PhpDb\TableGateway\Feature\SequenceFeature` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/SequenceFeature.php" -code = "invalid-array-access" -message = 'Cannot access array index on object `PhpDb\Sql\TableIdentifier` that does not implement `ArrayAccess`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/SequenceFeature.php" -code = "mixed-array-access" -message = "Unsafe array access on type `mixed`." -count = 2 - -[[issues]] -file = "src/TableGateway/Feature/SequenceFeature.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 2 - -[[issues]] -file = "src/TableGateway/Feature/SequenceFeature.php" -code = "mixed-property-type-coercion" -message = "A value with a less specific type `mixed` is being assigned to property `$sequenceValue` (int|null)." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/SequenceFeature.php" -code = "mixed-return-statement" -message = 'Could not infer a precise return type for function `PhpDb\TableGateway\Feature\SequenceFeature::lastSequenceId`. Saw type `mixed`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/SequenceFeature.php" -code = "mixed-return-statement" -message = 'Could not infer a precise return type for function `PhpDb\TableGateway\Feature\SequenceFeature::nextSequenceId`. Saw type `mixed`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/SequenceFeature.php" -code = "possible-method-access-on-null" -message = "Attempting to call a method on `null`." -count = 2 - -[[issues]] -file = "src/TableGateway/Feature/SequenceFeature.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #2 of `array_search`: expected `array`, but possibly received `PhpDb\Sql\TableIdentifier|array|string`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/SequenceFeature.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PhpDb\Exception\RuntimeException` in `PhpDb\TableGateway\Feature\SequenceFeature::preInsert`.' -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/SequenceFeature.php" -code = "unused-parameter" -message = "Parameter `$result` is never used." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/SequenceFeature.php" -code = "unused-parameter" -message = "Parameter `$statement` is never used." -count = 1 - [[issues]] file = "src/TableGateway/TableGateway.php" code = "class-must-be-final" @@ -6137,3 +5525,15 @@ file = "src/TableGateway/TableGatewayInterface.php" code = "missing-api-or-internal" message = 'Interface `PhpDb\TableGateway\TableGatewayInterface` is missing an `@api` or `@internal` annotation.' count = 1 + +[[issues]] +file = "src/TableGateway/AbstractTableGateway.php" +code = "nullable-return-statement" +message = 'Function `PhpDb\TableGateway\AbstractTableGateway::getTable` is declared to return `PhpDb\Sql\TableIdentifier|array|string` but possibly returns a nullable value (inferred as `PhpDb\Sql\TableIdentifier|array|null|string`).' +count = 1 + +[[issues]] +file = "src/TableGateway/AbstractTableGateway.php" +code = "invalid-return-statement" +message = 'Invalid return type for function `PhpDb\TableGateway\AbstractTableGateway::getTable`: expected `PhpDb\Sql\TableIdentifier|array|string`, but found `PhpDb\Sql\TableIdentifier|array|null|string`.' +count = 1 diff --git a/lint-baseline.toml b/lint-baseline.toml index 99046f37..fd77cbf6 100644 --- a/lint-baseline.toml +++ b/lint-baseline.toml @@ -348,12 +348,6 @@ code = "too-many-methods" message = "Class has too many methods." count = 1 -[[issues]] -file = "src/RowGateway/Feature/FeatureSet.php" -code = "too-many-methods" -message = "Class has too many methods." -count = 1 - [[issues]] file = "src/RowGateway/RowGateway.php" code = "no-else-clause" @@ -906,78 +900,6 @@ code = "too-many-methods" message = "Class has too many methods." count = 1 -[[issues]] -file = "src/TableGateway/Feature/EventFeature.php" -code = "no-shorthand-ternary" -message = "Use of the shorthand ternary operator." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/EventFeature.php" -code = "too-many-methods" -message = "Class has too many methods." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/FeatureSet.php" -code = "kan-defect" -message = "Class has a high kan defect score." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/FeatureSet.php" -code = "too-many-methods" -message = "Class has too many methods." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/GlobalAdapterFeature.php" -code = "no-isset" -message = "Use of the `isset` construct." -count = 2 - -[[issues]] -file = "src/TableGateway/Feature/MetadataFeature.php" -code = "no-else-clause" -message = "Avoid `else` clauses." -count = 2 - -[[issues]] -file = "src/TableGateway/Feature/RowGatewayFeature.php" -code = "no-else-clause" -message = "Avoid `else` clauses." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/RowGatewayFeature.php" -code = "no-else-clause" -message = "Avoid `elseif` clauses." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/RowGatewayFeature.php" -code = "no-isset" -message = "Use of the `isset` construct." -count = 2 - -[[issues]] -file = "src/TableGateway/Feature/SequenceFeature.php" -code = "strict-behavior" -message = "Call to `array_search` must enforce strict comparison." -count = 1 - -[[issues]] -file = "src/TableGateway/Feature/SequenceFeature.php" -code = "string-style" -message = "String concatenation can be replaced with interpolation." -count = 3 - -[[issues]] -file = "src/TableGateway/Feature/SequenceFeature.php" -code = "tagged-todo" -message = "TODO should be tagged with (@username) or (#issue)." -count = 1 - [[issues]] file = "src/TableGateway/TableGateway.php" code = "no-shorthand-ternary" @@ -9245,3 +9167,27 @@ file = "test/unit/TableGateway/TableGatewayTest.php" code = "strict-assertions" message = "Use strict assertions in PHPUnit tests." count = 2 + +[[issues]] +file = "src/RowGateway/Feature/FeatureSet.php" +code = "too-many-methods" +message = 'Class has too many methods.' +count = 1 + +[[issues]] +file = "src/TableGateway/Feature/FeatureSet.php" +code = "kan-defect" +message = 'Class has a high kan defect score.' +count = 1 + +[[issues]] +file = "src/TableGateway/Feature/FeatureSet.php" +code = "too-many-methods" +message = 'Class has too many methods.' +count = 1 + +[[issues]] +file = "src/TableGateway/Feature/EventFeature.php" +code = "too-many-methods" +message = 'Class has too many methods.' +count = 1 diff --git a/mago.toml b/mago.toml index ae9cf5c7..5ef92248 100644 --- a/mago.toml +++ b/mago.toml @@ -15,4 +15,6 @@ baseline = "lint-baseline.toml" baseline = "analyzer-baseline.toml" class-initializers = [ "PhpDb\\ResultSet\\AbstractResultSet::initialize", + "PhpDb\\TableGateway\\Feature\\AbstractFeature::setTableGateway", + "PhpDb\\RowGateway\\Feature\\AbstractFeature::setRowGateway", ] diff --git a/src/Feature/FeatureInterface.php b/src/Feature/FeatureInterface.php index 241a9121..4fcf01da 100644 --- a/src/Feature/FeatureInterface.php +++ b/src/Feature/FeatureInterface.php @@ -4,6 +4,9 @@ namespace PhpDb\Feature; +/** + * @api + */ interface FeatureInterface { /** @return array */ diff --git a/src/RowGateway/Feature/AbstractFeature.php b/src/RowGateway/Feature/AbstractFeature.php index fbf92656..2e4539db 100644 --- a/src/RowGateway/Feature/AbstractFeature.php +++ b/src/RowGateway/Feature/AbstractFeature.php @@ -9,10 +9,14 @@ use PhpDb\RowGateway\Exception; use PhpDb\RowGateway\Exception\RuntimeException; +/** + * @api + */ abstract class AbstractFeature extends AbstractRowGateway implements FeatureInterface { protected AbstractRowGateway $rowGateway; + /** @var array */ protected array $sharedData = []; /** @return array */ diff --git a/src/RowGateway/Feature/FeatureInterface.php b/src/RowGateway/Feature/FeatureInterface.php index d6931334..637c7701 100644 --- a/src/RowGateway/Feature/FeatureInterface.php +++ b/src/RowGateway/Feature/FeatureInterface.php @@ -7,6 +7,9 @@ use PhpDb\Feature\FeatureInterface as BaseFeatureInterface; use PhpDb\RowGateway\AbstractRowGateway; +/** + * @api + */ interface FeatureInterface extends BaseFeatureInterface { public function setRowGateway(AbstractRowGateway $rowGateway): void; diff --git a/src/RowGateway/Feature/FeatureSet.php b/src/RowGateway/Feature/FeatureSet.php index 213e1b58..1e567d23 100644 --- a/src/RowGateway/Feature/FeatureSet.php +++ b/src/RowGateway/Feature/FeatureSet.php @@ -13,15 +13,17 @@ */ class FeatureSet { - final public const APPLY_HALT = 'halt'; + final public const string APPLY_HALT = 'halt'; protected ?AbstractRowGateway $rowGateway = null; /** @var FeatureInterface[] */ protected array $features = []; + /** @var array */ protected array $magicSpecifications = []; + /** @param FeatureInterface[] $features */ public function __construct(array $features = []) { if ([] !== $features) { @@ -38,6 +40,7 @@ public function addFeature(FeatureInterface $feature): static return $this; } + /** @param FeatureInterface[] $features */ public function addFeatures(array $features): static { foreach ($features as $feature) { @@ -46,6 +49,12 @@ public function addFeatures(array $features): static return $this; } + /** + * @param array $args + * + * @mago-expect analysis:string-member-selector + * @mago-expect analysis:mixed-assignment + */ public function apply(string $method, array $args): void { foreach ($this->features as $feature) { @@ -60,31 +69,51 @@ public function apply(string $method, array $args): void } } + /** + * @mago-expect analysis:unused-parameter + * + * @param array $arguments + */ public function callMagicCall(string $method, array $arguments): mixed { return null; } + /** + * @mago-expect analysis:unused-parameter + */ public function callMagicGet(string $property): mixed { return null; } + /** + * @mago-expect analysis:unused-parameter + */ public function callMagicSet(string $property, mixed $value): mixed { return null; } + /** + * @mago-expect analysis:unused-parameter + */ public function canCallMagicCall(string $method): bool { return false; } + /** + * @mago-expect analysis:unused-parameter + */ public function canCallMagicGet(string $property): false { return false; } + /** + * @mago-expect analysis:unused-parameter + */ public function canCallMagicSet(string $property): false { return false; diff --git a/src/TableGateway/AbstractTableGateway.php b/src/TableGateway/AbstractTableGateway.php index 808a1a72..6c0b7da7 100644 --- a/src/TableGateway/AbstractTableGateway.php +++ b/src/TableGateway/AbstractTableGateway.php @@ -31,7 +31,7 @@ /** * @property AdapterInterface $adapter * @property int $lastInsertValue - * @property string $table + * @property TableIdentifier|string|array|null $table */ abstract class AbstractTableGateway implements TableGatewayInterface { diff --git a/src/TableGateway/Feature/AbstractFeature.php b/src/TableGateway/Feature/AbstractFeature.php index 310dd89e..29de0696 100644 --- a/src/TableGateway/Feature/AbstractFeature.php +++ b/src/TableGateway/Feature/AbstractFeature.php @@ -7,10 +7,14 @@ use Override; use PhpDb\TableGateway\AbstractTableGateway; +/** + * @api + */ abstract class AbstractFeature extends AbstractTableGateway implements FeatureInterface { protected AbstractTableGateway $tableGateway; + /** @var array */ protected array $sharedData = []; /** @return array */ diff --git a/src/TableGateway/Feature/EventFeature.php b/src/TableGateway/Feature/EventFeature.php index cac06cd6..a8d095a1 100644 --- a/src/TableGateway/Feature/EventFeature.php +++ b/src/TableGateway/Feature/EventFeature.php @@ -19,6 +19,9 @@ use function get_class; +/** + * @api + */ class EventFeature extends AbstractFeature implements EventFeatureEventsInterface, EventsCapableInterface { protected EventManagerInterface $eventManager; diff --git a/src/TableGateway/Feature/EventFeature/TableGatewayEvent.php b/src/TableGateway/Feature/EventFeature/TableGatewayEvent.php index f76a374c..df747c1d 100644 --- a/src/TableGateway/Feature/EventFeature/TableGatewayEvent.php +++ b/src/TableGateway/Feature/EventFeature/TableGatewayEvent.php @@ -8,12 +8,16 @@ use Override; use PhpDb\TableGateway\AbstractTableGateway; -class TableGatewayEvent implements EventInterface +/** + * @implements EventInterface|object> + */ +final class TableGatewayEvent implements EventInterface { protected ?AbstractTableGateway $target = null; protected ?string $name = null; + /** @var array|object */ protected array|object $params = []; #[Override] @@ -77,6 +81,8 @@ public function setName($name): void * * @param string|int $name * @param mixed $value + * + * @mago-expect analysis:possibly-invalid-array-access */ #[Override] public function setParam($name, $value): void @@ -87,8 +93,10 @@ public function setParam($name, $value): void /** * Set event parameters * - * @param array|object $params + * @param array|object $params * @phpstan-ignore selfOut.type + * + * @mago-expect analysis:unused-template-parameter */ #[Override] public function setParams($params): void @@ -101,6 +109,9 @@ public function setParams($params): void * * @param object|string|null $target * @phpstan-ignore selfOut.type + * + * @mago-expect analysis:unused-template-parameter + * @mago-expect analysis:property-type-coercion */ #[Override] public function setTarget($target): void diff --git a/src/TableGateway/Feature/EventFeatureEventsInterface.php b/src/TableGateway/Feature/EventFeatureEventsInterface.php index 166f0950..26778c4b 100644 --- a/src/TableGateway/Feature/EventFeatureEventsInterface.php +++ b/src/TableGateway/Feature/EventFeatureEventsInterface.php @@ -11,21 +11,23 @@ * into a separate interface that EventFeature implements; the change keeps * backwards compatibility, while simultaneously removing the need to add * another hard dependency to the component. + * + * @api */ interface EventFeatureEventsInterface { - public const EVENT_PRE_INITIALIZE = 'preInitialize'; - public const EVENT_POST_INITIALIZE = 'postInitialize'; + public const string EVENT_PRE_INITIALIZE = 'preInitialize'; + public const string EVENT_POST_INITIALIZE = 'postInitialize'; - public const EVENT_PRE_SELECT = 'preSelect'; - public const EVENT_POST_SELECT = 'postSelect'; + public const string EVENT_PRE_SELECT = 'preSelect'; + public const string EVENT_POST_SELECT = 'postSelect'; - public const EVENT_PRE_INSERT = 'preInsert'; - public const EVENT_POST_INSERT = 'postInsert'; + public const string EVENT_PRE_INSERT = 'preInsert'; + public const string EVENT_POST_INSERT = 'postInsert'; - public const EVENT_PRE_DELETE = 'preDelete'; - public const EVENT_POST_DELETE = 'postDelete'; + public const string EVENT_PRE_DELETE = 'preDelete'; + public const string EVENT_POST_DELETE = 'postDelete'; - public const EVENT_PRE_UPDATE = 'preUpdate'; - public const EVENT_POST_UPDATE = 'postUpdate'; + public const string EVENT_PRE_UPDATE = 'preUpdate'; + public const string EVENT_POST_UPDATE = 'postUpdate'; } diff --git a/src/TableGateway/Feature/FeatureInterface.php b/src/TableGateway/Feature/FeatureInterface.php index 29507148..3959d918 100644 --- a/src/TableGateway/Feature/FeatureInterface.php +++ b/src/TableGateway/Feature/FeatureInterface.php @@ -7,6 +7,9 @@ use PhpDb\Feature\FeatureInterface as BaseFeatureInterface; use PhpDb\TableGateway\AbstractTableGateway; +/** + * @api + */ interface FeatureInterface extends BaseFeatureInterface { public function setTableGateway(AbstractTableGateway $tableGateway): void; diff --git a/src/TableGateway/Feature/FeatureSet.php b/src/TableGateway/Feature/FeatureSet.php index efef7c2d..01c5c8d8 100644 --- a/src/TableGateway/Feature/FeatureSet.php +++ b/src/TableGateway/Feature/FeatureSet.php @@ -9,17 +9,22 @@ use function method_exists; +/** + * @api + */ class FeatureSet { - public const APPLY_HALT = 'halt'; + public const string APPLY_HALT = 'halt'; protected ?AbstractTableGateway $tableGateway = null; /** @var FeatureInterface[] */ protected array $features = []; + /** @var array */ protected array $magicSpecifications = []; + /** @param FeatureInterface[] $features */ public function __construct(array $features = []) { if ([] !== $features) { @@ -36,6 +41,7 @@ public function addFeature(FeatureInterface $feature): static return $this; } + /** @param FeatureInterface[] $features */ public function addFeatures(array $features): static { foreach ($features as $feature) { @@ -44,6 +50,12 @@ public function addFeatures(array $features): static return $this; } + /** + * @param array $args + * + * @mago-expect analysis:string-member-selector + * @mago-expect analysis:mixed-assignment + */ public function apply(string $method, array $args): void { foreach ($this->features as $feature) { @@ -60,6 +72,10 @@ public function apply(string $method, array $args): void /** * Call method of on added feature as though it were a local method + * + * @param array $arguments + * + * @mago-expect analysis:string-member-selector */ public function callMagicCall(string $method, array $arguments): mixed { @@ -72,11 +88,17 @@ public function callMagicCall(string $method, array $arguments): mixed return null; } + /** + * @mago-expect analysis:unused-parameter + */ public function callMagicGet(string $property): mixed { return null; } + /** + * @mago-expect analysis:unused-parameter + */ public function callMagicSet(string $property, mixed $value): mixed { return null; @@ -97,11 +119,17 @@ public function canCallMagicCall(string $method): bool return false; } + /** + * @mago-expect analysis:unused-parameter + */ public function canCallMagicGet(string $property): bool { return false; } + /** + * @mago-expect analysis:unused-parameter + */ public function canCallMagicSet(string $property): bool { return false; diff --git a/src/TableGateway/Feature/GlobalAdapterFeature.php b/src/TableGateway/Feature/GlobalAdapterFeature.php index de5fadeb..16f86cf5 100644 --- a/src/TableGateway/Feature/GlobalAdapterFeature.php +++ b/src/TableGateway/Feature/GlobalAdapterFeature.php @@ -7,6 +7,9 @@ use PhpDb\Adapter\AdapterInterface; use PhpDb\TableGateway\Exception; +/** + * @api + */ class GlobalAdapterFeature extends AbstractFeature { /** @var AdapterInterface[] */ diff --git a/src/TableGateway/Feature/MasterSlaveFeature.php b/src/TableGateway/Feature/MasterSlaveFeature.php index 500e56e6..d08bde5a 100644 --- a/src/TableGateway/Feature/MasterSlaveFeature.php +++ b/src/TableGateway/Feature/MasterSlaveFeature.php @@ -7,7 +7,7 @@ use PhpDb\Adapter\AdapterInterface; use PhpDb\Sql\Sql; -class MasterSlaveFeature extends AbstractFeature +final class MasterSlaveFeature extends AbstractFeature { protected AdapterInterface $slaveAdapter; diff --git a/src/TableGateway/Feature/MetadataFeature.php b/src/TableGateway/Feature/MetadataFeature.php index ac4c2d69..5cf102f0 100644 --- a/src/TableGateway/Feature/MetadataFeature.php +++ b/src/TableGateway/Feature/MetadataFeature.php @@ -13,6 +13,9 @@ use function current; use function is_array; +/** + * @api + */ class MetadataFeature extends AbstractFeature { /** diff --git a/src/TableGateway/Feature/RowGatewayFeature.php b/src/TableGateway/Feature/RowGatewayFeature.php index 95cc8161..6c25fe44 100644 --- a/src/TableGateway/Feature/RowGatewayFeature.php +++ b/src/TableGateway/Feature/RowGatewayFeature.php @@ -11,8 +11,9 @@ use function is_string; -class RowGatewayFeature extends AbstractFeature +final class RowGatewayFeature extends AbstractFeature { + /** @var array */ protected array $constructorArguments = []; public function __construct(mixed ...$constructorArguments) diff --git a/src/TableGateway/Feature/SequenceFeature.php b/src/TableGateway/Feature/SequenceFeature.php index 4cbe0091..2a50f392 100644 --- a/src/TableGateway/Feature/SequenceFeature.php +++ b/src/TableGateway/Feature/SequenceFeature.php @@ -11,6 +11,9 @@ use function array_search; +/** + * @api + */ class SequenceFeature extends AbstractFeature { protected string $primaryKeyField; @@ -29,6 +32,8 @@ public function __construct(string $primaryKeyField, string $sequenceName) * Return the most recent value from the specified sequence in the database. * * @throws RuntimeException + * + * @mago-expect analysis:mixed-assignment */ public function lastSequenceId(): int { @@ -56,6 +61,8 @@ public function lastSequenceId(): int * Generate a new value from the specified sequence in the database, and return it. * * @throws RuntimeException + * + * @mago-expect analysis:mixed-assignment */ public function nextSequenceId(): ?int { @@ -78,6 +85,9 @@ public function nextSequenceId(): ?int return $sequence['nextval']; } + /** + * @mago-expect analysis:unused-parameter + */ public function postInsert(StatementInterface $statement, ResultInterface $result): void { if (null !== $this->sequenceValue) { diff --git a/src/TableGateway/TableGateway.php b/src/TableGateway/TableGateway.php index c26604be..d2024437 100644 --- a/src/TableGateway/TableGateway.php +++ b/src/TableGateway/TableGateway.php @@ -15,6 +15,8 @@ class TableGateway extends AbstractTableGateway { /** + * @param Feature\FeatureSet|Feature\FeatureInterface|Feature\FeatureInterface[]|null $features + * * @throws Exception\InvalidArgumentException */ public function __construct( From 369798453387a0e8bbfb352cf717be95e5bdb0d6 Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Mon, 24 Aug 2026 10:20:33 +1000 Subject: [PATCH 2/2] Guard nullable state in the TableGateway features - Make EventFeature::$event non-nullable and assign it explicitly - Resolve GlobalAdapterFeature::getStaticAdapter through a coalesce chain and throw when no adapter is registered - Make MasterSlaveFeature::$masterSql nullable and throw when the gateway has no Sql instance or postInitialize has not run - Throw in SequenceFeature when a statement yields no result, when the sequence returns no usable value and when an insert exposes no arrays - Interpolate the SequenceFeature sequence statements and search insert columns strictly - Resolve the MetadataFeature table through getTable(), throw when it is not a named table, and take the primary key with reset() - Extract RowGatewayFeature::primaryKeyFromMetadata(), flatten the prototype branches and throw when the gateway has no named table - Replace the deprecated setArrayObjectPrototype() calls with setRowPrototype() --- src/TableGateway/Feature/EventFeature.php | 6 +- .../Feature/GlobalAdapterFeature.php | 12 +-- .../Feature/MasterSlaveFeature.php | 24 ++++- src/TableGateway/Feature/MetadataFeature.php | 42 +++++--- .../Feature/RowGatewayFeature.php | 101 ++++++++++++------ src/TableGateway/Feature/SequenceFeature.php | 51 ++++++--- 6 files changed, 163 insertions(+), 73 deletions(-) diff --git a/src/TableGateway/Feature/EventFeature.php b/src/TableGateway/Feature/EventFeature.php index a8d095a1..b153d9e0 100644 --- a/src/TableGateway/Feature/EventFeature.php +++ b/src/TableGateway/Feature/EventFeature.php @@ -26,7 +26,7 @@ class EventFeature extends AbstractFeature implements EventFeatureEventsInterfac { protected EventManagerInterface $eventManager; - protected ?EventFeature\TableGatewayEvent $event; + protected EventFeature\TableGatewayEvent $event; public function __construct( ?EventManagerInterface $eventManager = null, @@ -40,7 +40,9 @@ public function __construct( TableGateway::class, ]); - $this->event = $tableGatewayEvent ?: new EventFeature\TableGatewayEvent(); + $this->event = $tableGatewayEvent instanceof EventFeature\TableGatewayEvent + ? $tableGatewayEvent + : new EventFeature\TableGatewayEvent(); } /** diff --git a/src/TableGateway/Feature/GlobalAdapterFeature.php b/src/TableGateway/Feature/GlobalAdapterFeature.php index 16f86cf5..2e2f8eb0 100644 --- a/src/TableGateway/Feature/GlobalAdapterFeature.php +++ b/src/TableGateway/Feature/GlobalAdapterFeature.php @@ -24,17 +24,13 @@ public static function getStaticAdapter(): AdapterInterface { $class = static::class; - // class specific adapter - if (isset(static::$staticAdapters[$class])) { - return static::$staticAdapters[$class]; - } + $adapter = static::$staticAdapters[$class] ?? static::$staticAdapters[self::class] ?? null; - // default adapter - if (isset(static::$staticAdapters[self::class])) { - return static::$staticAdapters[self::class]; + if (! $adapter instanceof AdapterInterface) { + throw new Exception\RuntimeException('No database adapter was found in the static registry.'); } - throw new Exception\RuntimeException('No database adapter was found in the static registry.'); + return $adapter; } /** diff --git a/src/TableGateway/Feature/MasterSlaveFeature.php b/src/TableGateway/Feature/MasterSlaveFeature.php index d08bde5a..e07eac52 100644 --- a/src/TableGateway/Feature/MasterSlaveFeature.php +++ b/src/TableGateway/Feature/MasterSlaveFeature.php @@ -6,12 +6,13 @@ use PhpDb\Adapter\AdapterInterface; use PhpDb\Sql\Sql; +use PhpDb\TableGateway\Exception; final class MasterSlaveFeature extends AbstractFeature { protected AdapterInterface $slaveAdapter; - protected Sql $masterSql; + protected ?Sql $masterSql = null; protected ?Sql $slaveSql = null; @@ -35,14 +36,23 @@ public function getSlaveSql(): ?Sql /** * after initialization, retrieve the original adapter as "master" + * + * @throws Exception\RuntimeException */ public function postInitialize(): void { - $this->masterSql = $this->tableGateway->sql; + $masterSql = $this->tableGateway->sql; + if (! $masterSql instanceof Sql) { + throw new Exception\RuntimeException( + 'The table gateway must be initialized with a Sql instance before this feature is applied.', + ); + } + + $this->masterSql = $masterSql; if (null === $this->slaveSql) { $this->slaveSql = new Sql( $this->slaveAdapter, - $this->tableGateway->sql->getTable(), + $masterSql->getTable(), ); } } @@ -50,9 +60,17 @@ public function postInitialize(): void /** * postSelect() * Ensure to return to the master adapter + * + * @throws Exception\RuntimeException */ public function postSelect(): void { + if (! $this->masterSql instanceof Sql) { + throw new Exception\RuntimeException( + 'The master Sql instance is not available; postInitialize() has not been run.', + ); + } + $this->tableGateway->sql = $this->masterSql; } diff --git a/src/TableGateway/Feature/MetadataFeature.php b/src/TableGateway/Feature/MetadataFeature.php index 5cf102f0..2bc5423b 100644 --- a/src/TableGateway/Feature/MetadataFeature.php +++ b/src/TableGateway/Feature/MetadataFeature.php @@ -12,6 +12,8 @@ use function count; use function current; use function is_array; +use function is_string; +use function reset; /** * @api @@ -30,28 +32,43 @@ public function __construct( ]; } + /** + * @throws Exception\RuntimeException + */ public function postInitialize(): void { // localize variable for brevity $t = $this->tableGateway; $m = $this->metadata; - $tableGatewayTable = is_array($t->table) ? current($t->table) : $t->table; + $tableGatewayTable = $t->getTable(); + if (is_array($tableGatewayTable)) { + $tableGatewayTable = current($tableGatewayTable); + } - if ($tableGatewayTable instanceof TableIdentifier) { - $table = $tableGatewayTable->getTable(); - $schema = $tableGatewayTable->getSchema(); - } else { - $table = $tableGatewayTable; - $schema = null; + if (! $tableGatewayTable instanceof TableIdentifier && ! is_string($tableGatewayTable)) { + throw new Exception\RuntimeException( + 'The table gateway must reference a named table before metadata can be resolved.', + ); } + $table = $tableGatewayTable instanceof TableIdentifier + ? $tableGatewayTable->getTable() + : $tableGatewayTable; + + $schema = $tableGatewayTable instanceof TableIdentifier + ? $tableGatewayTable->getSchema() + : null; + // get column named $columns = $m->getColumnNames($table, $schema); $t->columns = $columns; // set locally - $this->sharedData['metadata']['columns'] = $columns; + $metadata = $this->sharedData['metadata'] ?? []; + $metadata = is_array($metadata) ? $metadata : []; + $metadata['columns'] = $columns; + $this->sharedData['metadata'] = $metadata; // process primary key only if table is a table; there are no PK constraints on views if (! $m->getTable($table, $schema) instanceof TableObject) { @@ -74,12 +91,9 @@ public function postInitialize(): void } $pkcColumns = $pkc->getColumns(); - if (count($pkcColumns) === 1) { - $primaryKey = $pkcColumns[0]; - } else { - $primaryKey = $pkcColumns; - } + $primaryKey = 1 === count($pkcColumns) ? reset($pkcColumns) : $pkcColumns; - $this->sharedData['metadata']['primaryKey'] = $primaryKey; + $metadata['primaryKey'] = $primaryKey; + $this->sharedData['metadata'] = $metadata; } } diff --git a/src/TableGateway/Feature/RowGatewayFeature.php b/src/TableGateway/Feature/RowGatewayFeature.php index 6c25fe44..cdfd9737 100644 --- a/src/TableGateway/Feature/RowGatewayFeature.php +++ b/src/TableGateway/Feature/RowGatewayFeature.php @@ -7,8 +7,10 @@ use PhpDb\ResultSet\ResultSet; use PhpDb\RowGateway\RowGateway; use PhpDb\RowGateway\RowGatewayInterface; +use PhpDb\Sql\TableIdentifier; use PhpDb\TableGateway\Exception; +use function is_array; use function is_string; final class RowGatewayFeature extends AbstractFeature @@ -21,50 +23,81 @@ public function __construct(mixed ...$constructorArguments) $this->constructorArguments = $constructorArguments; } + /** + * @throws Exception\RuntimeException + */ public function postInitialize(): void { - $args = $this->constructorArguments; - - /** @var ResultSet $resultSetPrototype */ $resultSetPrototype = $this->tableGateway->resultSetPrototype; - - if (! $this->tableGateway->resultSetPrototype instanceof ResultSet) { + if (! $resultSetPrototype instanceof ResultSet) { throw new Exception\RuntimeException( 'This feature ' . self::class . ' expects the ResultSet to be an instance of ' . ResultSet::class, ); } - if (isset($args[0])) { - if (is_string($args[0])) { - $primaryKey = $args[0]; - $rowGatewayPrototype = new RowGateway( - $primaryKey, - $this->tableGateway->table, - $this->tableGateway->adapter, - ); - $resultSetPrototype->setArrayObjectPrototype($rowGatewayPrototype); - } elseif ($args[0] instanceof RowGatewayInterface) { - $rowGatewayPrototype = $args[0]; - $resultSetPrototype->setArrayObjectPrototype($rowGatewayPrototype); - } - } else { - // get from metadata feature - $metadata = $this->tableGateway->featureSet->getFeatureByClassName( - MetadataFeature::class, + $firstArgument = $this->constructorArguments[0] ?? null; + + if ($firstArgument instanceof RowGatewayInterface) { + $resultSetPrototype->setRowPrototype($firstArgument); + return; + } + + if (null !== $firstArgument && ! is_string($firstArgument)) { + return; + } + + $primaryKey = $firstArgument ?? $this->primaryKeyFromMetadata(); + + $table = $this->tableGateway->table; + if (! is_string($table) && ! $table instanceof TableIdentifier) { + throw new Exception\RuntimeException( + 'The table gateway must reference a named table before a RowGateway prototype can be created.', + ); + } + + $resultSetPrototype->setRowPrototype(new RowGateway( + $primaryKey, + $table, + $this->tableGateway->adapter, + )); + } + + /** + * @return string|array + * + * @throws Exception\RuntimeException + * + * @mago-expect analysis:mixed-assignment + */ + private function primaryKeyFromMetadata(): string|array + { + $featureSet = $this->tableGateway->featureSet; + $metadata = $featureSet instanceof FeatureSet + ? $featureSet->getFeatureByClassName(MetadataFeature::class) + : null; + + $metadataData = $metadata instanceof MetadataFeature + ? $metadata->sharedData['metadata'] ?? null + : null; + + if (null === $metadataData) { + throw new Exception\RuntimeException( + 'No information was provided to the RowGatewayFeature and/or no MetadataFeature could be consulted ' + . 'to find the primary key necessary for RowGateway object creation.', ); - if (null === $metadata || ! isset($metadata->sharedData['metadata'])) { - throw new Exception\RuntimeException( - 'No information was provided to the RowGatewayFeature and/or no MetadataFeature could be consulted ' - . 'to find the primary key necessary for RowGateway object creation.', - ); - } - $primaryKey = $metadata->sharedData['metadata']['primaryKey']; - $rowGatewayPrototype = new RowGateway( - $primaryKey, - $this->tableGateway->table, - $this->tableGateway->adapter, + } + + if (! is_array($metadataData)) { + throw new Exception\RuntimeException('The MetadataFeature did not expose its metadata as an array.'); + } + + $primaryKey = $metadataData['primaryKey'] ?? null; + if (! is_string($primaryKey) && ! is_array($primaryKey)) { + throw new Exception\RuntimeException( + 'The MetadataFeature did not expose a usable primary key for RowGateway object creation.', ); - $resultSetPrototype->setArrayObjectPrototype($rowGatewayPrototype); } + + return $primaryKey; } } diff --git a/src/TableGateway/Feature/SequenceFeature.php b/src/TableGateway/Feature/SequenceFeature.php index 2a50f392..2e1bccd6 100644 --- a/src/TableGateway/Feature/SequenceFeature.php +++ b/src/TableGateway/Feature/SequenceFeature.php @@ -10,6 +10,8 @@ use PhpDb\Sql\Insert; use function array_search; +use function is_array; +use function is_int; /** * @api @@ -42,18 +44,25 @@ public function lastSequenceId(): int // todo: Remove string usage $sql = match ($platformName) { - 'Oracle' => 'SELECT ' - . $platform->quoteIdentifier($this->sequenceName) - . '.CURRVAL as "currval" FROM dual', + 'Oracle' => "SELECT {$platform->quoteIdentifier($this->sequenceName)}.CURRVAL as \"currval\" FROM dual", 'PostgreSQL' => 'SELECT LAST_INSERT_ROWID() as "currval"', default => throw new RuntimeException('Unsupported platform for retrieving last sequence id'), }; $statement = $this->tableGateway->adapter->createStatement(); $statement->prepare($sql); - $result = $statement->execute(); + $result = $statement->execute(); + if (! $result instanceof ResultInterface) { + throw new RuntimeException('The sequence statement did not produce a result.'); + } + $sequence = $result->current(); unset($statement, $result); + + if (! is_array($sequence) || ! is_int($sequence['currval'] ?? null)) { + throw new RuntimeException('The sequence did not return a current value.'); + } + return $sequence['currval']; } @@ -70,19 +79,28 @@ public function nextSequenceId(): ?int $platformName = $platform->getName(); $sql = match ($platformName) { - 'Oracle' => 'SELECT ' - . $platform->quoteIdentifier($this->sequenceName) - . '.NEXTVAL as "nextval" FROM dual', - 'PostgreSQL' => 'SELECT NEXTVAL(\'"' . $this->sequenceName . '"\')', + 'Oracle' => "SELECT {$platform->quoteIdentifier($this->sequenceName)}.NEXTVAL as \"nextval\" FROM dual", + 'PostgreSQL' => "SELECT NEXTVAL('\"{$this->sequenceName}\"')", default => throw new RuntimeException('Unsupported platform for retrieving next sequence id'), }; $statement = $this->tableGateway->adapter->createStatement(); $statement->prepare($sql); - $result = $statement->execute(); + $result = $statement->execute(); + if (! $result instanceof ResultInterface) { + throw new RuntimeException('The sequence statement did not produce a result.'); + } + $sequence = $result->current(); unset($statement, $result); - return $sequence['nextval']; + + if (! is_array($sequence)) { + throw new RuntimeException('The sequence did not return a next value.'); + } + + $nextValue = $sequence['nextval'] ?? null; + + return is_int($nextValue) ? $nextValue : null; } /** @@ -95,13 +113,22 @@ public function postInsert(StatementInterface $statement, ResultInterface $resul } } + /** + * @throws RuntimeException + */ public function preInsert(Insert $insert): Insert { $columns = $insert->getRawState('columns'); $values = $insert->getRawState('values'); - $key = array_search($this->primaryKeyField, $columns); + + if (! is_array($columns) || ! is_array($values)) { + throw new RuntimeException('The insert does not expose columns and values as arrays.'); + } + + $key = array_search($this->primaryKeyField, $columns, strict: true); if (false !== $key) { - $this->sequenceValue = $values[$key] ?? null; + $sequenceValue = $values[$key] ?? null; + $this->sequenceValue = is_int($sequenceValue) ? $sequenceValue : null; return $insert; }