diff --git a/analyzer-baseline.toml b/analyzer-baseline.toml index 0cf11376..d7150b8a 100644 --- a/analyzer-baseline.toml +++ b/analyzer-baseline.toml @@ -1500,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" 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/lint-baseline.toml b/lint-baseline.toml index 263478e5..b2f822ad 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" @@ -8465,27 +8489,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/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); + } +}