diff --git a/src/ResultSet/RowPrototypeInterface.php b/src/ResultSet/RowPrototypeInterface.php index 64734d64..44b3ece1 100644 --- a/src/ResultSet/RowPrototypeInterface.php +++ b/src/ResultSet/RowPrototypeInterface.php @@ -5,18 +5,18 @@ namespace PhpDb\ResultSet; /** - * Interface for objects that can serve as row prototypes in ResultSets. + * Interface for objects that can serve as row prototypes in RowPrototypeResultSets. * - * Row prototypes are cloned for each row and populated via exchangeArray(). + * Row prototypes are cloned (but do not have to be) for each row and populated via populate(). * This interface allows custom row objects (like RowGateway) to be used - * as prototypes alongside ArrayObject. + * as prototypes without depending on ArrayObject. */ interface RowPrototypeInterface { /** - * Exchange the current data for the provided array. + * Populate the prototype with row data. Mutating vs. returning a new instance is up to the implementation. */ - public function exchangeArray(array $array): array; + public function populate(array $data): RowPrototypeInterface; /** * Current data as an array and match current RowGateway implementations. diff --git a/src/ResultSet/RowPrototypeResultSet.php b/src/ResultSet/RowPrototypeResultSet.php new file mode 100644 index 00000000..898a66bb --- /dev/null +++ b/src/ResultSet/RowPrototypeResultSet.php @@ -0,0 +1,59 @@ +getRowPrototype())->populate($data); + } + + return $data; + } + + /** {@inheritDoc} */ + #[Override] + public function getRowPrototype(): RowPrototypeInterface + { + return $this->rowPrototype; + } + + /** {@inheritDoc} */ + #[Override] + public function setRowPrototype(RowPrototypeInterface $rowPrototype): ResultSetInterface&RowPrototypeResultSetInterface + { + $this->rowPrototype = $rowPrototype; + + return $this; + } + + /** {@inheritDoc} */ + #[Override] + public function toArray(): array + { + $return = []; + foreach ($this as $row) { + $return[] = $row instanceof RowPrototypeInterface ? $row->toArray() : $row; + } + + return $return; + } +} diff --git a/src/ResultSet/RowPrototypeResultSetInterface.php b/src/ResultSet/RowPrototypeResultSetInterface.php new file mode 100644 index 00000000..ce10aa20 --- /dev/null +++ b/src/ResultSet/RowPrototypeResultSetInterface.php @@ -0,0 +1,15 @@ +data); + } + + #[Override] + public function delete(): int + { + $this->initialize(); + + $where = []; + foreach ($this->primaryKeyColumn as $pkColumn) { + $where[$pkColumn] = $this->primaryKeyData[$pkColumn] ?? null; + } + + // @todo determine if we need to do a select to ensure 1 row will be affected + + $rowsAffected = 0; + $statement = $this->sql->prepareStatementForSqlObject($this->sql->delete()->where($where)); + $result = $statement->execute(); + + $rowsAffected = $result->getAffectedRows(); + if ($rowsAffected === 1) { + $this->primaryKeyData = null; + } + + return $rowsAffected; + } + + /** + * docs: Behaviour has changed - this no longer returns RowGatewayInterface but + * instead an array of the old data as per original PHP spec. + * + * @return array + */ + public function exchangeArray(array $array): array + { + $oldData = $this->data; + + $this->populate($array, true); + + return $oldData; + } + /** * initialize() */ @@ -63,10 +109,62 @@ public function initialize(): void $this->isInitialized = true; } + /** + * Offset Exists + * + * @param string $offset + */ + #[Override] + #[ReturnTypeWillChange] + public function offsetExists($offset): bool + { + return array_key_exists($offset, $this->data); + } + + /** + * Offset get + * + * @param string $offset + */ + #[Override] + #[ReturnTypeWillChange] + public function offsetGet($offset): mixed + { + return $this->data[$offset]; + } + + /** + * Offset set + * + * @param string $offset + */ + #[Override] + #[ReturnTypeWillChange] + public function offsetSet($offset, mixed $value): static + { + $this->data[$offset] = $value; + + return $this; + } + + /** + * Offset unset + * + * @param string $offset + */ + #[Override] + #[ReturnTypeWillChange] + public function offsetUnset($offset): static + { + $this->data[$offset] = null; + + return $this; + } + /** * Populate Data */ - public function populate(array $rowData, bool $rowExistsInDatabase = false): RowGatewayInterface + public function populate(array $rowData, bool $rowExistsInDatabase = false): static { $this->initialize(); @@ -80,19 +178,9 @@ public function populate(array $rowData, bool $rowExistsInDatabase = false): Row return $this; } - /** - * docs: Behaviour has changed - this no longer returns RowGatewayInterface but - * instead an array of the old data as per original PHP spec. - * - * @return array - */ - public function exchangeArray(array $array): array + public function rowExistsInDatabase(): bool { - $oldData = $this->data; - - $this->populate($array, true); - - return $oldData; + return $this->primaryKeyData !== null; } #[Override] @@ -158,92 +246,25 @@ public function save(): int return $rowsAffected; } - #[Override] - public function delete(): int - { - $this->initialize(); - - $where = []; - foreach ($this->primaryKeyColumn as $pkColumn) { - $where[$pkColumn] = $this->primaryKeyData[$pkColumn] ?? null; - } - - // @todo determine if we need to do a select to ensure 1 row will be affected - - $rowsAffected = 0; - $statement = $this->sql->prepareStatementForSqlObject($this->sql->delete()->where($where)); - $result = $statement->execute(); - - $rowsAffected = $result->getAffectedRows(); - if ($rowsAffected === 1) { - $this->primaryKeyData = null; - } - - return $rowsAffected; - } - - /** - * Offset Exists - * - * @param string $offset - */ - #[Override] - #[ReturnTypeWillChange] - public function offsetExists($offset): bool - { - return array_key_exists($offset, $this->data); - } - - /** - * Offset get - * - * @param string $offset - */ - #[Override] - #[ReturnTypeWillChange] - public function offsetGet($offset): mixed - { - return $this->data[$offset]; - } - - /** - * Offset set - * - * @param string $offset - */ - #[Override] - #[ReturnTypeWillChange] - public function offsetSet($offset, mixed $value): static + public function toArray(): array { - $this->data[$offset] = $value; - - return $this; + return $this->data; } /** - * Offset unset - * - * @param string $offset + * @throws Exception\RuntimeException */ - #[Override] - #[ReturnTypeWillChange] - public function offsetUnset($offset): static - { - $this->data[$offset] = null; - - return $this; - } - - #[Override] - #[ReturnTypeWillChange] - public function count(): int - { - return count($this->data); - } - - public function toArray(): array + protected function processPrimaryKeyData(): void { - return $this->data; + $this->primaryKeyData = []; + foreach ($this->primaryKeyColumn as $column) { + if (! isset($this->data[$column])) { + throw new Exception\RuntimeException( + 'While processing primary key data, a known key ' . $column . ' was not found in the data array', + ); + } + $this->primaryKeyData[$column] = $this->data[$column]; + } } /** @@ -257,39 +278,18 @@ public function __get(string $name): mixed throw new Exception\InvalidArgumentException('Not a valid column in this row: ' . $name); } - public function __set(string $name, mixed $value): void - { - $this->offsetSet($name, $value); - } - public function __isset(string $name): bool { return $this->offsetExists($name); } - public function __unset(string $name): void - { - $this->offsetUnset($name); - } - - public function rowExistsInDatabase(): bool + public function __set(string $name, mixed $value): void { - return $this->primaryKeyData !== null; + $this->offsetSet($name, $value); } - /** - * @throws Exception\RuntimeException - */ - protected function processPrimaryKeyData(): void + public function __unset(string $name): void { - $this->primaryKeyData = []; - foreach ($this->primaryKeyColumn as $column) { - if (! isset($this->data[$column])) { - throw new Exception\RuntimeException( - 'While processing primary key data, a known key ' . $column . ' was not found in the data array' - ); - } - $this->primaryKeyData[$column] = $this->data[$column]; - } + $this->offsetUnset($name); } } diff --git a/src/TableGateway/Feature/RowGatewayFeature.php b/src/TableGateway/Feature/RowGatewayFeature.php index 5442ca3b..29ae3da4 100644 --- a/src/TableGateway/Feature/RowGatewayFeature.php +++ b/src/TableGateway/Feature/RowGatewayFeature.php @@ -4,7 +4,7 @@ namespace PhpDb\TableGateway\Feature; -use PhpDb\ResultSet\ResultSet; +use PhpDb\ResultSet\RowPrototypeResultSet; use PhpDb\RowGateway\RowGateway; use PhpDb\RowGateway\RowGatewayInterface; use PhpDb\TableGateway\Exception; @@ -25,12 +25,15 @@ public function postInitialize(): void { $args = $this->constructorArguments; - /** @var ResultSet $resultSetPrototype */ + /** @var RowPrototypeResultSet $resultSetPrototype */ $resultSetPrototype = $this->tableGateway->resultSetPrototype; - if (! $this->tableGateway->resultSetPrototype instanceof ResultSet) { + if (! $this->tableGateway->resultSetPrototype instanceof RowPrototypeResultSet) { throw new Exception\RuntimeException( - 'This feature ' . self::class . ' expects the ResultSet to be an instance of ' . ResultSet::class + 'This feature ' + . self::class + . ' expects the ResultSet to be an instance of ' + . RowPrototypeResultSet::class, ); } @@ -40,31 +43,31 @@ public function postInitialize(): void $rowGatewayPrototype = new RowGateway( $primaryKey, $this->tableGateway->table, - $this->tableGateway->adapter + $this->tableGateway->adapter, ); - $resultSetPrototype->setArrayObjectPrototype($rowGatewayPrototype); + $resultSetPrototype->setRowPrototype($rowGatewayPrototype); } elseif ($args[0] instanceof RowGatewayInterface) { $rowGatewayPrototype = $args[0]; - $resultSetPrototype->setArrayObjectPrototype($rowGatewayPrototype); + $resultSetPrototype->setRowPrototype($rowGatewayPrototype); } } else { // get from metadata feature $metadata = $this->tableGateway->featureSet->getFeatureByClassName( - MetadataFeature::class + MetadataFeature::class, ); if ($metadata === null || ! 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.' + . '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 + $this->tableGateway->adapter, ); - $resultSetPrototype->setArrayObjectPrototype($rowGatewayPrototype); + $resultSetPrototype->setRowPrototype($rowGatewayPrototype); } } } diff --git a/test/unit/TableGateway/Feature/RowGatewayFeatureTest.php b/test/unit/TableGateway/Feature/RowGatewayFeatureTest.php index 97ae4325..d7f690a1 100644 --- a/test/unit/TableGateway/Feature/RowGatewayFeatureTest.php +++ b/test/unit/TableGateway/Feature/RowGatewayFeatureTest.php @@ -5,8 +5,8 @@ namespace PhpDbTest\TableGateway\Feature; use PhpDb\Adapter\AdapterInterface; -use PhpDb\ResultSet\ResultSet; use PhpDb\ResultSet\ResultSetInterface; +use PhpDb\ResultSet\RowPrototypeResultSet; use PhpDb\RowGateway\RowGatewayInterface; use PhpDb\TableGateway\AbstractTableGateway; use PhpDb\TableGateway\Exception\RuntimeException; @@ -19,63 +19,40 @@ class RowGatewayFeatureTest extends TestCase { - private function createTableGatewayMock( - ResultSetInterface $resultSetPrototype, - ?FeatureSet $featureSet = null - ): AbstractTableGateway&MockObject { - /** @var AbstractTableGateway&MockObject $tableGateway */ - $tableGateway = $this->getMockBuilder(AbstractTableGateway::class) - ->disableOriginalConstructor() - ->getMock(); - - $adapter = $this->createMock(AdapterInterface::class); - - $tableProperty = new ReflectionProperty(AbstractTableGateway::class, 'table'); - $tableProperty->setValue($tableGateway, 'test_table'); - - $adapterProperty = new ReflectionProperty(AbstractTableGateway::class, 'adapter'); - $adapterProperty->setValue($tableGateway, $adapter); - - $resultSetProperty = new ReflectionProperty(AbstractTableGateway::class, 'resultSetPrototype'); - $resultSetProperty->setValue($tableGateway, $resultSetPrototype); - - if ($featureSet !== null) { - $featureSetProperty = new ReflectionProperty(AbstractTableGateway::class, 'featureSet'); - $featureSetProperty->setValue($tableGateway, $featureSet); - } - - return $tableGateway; - } - - public function testPostInitializeWithStringPrimaryKey(): void + public function testConstructorStoresArguments(): void { - $resultSet = new ResultSet(); - $tableGateway = $this->createTableGatewayMock($resultSet); - $feature = new RowGatewayFeature('id'); - $feature->setTableGateway($tableGateway); - $feature->postInitialize(); + // Use reflection to check the constructorArguments property + $property = new ReflectionProperty(RowGatewayFeature::class, 'constructorArguments'); + $args = $property->getValue($feature); - $prototype = $resultSet->getRowPrototype(); - self::assertInstanceOf(RowGatewayInterface::class, $prototype); + self::assertEquals(['id'], $args); } - public function testPostInitializeWithRowGatewayInstance(): void + public function testConstructorStoresRowGatewayInstance(): void { - $resultSet = new ResultSet(); - /** @var RowGatewayInterface&MockObject $rowGateway */ $rowGateway = $this->createMock(RowGatewayInterface::class); - $tableGateway = $this->createTableGatewayMock($resultSet); - $feature = new RowGatewayFeature($rowGateway); - $feature->setTableGateway($tableGateway); - $feature->postInitialize(); + // Use reflection to check the constructorArguments property + $property = new ReflectionProperty(RowGatewayFeature::class, 'constructorArguments'); + $args = $property->getValue($feature); - self::assertSame($rowGateway, $resultSet->getRowPrototype()); + self::assertSame($rowGateway, $args[0]); + } + + public function testConstructorWithNoArguments(): void + { + $feature = new RowGatewayFeature(); + + // Use reflection to check the constructorArguments property + $property = new ReflectionProperty(RowGatewayFeature::class, 'constructorArguments'); + $args = $property->getValue($feature); + + self::assertEquals([], $args); } public function testPostInitializeThrowsExceptionForNonResultSet(): void @@ -92,20 +69,18 @@ public function testPostInitializeThrowsExceptionForNonResultSet(): void $feature->postInitialize(); } - public function testPostInitializeWithMetadataFeature(): void + public function testPostInitializeThrowsExceptionWhenMetadataHasNoMetadataKey(): void { - $resultSet = new ResultSet(); + $resultSet = $this->createInitialResultSet(); - // Create a MetadataFeature mock with primary key in sharedData + // Create a MetadataFeature mock without the metadata key in sharedData $metadataFeature = $this->getMockBuilder(MetadataFeature::class) ->disableOriginalConstructor() ->getMock(); - // Set sharedData with metadata containing primaryKey + // Set empty sharedData on the metadata feature $sharedDataProperty = new ReflectionProperty(MetadataFeature::class, 'sharedData'); - $sharedDataProperty->setValue($metadataFeature, [ - 'metadata' => ['primaryKey' => 'id'], - ]); + $sharedDataProperty->setValue($metadataFeature, []); $featureSet = $this->createMock(FeatureSet::class); $featureSet->expects($this->once()) @@ -118,15 +93,15 @@ public function testPostInitializeWithMetadataFeature(): void $feature = new RowGatewayFeature(); $feature->setTableGateway($tableGateway); - $feature->postInitialize(); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('No information was provided to the RowGatewayFeature'); - $prototype = $resultSet->getRowPrototype(); - self::assertInstanceOf(RowGatewayInterface::class, $prototype); + $feature->postInitialize(); } public function testPostInitializeThrowsExceptionWhenNoMetadataAndNoPrimaryKey(): void { - $resultSet = new ResultSet(); + $resultSet = $this->createInitialResultSet(); $featureSet = $this->createMock(FeatureSet::class); $featureSet->expects($this->once()) @@ -145,18 +120,20 @@ public function testPostInitializeThrowsExceptionWhenNoMetadataAndNoPrimaryKey() $feature->postInitialize(); } - public function testPostInitializeThrowsExceptionWhenMetadataHasNoMetadataKey(): void + public function testPostInitializeWithMetadataFeature(): void { - $resultSet = new ResultSet(); + $resultSet = $this->createInitialResultSet(); - // Create a MetadataFeature mock without the metadata key in sharedData + // Create a MetadataFeature mock with primary key in sharedData $metadataFeature = $this->getMockBuilder(MetadataFeature::class) ->disableOriginalConstructor() ->getMock(); - // Set empty sharedData on the metadata feature + // Set sharedData with metadata containing primaryKey $sharedDataProperty = new ReflectionProperty(MetadataFeature::class, 'sharedData'); - $sharedDataProperty->setValue($metadataFeature, []); + $sharedDataProperty->setValue($metadataFeature, [ + 'metadata' => ['primaryKey' => 'id'], + ]); $featureSet = $this->createMock(FeatureSet::class); $featureSet->expects($this->once()) @@ -169,45 +146,76 @@ public function testPostInitializeThrowsExceptionWhenMetadataHasNoMetadataKey(): $feature = new RowGatewayFeature(); $feature->setTableGateway($tableGateway); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('No information was provided to the RowGatewayFeature'); - $feature->postInitialize(); + + $prototype = $resultSet->getRowPrototype(); + self::assertInstanceOf(RowGatewayInterface::class, $prototype); } - public function testConstructorStoresArguments(): void + public function testPostInitializeWithRowGatewayInstance(): void { - $feature = new RowGatewayFeature('id'); + $resultSet = $this->createInitialResultSet(); - // Use reflection to check the constructorArguments property - $property = new ReflectionProperty(RowGatewayFeature::class, 'constructorArguments'); - $args = $property->getValue($feature); + /** @var RowGatewayInterface&MockObject $rowGateway */ + $rowGateway = $this->createMock(RowGatewayInterface::class); - self::assertEquals(['id'], $args); + $tableGateway = $this->createTableGatewayMock($resultSet); + + $feature = new RowGatewayFeature($rowGateway); + $feature->setTableGateway($tableGateway); + + $feature->postInitialize(); + + self::assertSame($rowGateway, $resultSet->getRowPrototype()); } - public function testConstructorStoresRowGatewayInstance(): void + public function testPostInitializeWithStringPrimaryKey(): void { - /** @var RowGatewayInterface&MockObject $rowGateway */ - $rowGateway = $this->createMock(RowGatewayInterface::class); + $resultSet = $this->createInitialResultSet(); + $tableGateway = $this->createTableGatewayMock($resultSet); - $feature = new RowGatewayFeature($rowGateway); + $feature = new RowGatewayFeature('id'); + $feature->setTableGateway($tableGateway); - // Use reflection to check the constructorArguments property - $property = new ReflectionProperty(RowGatewayFeature::class, 'constructorArguments'); - $args = $property->getValue($feature); + $feature->postInitialize(); - self::assertSame($rowGateway, $args[0]); + $prototype = $resultSet->getRowPrototype(); + self::assertInstanceOf(RowGatewayInterface::class, $prototype); } - public function testConstructorWithNoArguments(): void + /** + * RowPrototypeResultSet requires a prototype up front; postInitialize() always replaces it. + */ + private function createInitialResultSet(): RowPrototypeResultSet { - $feature = new RowGatewayFeature(); + return new RowPrototypeResultSet($this->createMock(RowGatewayInterface::class)); + } - // Use reflection to check the constructorArguments property - $property = new ReflectionProperty(RowGatewayFeature::class, 'constructorArguments'); - $args = $property->getValue($feature); + private function createTableGatewayMock( + ResultSetInterface $resultSetPrototype, + ?FeatureSet $featureSet = null, + ): AbstractTableGateway&MockObject { + /** @var AbstractTableGateway&MockObject $tableGateway */ + $tableGateway = $this->getMockBuilder(AbstractTableGateway::class) + ->disableOriginalConstructor() + ->getMock(); - self::assertEquals([], $args); + $adapter = $this->createMock(AdapterInterface::class); + + $tableProperty = new ReflectionProperty(AbstractTableGateway::class, 'table'); + $tableProperty->setValue($tableGateway, 'test_table'); + + $adapterProperty = new ReflectionProperty(AbstractTableGateway::class, 'adapter'); + $adapterProperty->setValue($tableGateway, $adapter); + + $resultSetProperty = new ReflectionProperty(AbstractTableGateway::class, 'resultSetPrototype'); + $resultSetProperty->setValue($tableGateway, $resultSetPrototype); + + if ($featureSet !== null) { + $featureSetProperty = new ReflectionProperty(AbstractTableGateway::class, 'featureSet'); + $featureSetProperty->setValue($tableGateway, $featureSet); + } + + return $tableGateway; } }