From 1830c9f49e7ba8150d66aef9ffb52f8e84da2965 Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Mon, 28 Sep 2026 11:10:24 +1000 Subject: [PATCH 1/2] Add PrimaryReplicaFeature; deprecate MasterSlaveFeature Introduces PrimaryReplicaFeature with the neutral primary/replica terminology, carrying the full behaviour of MasterSlaveFeature including the nullable Sql state and the postInitialize()/postSelect() guard clauses. MasterSlaveFeature now extends PrimaryReplicaFeature and is marked @deprecated. Its constructor keeps the original parameter names so named arguments continue to work, and getSlaveAdapter()/getSlaveSql() proxy to getReplicaAdapter()/getReplicaSql(). It is slated for removal in the next major release. Note that the RuntimeException message raised by postSelect() now refers to the "primary" Sql instance rather than the "master" one. Closes #111 --- docs/book/table-gateway.md | 19 +- .../Feature/MasterSlaveFeature.php | 72 +------ .../Feature/PrimaryReplicaFeature.php | 85 ++++++++ .../TableGateway/Feature/FeatureSetTest.php | 12 +- .../Feature/MasterSlaveFeatureTest.php | 157 +-------------- .../Feature/PrimaryReplicaFeatureTest.php | 188 ++++++++++++++++++ 6 files changed, 316 insertions(+), 217 deletions(-) create mode 100644 src/TableGateway/Feature/PrimaryReplicaFeature.php create mode 100644 test/unit/TableGateway/Feature/PrimaryReplicaFeatureTest.php diff --git a/docs/book/table-gateway.md b/docs/book/table-gateway.md index 1a4bdb4b..0e94e6a0 100644 --- a/docs/book/table-gateway.md +++ b/docs/book/table-gateway.md @@ -185,19 +185,30 @@ PhpDb\TableGateway\Feature\GlobalAdapterFeature::setStaticAdapter( $table = new MyTableGateway(); // adapter is statically loaded ``` -### MasterSlaveFeature +### PrimaryReplicaFeature -Use a master adapter for `insert()`, `update()`, and `delete()`, but switch -to a slave adapter for all `select()` operations: +Use a primary adapter for `insert()`, `update()`, and `delete()`, but switch +to a replica adapter for all `select()` operations: ```php $table = new TableGateway( 'artist', $adapter, - new Feature\MasterSlaveFeature($slaveAdapter) + new Feature\PrimaryReplicaFeature($replicaAdapter) ); ``` +### MasterSlaveFeature + +> ### Deprecated +> +> `MasterSlaveFeature` is deprecated as of 0.6.0 and will be removed in the next +> major release. Use [PrimaryReplicaFeature](#primaryreplicafeature) instead. + +`MasterSlaveFeature` extends `PrimaryReplicaFeature` and retains the original +API; `getSlaveAdapter()` and `getSlaveSql()` proxy to `getReplicaAdapter()` and +`getReplicaSql()` respectively. + ### MetadataFeature Populate `TableGateway` with column information from a `Metadata` object. It diff --git a/src/TableGateway/Feature/MasterSlaveFeature.php b/src/TableGateway/Feature/MasterSlaveFeature.php index e07eac52..aa36bfad 100644 --- a/src/TableGateway/Feature/MasterSlaveFeature.php +++ b/src/TableGateway/Feature/MasterSlaveFeature.php @@ -6,80 +6,30 @@ use PhpDb\Adapter\AdapterInterface; use PhpDb\Sql\Sql; -use PhpDb\TableGateway\Exception; -final class MasterSlaveFeature extends AbstractFeature +/** + * @deprecated Use PrimaryReplicaFeature instead. This class will be removed in the next major release. + */ +final class MasterSlaveFeature extends PrimaryReplicaFeature { - protected AdapterInterface $slaveAdapter; - - protected ?Sql $masterSql = null; - - protected ?Sql $slaveSql = null; - public function __construct(AdapterInterface $slaveAdapter, ?Sql $slaveSql = null) { - $this->slaveAdapter = $slaveAdapter; - if ($slaveSql instanceof Sql) { - $this->slaveSql = $slaveSql; - } - } - - public function getSlaveAdapter(): AdapterInterface - { - return $this->slaveAdapter; - } - - public function getSlaveSql(): ?Sql - { - return $this->slaveSql; - } - - /** - * after initialization, retrieve the original adapter as "master" - * - * @throws Exception\RuntimeException - */ - public function postInitialize(): void - { - $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, - $masterSql->getTable(), - ); - } + parent::__construct($slaveAdapter, $slaveSql); } /** - * postSelect() - * Ensure to return to the master adapter - * - * @throws Exception\RuntimeException + * @deprecated Use PrimaryReplicaFeature::getReplicaAdapter() instead. */ - public function postSelect(): void + public function getSlaveAdapter(): AdapterInterface { - 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; + return $this->getReplicaAdapter(); } /** - * preSelect() - * Replace adapter with slave temporarily + * @deprecated Use PrimaryReplicaFeature::getReplicaSql() instead. */ - public function preSelect(): void + public function getSlaveSql(): ?Sql { - $this->tableGateway->sql = $this->slaveSql; + return $this->getReplicaSql(); } } diff --git a/src/TableGateway/Feature/PrimaryReplicaFeature.php b/src/TableGateway/Feature/PrimaryReplicaFeature.php new file mode 100644 index 00000000..54ec91ec --- /dev/null +++ b/src/TableGateway/Feature/PrimaryReplicaFeature.php @@ -0,0 +1,85 @@ +replicaAdapter = $replicaAdapter; + if ($replicaSql instanceof Sql) { + $this->replicaSql = $replicaSql; + } + } + + public function getReplicaAdapter(): AdapterInterface + { + return $this->replicaAdapter; + } + + public function getReplicaSql(): ?Sql + { + return $this->replicaSql; + } + + /** + * after initialization, retrieve the original adapter as "primary" + * + * @throws Exception\RuntimeException + */ + public function postInitialize(): void + { + $primarySql = $this->tableGateway->sql; + if (! $primarySql instanceof Sql) { + throw new Exception\RuntimeException( + 'The table gateway must be initialized with a Sql instance before this feature is applied.', + ); + } + + $this->primarySql = $primarySql; + if (null === $this->replicaSql) { + $this->replicaSql = new Sql( + $this->replicaAdapter, + $primarySql->getTable(), + ); + } + } + + /** + * postSelect() + * Ensure to return to the primary adapter + * + * @throws Exception\RuntimeException + */ + public function postSelect(): void + { + if (! $this->primarySql instanceof Sql) { + throw new Exception\RuntimeException( + 'The primary Sql instance is not available; postInitialize() has not been run.', + ); + } + + $this->tableGateway->sql = $this->primarySql; + } + + /** + * preSelect() + * Replace adapter with replica temporarily + */ + public function preSelect(): void + { + $this->tableGateway->sql = $this->replicaSql; + } +} diff --git a/test/unit/TableGateway/Feature/FeatureSetTest.php b/test/unit/TableGateway/Feature/FeatureSetTest.php index a4cd6c4d..cd9548f2 100644 --- a/test/unit/TableGateway/Feature/FeatureSetTest.php +++ b/test/unit/TableGateway/Feature/FeatureSetTest.php @@ -12,8 +12,8 @@ use PhpDb\Metadata\Object\ConstraintObject; use PhpDb\TableGateway\AbstractTableGateway; use PhpDb\TableGateway\Feature\FeatureSet; -use PhpDb\TableGateway\Feature\MasterSlaveFeature; use PhpDb\TableGateway\Feature\MetadataFeature; +use PhpDb\TableGateway\Feature\PrimaryReplicaFeature; use PhpDb\TableGateway\Feature\SequenceFeature; use PhpDbTest\TableGateway\Feature\TestAsset\TestTableGatewayFeature; use PHPUnit\Framework\Attributes\CoversMethod; @@ -68,17 +68,17 @@ public function addFeatureThatFeatureDoesNotHaveTableGatewayButFeatureSetHas(): $mockMasterAdapter->expects($this->any())->method('getDriver')->willReturn($mockDriver); $mockMasterAdapter->expects($this->any())->method('getPlatform')->willReturn(new Sql92()); - $mockSlaveAdapter = $this->getMockBuilder(AdapterInterface::class)->getMock(); + $mockReplicaAdapter = $this->getMockBuilder(AdapterInterface::class)->getMock(); $mockStatement = $this->getMockBuilder(StatementInterface::class)->getMock(); $mockDriver = $this->getMockBuilder(DriverInterface::class)->getMock(); $mockDriver->expects($this->any())->method('createStatement')->willReturn($mockStatement); - $mockSlaveAdapter->expects($this->any())->method('getDriver')->willReturn($mockDriver); - $mockSlaveAdapter->expects($this->any())->method('getPlatform')->willReturn(new Sql92()); + $mockReplicaAdapter->expects($this->any())->method('getDriver')->willReturn($mockDriver); + $mockReplicaAdapter->expects($this->any())->method('getPlatform')->willReturn(new Sql92()); $tableGatewayMock = $this->getMockBuilder(AbstractTableGateway::class)->onlyMethods([])->getMock(); - $feature = new MasterSlaveFeature($mockSlaveAdapter); + $feature = new PrimaryReplicaFeature($mockReplicaAdapter); $featureSet = new FeatureSet(); $featureSet->setTableGateway($tableGatewayMock); @@ -132,7 +132,7 @@ public function applyCallsMethodOnFeatures(): void ->disableOriginalConstructor() ->getMock(); - $feature = new MasterSlaveFeature( + $feature = new PrimaryReplicaFeature( $this->getMockBuilder(AdapterInterface::class)->getMock(), ); diff --git a/test/unit/TableGateway/Feature/MasterSlaveFeatureTest.php b/test/unit/TableGateway/Feature/MasterSlaveFeatureTest.php index 3bf2103a..aa79223e 100644 --- a/test/unit/TableGateway/Feature/MasterSlaveFeatureTest.php +++ b/test/unit/TableGateway/Feature/MasterSlaveFeatureTest.php @@ -6,184 +6,49 @@ use Override; use PhpDb\Adapter\AdapterInterface; -use PhpDb\Adapter\Driver\DriverInterface; -use PhpDb\Adapter\Driver\ResultInterface; -use PhpDb\Adapter\Driver\StatementInterface; -use PhpDb\Adapter\Platform\Sql92; use PhpDb\Sql\Sql; -use PhpDb\TableGateway\AbstractTableGateway; -use PhpDb\TableGateway\Exception\RuntimeException; use PhpDb\TableGateway\Feature\MasterSlaveFeature; -use PhpDb\TableGateway\TableGateway; +use PhpDb\TableGateway\Feature\PrimaryReplicaFeature; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +#[IgnoreDeprecations] final class MasterSlaveFeatureTest extends TestCase { - protected MockObject&AdapterInterface $mockMasterAdapter; protected MockObject&AdapterInterface $mockSlaveAdapter; - protected MockObject&StatementInterface $mockStatement; protected MasterSlaveFeature $feature; - protected TableGateway&MockObject $table; - - /** - * @throws Exception - */ - #[Test] - public function constructorWithSlaveSql(): void - { - $slaveSql = new Sql($this->mockSlaveAdapter, 'foo'); - $feature = new MasterSlaveFeature($this->mockSlaveAdapter, $slaveSql); - - static::assertSame($slaveSql, $feature->getSlaveSql()); - } #[Test] - public function getSlaveAdapter(): void + public function extendsPrimaryReplicaFeature(): void { - static::assertSame($this->mockSlaveAdapter, $this->feature->getSlaveAdapter()); + static::assertInstanceOf(PrimaryReplicaFeature::class, $this->feature); } - /** - * @throws Exception - */ #[Test] - public function postInitialize(): void + public function getSlaveAdapterProxiesToGetReplicaAdapter(): void { - $this->getMockBuilder(TableGateway::class) - ->setConstructorArgs(['foo', $this->mockMasterAdapter, $this->feature]) - ->onlyMethods([]) - ->getMock(); - // postInitialize is run - static::assertSame($this->mockSlaveAdapter, $this->feature->getSlaveSql()->getAdapter()); + static::assertSame($this->feature->getReplicaAdapter(), $this->feature->getSlaveAdapter()); } /** * @throws Exception */ #[Test] - public function postInitializeThrowsWhenTableGatewayHasNoSql(): void - { - $tableGateway = $this->getMockBuilder(AbstractTableGateway::class)->onlyMethods([])->getMock(); - - $feature = new MasterSlaveFeature($this->mockSlaveAdapter); - $feature->setTableGateway($tableGateway); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage( - 'The table gateway must be initialized with a Sql instance before this feature is applied.', - ); - - $feature->postInitialize(); - } - - /** - * @throws Exception - */ - #[Test] - public function postInitializeWithProvidedSlaveSql(): void + public function getSlaveSqlProxiesToGetReplicaSql(): void { $slaveSql = new Sql($this->mockSlaveAdapter, 'foo'); $feature = new MasterSlaveFeature($this->mockSlaveAdapter, $slaveSql); - $this->getMockBuilder(TableGateway::class) - ->setConstructorArgs(['foo', $this->mockMasterAdapter, $feature]) - ->onlyMethods([]) - ->getMock(); - - // The provided slaveSql should be used instead of creating a new one - static::assertSame($slaveSql, $feature->getSlaveSql()); - } - - /** - * @throws Exception - */ - #[Test] - public function postSelect(): void - { - $table = $this->getMockBuilder(TableGateway::class) - ->setConstructorArgs(['foo', $this->mockMasterAdapter, $this->feature]) - ->onlyMethods([]) - ->getMock(); - - /** @var MockObject&StatementInterface $stmt */ - $stmt = $this->mockSlaveAdapter - ->getDriver() - ->createStatement(); - - $stmt->expects($this->once()) - ->method('execute') - ->willReturn( - $this->getMockBuilder(ResultInterface::class) - ->onlyMethods([]) - ->getMock(), - ); - - $masterSql = $table->getSql(); - $table->select('foo = bar'); - - // test that the sql object is restored - static::assertSame($masterSql, $table->getSql()); - } - - #[Test] - public function postSelectThrowsWhenPostInitializeHasNotRun(): void - { - $feature = new MasterSlaveFeature($this->mockSlaveAdapter); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('The master Sql instance is not available; postInitialize() has not been run.'); - - $feature->postSelect(); - } - - /** - * @throws Exception - */ - #[Test] - public function preSelect(): void - { - $this->expectNotToPerformAssertions(); - - $table = $this->getMockBuilder(TableGateway::class) - ->setConstructorArgs(['foo', $this->mockMasterAdapter, $this->feature]) - ->onlyMethods([]) - ->getMock(); - - /** @var MockObject&StatementInterface $stmt */ - $stmt = $this->mockSlaveAdapter - ->getDriver() - ->createStatement(); - - $stmt->expects($this->once()) - ->method('execute') - ->willReturn($this->getMockBuilder(ResultInterface::class)->onlyMethods([])->getMock()); - $table->select('foo = bar'); + static::assertSame($feature->getReplicaSql(), $feature->getSlaveSql()); } #[Override] protected function setUp(): void { - $this->mockMasterAdapter = $this->getMockBuilder(AdapterInterface::class)->onlyMethods([])->getMock(); - $this->mockSlaveAdapter = $this->getMockBuilder(AdapterInterface::class)->onlyMethods([])->getMock(); - $this->mockStatement = $this->getMockBuilder(StatementInterface::class)->onlyMethods([])->getMock(); - - $mockDriver = $this->getMockBuilder(DriverInterface::class)->onlyMethods([])->getMock(); - $mockDriver->expects($this->any()) - ->method('createStatement') - ->willReturn(clone $this->mockStatement); - $this->mockMasterAdapter->expects($this->any())->method('getDriver')->willReturn($mockDriver); - $this->mockMasterAdapter->expects($this->any())->method('getPlatform')->willReturn(new Sql92()); - - $mockDriver = $this->getMockBuilder(DriverInterface::class)->onlyMethods([])->getMock(); - $mockDriver->expects($this->any()) - ->method('createStatement') - ->willReturn(clone $this->mockStatement); - $this->mockSlaveAdapter->expects($this->any())->method('getDriver')->willReturn($mockDriver); - $this->mockSlaveAdapter->expects($this->any())->method('getPlatform')->willReturn(new Sql92()); - - $this->feature = new MasterSlaveFeature($this->mockSlaveAdapter); + $this->mockSlaveAdapter = $this->getMockBuilder(AdapterInterface::class)->onlyMethods([])->getMock(); + $this->feature = new MasterSlaveFeature($this->mockSlaveAdapter); } } diff --git a/test/unit/TableGateway/Feature/PrimaryReplicaFeatureTest.php b/test/unit/TableGateway/Feature/PrimaryReplicaFeatureTest.php new file mode 100644 index 00000000..7af3ebb3 --- /dev/null +++ b/test/unit/TableGateway/Feature/PrimaryReplicaFeatureTest.php @@ -0,0 +1,188 @@ +mockReplicaAdapter, 'foo'); + $feature = new PrimaryReplicaFeature($this->mockReplicaAdapter, $replicaSql); + + static::assertSame($replicaSql, $feature->getReplicaSql()); + } + + #[Test] + public function getReplicaAdapter(): void + { + static::assertSame($this->mockReplicaAdapter, $this->feature->getReplicaAdapter()); + } + + /** + * @throws Exception + */ + #[Test] + public function postInitialize(): void + { + $this->getMockBuilder(TableGateway::class) + ->setConstructorArgs(['foo', $this->mockPrimaryAdapter, $this->feature]) + ->onlyMethods([]) + ->getMock(); + // postInitialize is run + static::assertSame($this->mockReplicaAdapter, $this->feature->getReplicaSql()->getAdapter()); + } + + /** + * @throws Exception + */ + #[Test] + public function postInitializeThrowsWhenTableGatewayHasNoSql(): void + { + $tableGateway = $this->getMockBuilder(AbstractTableGateway::class)->onlyMethods([])->getMock(); + + $feature = new PrimaryReplicaFeature($this->mockReplicaAdapter); + $feature->setTableGateway($tableGateway); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage( + 'The table gateway must be initialized with a Sql instance before this feature is applied.', + ); + + $feature->postInitialize(); + } + + /** + * @throws Exception + */ + #[Test] + public function postInitializeWithProvidedReplicaSql(): void + { + $replicaSql = new Sql($this->mockReplicaAdapter, 'foo'); + $feature = new PrimaryReplicaFeature($this->mockReplicaAdapter, $replicaSql); + + $this->getMockBuilder(TableGateway::class) + ->setConstructorArgs(['foo', $this->mockPrimaryAdapter, $feature]) + ->onlyMethods([]) + ->getMock(); + + // The provided replicaSql should be used instead of creating a new one + static::assertSame($replicaSql, $feature->getReplicaSql()); + } + + /** + * @throws Exception + */ + #[Test] + public function postSelect(): void + { + $table = $this->getMockBuilder(TableGateway::class) + ->setConstructorArgs(['foo', $this->mockPrimaryAdapter, $this->feature]) + ->onlyMethods([]) + ->getMock(); + + /** @var MockObject&StatementInterface $stmt */ + $stmt = $this->mockReplicaAdapter + ->getDriver() + ->createStatement(); + + $stmt->expects($this->once()) + ->method('execute') + ->willReturn( + $this->getMockBuilder(ResultInterface::class) + ->onlyMethods([]) + ->getMock(), + ); + + $primarySql = $table->getSql(); + $table->select('foo = bar'); + + // test that the sql object is restored + static::assertSame($primarySql, $table->getSql()); + } + + #[Test] + public function postSelectThrowsWhenPostInitializeHasNotRun(): void + { + $feature = new PrimaryReplicaFeature($this->mockReplicaAdapter); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The primary Sql instance is not available; postInitialize() has not been run.'); + + $feature->postSelect(); + } + + /** + * @throws Exception + */ + #[Test] + public function preSelect(): void + { + $this->expectNotToPerformAssertions(); + + $table = $this->getMockBuilder(TableGateway::class) + ->setConstructorArgs(['foo', $this->mockPrimaryAdapter, $this->feature]) + ->onlyMethods([]) + ->getMock(); + + /** @var MockObject&StatementInterface $stmt */ + $stmt = $this->mockReplicaAdapter + ->getDriver() + ->createStatement(); + + $stmt->expects($this->once()) + ->method('execute') + ->willReturn($this->getMockBuilder(ResultInterface::class)->onlyMethods([])->getMock()); + $table->select('foo = bar'); + } + + #[Override] + protected function setUp(): void + { + $this->mockPrimaryAdapter = $this->getMockBuilder(AdapterInterface::class)->onlyMethods([])->getMock(); + $this->mockReplicaAdapter = $this->getMockBuilder(AdapterInterface::class)->onlyMethods([])->getMock(); + $this->mockStatement = $this->getMockBuilder(StatementInterface::class)->onlyMethods([])->getMock(); + + $mockDriver = $this->getMockBuilder(DriverInterface::class)->onlyMethods([])->getMock(); + $mockDriver->expects($this->any()) + ->method('createStatement') + ->willReturn(clone $this->mockStatement); + $this->mockPrimaryAdapter->expects($this->any())->method('getDriver')->willReturn($mockDriver); + $this->mockPrimaryAdapter->expects($this->any())->method('getPlatform')->willReturn(new Sql92()); + + $mockDriver = $this->getMockBuilder(DriverInterface::class)->onlyMethods([])->getMock(); + $mockDriver->expects($this->any()) + ->method('createStatement') + ->willReturn(clone $this->mockStatement); + $this->mockReplicaAdapter->expects($this->any())->method('getDriver')->willReturn($mockDriver); + $this->mockReplicaAdapter->expects($this->any())->method('getPlatform')->willReturn(new Sql92()); + + $this->feature = new PrimaryReplicaFeature($this->mockReplicaAdapter); + } +} From 02eac40671c556f3e9275776b50124dcd87edf10 Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Mon, 28 Sep 2026 11:20:12 +1000 Subject: [PATCH 2/2] Restore formatting and mago baselines for mago 1.50.0 The Mago QA job fails on 0.6.x independently of any feature work: the pinned toolchain is now mago 1.50.0, whose analyzer emits codes and inferred types that the committed baselines predate, and two multi-line conditions in AbstractTableGateway drifted from the formatter. Reformats those conditions and regenerates both baselines so format, lint and analyze pass again. The regenerated analyzer baseline absorbs 15 genuine type-precision complaints in Profiler, PredicateSet, AbstractSql and CreateTable. They are suppressed here only to restore a green build; each still warrants fixing on its own merit. --- analyzer-baseline.toml | 205 ++++++++-------------- lint-baseline.toml | 60 +++---- src/TableGateway/AbstractTableGateway.php | 8 +- 3 files changed, 100 insertions(+), 173 deletions(-) diff --git a/analyzer-baseline.toml b/analyzer-baseline.toml index 90717a3d..d7150b8a 100644 --- a/analyzer-baseline.toml +++ b/analyzer-baseline.toml @@ -1173,7 +1173,7 @@ count = 1 [[issues]] file = "src/Adapter/Profiler/Profiler.php" code = "invalid-property-assignment-value" -message = '''Invalid type for property `$profiles`: expected `array`, but got `array`.''' +message = '''Invalid type for property `$profiles`: expected `array`, but got `non-empty-array`.''' count = 1 [[issues]] @@ -1194,24 +1194,6 @@ code = "missing-property-type" message = "Property `$profiles` is missing a type hint." count = 1 -[[issues]] -file = "src/Adapter/Profiler/Profiler.php" -code = "possibly-null-operand" -message = "Right operand in arithmetic operation might be `null` (type `float|null`)." -count = 1 - -[[issues]] -file = "src/Adapter/Profiler/Profiler.php" -code = "possibly-undefined-int-array-index" -message = '''Possibly undefined array key `int` accessed on `array`.''' -count = 1 - -[[issues]] -file = "src/Adapter/Profiler/Profiler.php" -code = "possibly-undefined-string-array-index" -message = '''Possibly undefined array key accessed on `array{'elapse': float|null, 'end': float, 'parameters': PhpDb\Adapter\ParameterContainer|null, 'sql': string, 'start': float}|array{'end': float}`.''' -count = 1 - [[issues]] file = "src/Adapter/Profiler/Profiler.php" code = "unhandled-thrown-type" @@ -1518,6 +1500,12 @@ code = "missing-constant-type" message = "Class constant `DEFAULT_SCHEMA` is missing a type hint." count = 1 +[[issues]] +file = "src/Metadata/Source/AbstractSource.php" +code = "unfulfilled-expect" +message = "This pragma was not used and may be removed." +count = 3 + [[issues]] file = "src/ResultSet/AbstractResultSet.php" code = "imprecise-type" @@ -2076,6 +2064,12 @@ code = "class-must-be-final" message = 'Class `PhpDb\RowGateway\Exception\RuntimeException` should be declared `final`.' 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" @@ -2142,12 +2136,6 @@ code = "invalid-iterator" message = "The expression provided to `foreach` is not iterable. It resolved to type `mixed`, which is not iterable." count = 1 -[[issues]] -file = "src/Sql/AbstractSql.php" -code = "invalid-member-selector" -message = "Invalid type `array-key` used as a member selector; a string is expected." -count = 1 - [[issues]] file = "src/Sql/AbstractSql.php" code = "invalid-operand" @@ -2187,7 +2175,7 @@ count = 1 [[issues]] file = "src/Sql/AbstractSql.php" code = "less-specific-nested-argument-type" -message = "Argument type mismatch for argument #2 of `vsprintf`: expected `array`, but provided type `array{}|non-empty-list` is less specific." +message = "Argument type mismatch for argument #2 of `vsprintf`: expected `array`, but provided type `list` is less specific." count = 1 [[issues]] @@ -2376,6 +2364,12 @@ code = "string-member-selector" message = "This member selector uses a non-literal string type (`non-empty-string`); its specific value cannot be statically determined." count = 1 +[[issues]] +file = "src/Sql/AbstractSql.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/Sql/AbstractSql.php" code = "unhandled-thrown-type" @@ -3219,7 +3213,13 @@ count = 1 [[issues]] file = "src/Sql/Ddl/CreateTable.php" code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `string('combinedBy')` accessed on `array|string>`." +message = "Possibly undefined array key `string('combinedBy')` accessed on `array>`." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTable.php" +code = "possibly-undefined-string-array-index" +message = "Possibly undefined array key `string('combinedBy')` accessed on `array`." count = 1 [[issues]] @@ -3900,12 +3900,6 @@ code = "class-must-be-final" message = 'Class `PhpDb\Sql\Platform\Platform` should be declared `final`.' count = 1 -[[issues]] -file = "src/Sql/Platform/Platform.php" -code = "imprecise-type" -message = "Type `array` in return type of `getDecorators` is imprecise, equivalent to `array`." -count = 1 - [[issues]] file = "src/Sql/Platform/Platform.php" code = "invalid-iterator" @@ -4251,7 +4245,7 @@ count = 1 [[issues]] file = "src/Sql/Predicate/PredicateSet.php" code = "less-specific-nested-argument-type" -message = "Argument type mismatch for argument #2 of `implode`: expected `array|null`, but provided type `array{}|non-empty-list` is less specific." +message = "Argument type mismatch for argument #2 of `implode`: expected `array|null`, but provided type `list` is less specific." count = 1 [[issues]] @@ -4263,7 +4257,7 @@ count = 1 [[issues]] file = "src/Sql/Predicate/PredicateSet.php" code = "less-specific-nested-return-statement" -message = '''Returned type `array{'spec': string, 'values': array{}|list}` is less specific than the declared return type `array{'spec': string, 'values': array}` for function `PhpDb\Sql\Predicate\PredicateSet::getExpressionData` due to nested 'mixed'.''' +message = '''Returned type `array{'spec': string, 'values': list}` is less specific than the declared return type `array{'spec': string, 'values': array}` for function `PhpDb\Sql\Predicate\PredicateSet::getExpressionData` due to nested 'mixed'.''' count = 1 [[issues]] @@ -4518,12 +4512,6 @@ code = "invalid-property-assignment-value" message = 'Invalid type for property `$joins`: expected `PhpDb\Sql\Join`, but got `null`.' count = 1 -[[issues]] -file = "src/Sql/Select.php" -code = "invalid-property-assignment-value" -message = "Invalid type for property `$specifications`: expected `array>|array`, but got `array|string>`." -count = 1 - [[issues]] file = "src/Sql/Select.php" code = "invalid-property-assignment-value" @@ -5218,36 +5206,6 @@ count = 1 file = "src/TableGateway/AbstractTableGateway.php" code = "impossible-condition" message = "This condition (type `false`) will always evaluate to false." -count = 7 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "impossible-type-comparison" -message = "Impossible type assertion: `$deleteState['table']` of type `string` can never be `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "impossible-type-comparison" -message = "Impossible type assertion: `$insertState['table']` of type `string` can never be `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "impossible-type-comparison" -message = "Impossible type assertion: `$this->table` of type `string` can never be `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "impossible-type-comparison" -message = "Impossible type assertion: `$this->table` of type `string` can never be `object`." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "impossible-type-comparison" -message = "Impossible type assertion: `$updateState['table']` of type `string` can never be `array`." count = 1 [[issues]] @@ -5294,9 +5252,9 @@ count = 1 [[issues]] file = "src/TableGateway/AbstractTableGateway.php" -code = "invalid-clone" -message = "Invalid `clone` operation on non-cloneable type `never`." -count = 1 +code = "invalid-array-access" +message = 'Cannot access array index on object `PhpDb\Sql\TableIdentifier` that does not implement `ArrayAccess`.' +count = 3 [[issues]] file = "src/TableGateway/AbstractTableGateway.php" @@ -5328,6 +5286,12 @@ code = "invalid-return-statement" message = 'Invalid return type for function `PhpDb\TableGateway\AbstractTableGateway::getSql`: expected `PhpDb\Sql\Sql`, but found `PhpDb\Sql\Sql|null`.' 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 + [[issues]] file = "src/TableGateway/AbstractTableGateway.php" code = "missing-api-or-internal" @@ -5430,24 +5394,6 @@ code = "mixed-return-statement" message = 'Could not infer a precise return type for function `PhpDb\TableGateway\AbstractTableGateway::executeUpdate`. Saw type `mixed`.' count = 1 -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "no-value" -message = "Argument #1 passed to function `array_values` has type `never`, meaning it cannot produce a value." -count = 3 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "no-value" -message = "Argument #1 passed to function `count` has type `never`, meaning it cannot produce a value." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "no-value" -message = "Argument #1 passed to function `reset` has type `never`, meaning it cannot produce a value." -count = 1 - [[issues]] file = "src/TableGateway/AbstractTableGateway.php" code = "nullable-return-statement" @@ -5472,6 +5418,12 @@ code = "nullable-return-statement" message = 'Function `PhpDb\TableGateway\AbstractTableGateway::getSql` is declared to return `PhpDb\Sql\Sql` but possibly returns a nullable value (inferred as `PhpDb\Sql\Sql|null`).' 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 = "overly-wide-return-type" @@ -5498,15 +5450,15 @@ count = 1 [[issues]] file = "src/TableGateway/AbstractTableGateway.php" -code = "redundant-comparison" -message = "Redundant `===` comparison: left-hand side is never identical to right-hand side." -count = 2 +code = "possibly-undefined-string-array-index" +message = "Possibly undefined array key `string('table')` accessed on `array`." +count = 3 [[issues]] file = "src/TableGateway/AbstractTableGateway.php" -code = "redundant-logical-operation" -message = "Redundant `&&` operation: left operand is always falsy and right operand is not evaluated." -count = 2 +code = "redundant-comparison" +message = "Redundant `===` comparison: left-hand side is never identical to right-hand side." +count = 1 [[issues]] file = "src/TableGateway/Exception/ExceptionInterface.php" @@ -5527,27 +5479,39 @@ message = 'Class `PhpDb\TableGateway\Exception\RuntimeException` should be decla count = 1 [[issues]] -file = "src/TableGateway/TableGateway.php" -code = "class-must-be-final" -message = 'Class `PhpDb\TableGateway\TableGateway` should be declared `final`.' +file = "src/TableGateway/Feature/MetadataFeature.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." count = 1 [[issues]] -file = "src/TableGateway/TableGateway.php" -code = "imprecise-type" -message = "Type `array` in parameter `$features` is imprecise, equivalent to `array`." +file = "src/TableGateway/Feature/MetadataFeature.php" +code = "mixed-assignment" +message = "Assigning `nonnull` type to a variable may lead to unexpected behavior." +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 = 1 + +[[issues]] +file = "src/TableGateway/Feature/SequenceFeature.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." count = 1 [[issues]] file = "src/TableGateway/TableGateway.php" -code = "imprecise-type" -message = "Type `array` in parameter `$table` is imprecise, equivalent to `array`." +code = "class-must-be-final" +message = 'Class `PhpDb\TableGateway\TableGateway` should be declared `final`.' count = 1 [[issues]] file = "src/TableGateway/TableGateway.php" -code = "invalid-property-assignment-value" -message = 'Invalid type for property `$table`: expected `string`, but got `PhpDb\Sql\TableIdentifier|array|string`.' +code = "imprecise-type" +message = "Type `array` in parameter `$table` is imprecise, equivalent to `array`." count = 1 [[issues]] @@ -5567,28 +5531,3 @@ 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 - - -[[issues]] -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 = 3 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `string('table')` accessed on `array`." -count = 3 diff --git a/lint-baseline.toml b/lint-baseline.toml index ff32f48d..23abc728 100644 --- a/lint-baseline.toml +++ b/lint-baseline.toml @@ -414,6 +414,12 @@ code = "yoda-conditions" message = "Use Yoda condition style for safer comparisons" count = 6 +[[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" @@ -966,6 +972,24 @@ 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 + +[[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/TableGateway.php" code = "no-shorthand-ternary" @@ -1284,18 +1308,6 @@ code = "assertion-style" message = "Inconsistent assertions style." count = 10 -[[issues]] -file = "test/unit/Adapter/Container/AdapterInterfaceDelegatorTest.php" -code = "no-redundant-variable" -message = "Variable `$container` is assigned but never used." -count = 1 - -[[issues]] -file = "test/unit/Adapter/Container/AdapterInterfaceDelegatorTest.php" -code = "no-redundant-variable" -message = "Variable `$pluginManagerConfig` is assigned but never used." -count = 1 - [[issues]] file = "test/unit/Adapter/Container/AdapterInterfaceDelegatorTest.php" code = "prefer-test-attribute" @@ -8483,27 +8495,3 @@ 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/src/TableGateway/AbstractTableGateway.php b/src/TableGateway/AbstractTableGateway.php index 6c0b7da7..0d0c2f51 100644 --- a/src/TableGateway/AbstractTableGateway.php +++ b/src/TableGateway/AbstractTableGateway.php @@ -347,8 +347,8 @@ protected function executeSelect(Select $select): ResultSetInterface if ( isset($selectState['columns']) - && [Select::SQL_STAR] === $selectState['columns'] - && [] !== $this->columns + && [Select::SQL_STAR] === $selectState['columns'] + && [] !== $this->columns ) { $select->columns($this->columns); } @@ -430,8 +430,8 @@ public function __clone(): void $this->table = clone $this->table; } elseif ( is_array($this->table) - && count($this->table) === 1 - && is_object(reset($this->table)) + && count($this->table) === 1 + && is_object(reset($this->table)) ) { foreach ($this->table as &$tableObject) { $tableObject = clone $tableObject;