From a6cddcc2d2ca4fd33e3631d548e2a6ffa7434f8b Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Fri, 7 Aug 2026 12:58:49 -0500 Subject: [PATCH 1/5] Split ResultSetInterface into capability interfaces - Trim ResultSetInterface down to Iterator/Countable/initialize/getFieldCount/toArray - Add standalone ArrayObjectResultSetInterface and HydratingResultSetInterface capability interfaces (combined via implements + intersection types, not inheritance), so setRowPrototype()/getRowPrototype() no longer force a wide ArrayObject|RowPrototypeInterface union onto every implementation - ResultSet narrows to ArrayObject-only; HydratingResultSet keeps its intentionally wide object typing, now isolated to its own interface - Move toArray() out of AbstractResultSet; each concrete class implements only the row-casting logic it actually needs --- src/ResultSet/AbstractResultSet.php | 48 ++---------------- .../ArrayObjectResultSetInterface.php | 17 +++++++ src/ResultSet/HydratingResultSet.php | 6 +-- src/ResultSet/HydratingResultSetInterface.php | 15 ++++++ src/ResultSet/ResultSet.php | 28 ++++++++--- src/ResultSet/ResultSetInterface.php | 17 +------ .../AbstractResultSetIntegrationTest.php | 2 +- test/unit/ResultSet/AbstractResultSetTest.php | 50 +------------------ .../ResultSet/ResultSetIntegrationTest.php | 20 +------- 9 files changed, 65 insertions(+), 138 deletions(-) create mode 100644 src/ResultSet/ArrayObjectResultSetInterface.php create mode 100644 src/ResultSet/HydratingResultSetInterface.php diff --git a/src/ResultSet/AbstractResultSet.php b/src/ResultSet/AbstractResultSet.php index af98fb3b..ca236318 100644 --- a/src/ResultSet/AbstractResultSet.php +++ b/src/ResultSet/AbstractResultSet.php @@ -17,10 +17,7 @@ use function count; use function current; -use function gettype; use function is_array; -use function is_object; -use function method_exists; use function reset; abstract class AbstractResultSet implements ResultSetInterface @@ -49,9 +46,7 @@ public function buffer(): ResultSetInterface { if ($this->buffer === -2) { throw new RuntimeException('Buffering must be enabled before iteration is started'); - } - - if (null === $this->buffer) { + } elseif ($this->buffer === null) { $this->buffer = []; if ($this->dataSource instanceof ResultInterface) { $this->dataSource->rewind(); @@ -68,7 +63,7 @@ public function buffer(): ResultSetInterface #[ReturnTypeWillChange] public function count(): ?int { - if (null !== $this->count) { + if ($this->count !== null) { return $this->count; } @@ -90,7 +85,7 @@ public function current(): array|object|null return $this->dataSource->current(); } - if (null === $this->buffer) { + if ($this->buffer === null) { $this->buffer = -2; // implicitly disable buffering from here on } elseif (is_array($this->buffer) && isset($this->buffer[$this->position])) { return $this->buffer[$this->position]; @@ -175,7 +170,7 @@ public function initialize(iterable $dataSource): ResultSetInterface // its safe to get numbers from an array $first = current($dataSource); reset($dataSource); - $this->fieldCount = false === $first ? 0 : count($first); + $this->fieldCount = $first === false ? 0 : count($first); $this->dataSource = new ArrayIterator($dataSource); $this->buffer = -1; // array's are a natural buffer } elseif ($dataSource instanceof IteratorAggregate) { @@ -209,7 +204,7 @@ public function key(): int #[Override] public function next(): void { - if (null === $this->buffer) { + if ($this->buffer === null) { $this->buffer = -2; // implicitly disable buffering from here on } @@ -233,39 +228,6 @@ public function rewind(): void $this->position = 0; } - /** - * Cast result set to array of arrays - * - * @throws RuntimeException If any row is not castable to an array. - */ - #[Override] - public function toArray(): array - { - $return = []; - foreach ($this as $row) { - if (is_array($row)) { - $return[] = $row; - continue; - } - - if ( - ! is_object($row) - || ( - ! method_exists($row, 'toArray') - && ! method_exists($row, 'getArrayCopy') - ) - ) { - throw new RuntimeException( - 'Rows as part of this DataSource, with type ' . gettype($row) . ' cannot be cast to an array', - ); - } - - $return[] = method_exists($row, 'toArray') ? $row->toArray() : $row->getArrayCopy(); - } - - return $return; - } - /** * Iterator: is pointer valid? */ diff --git a/src/ResultSet/ArrayObjectResultSetInterface.php b/src/ResultSet/ArrayObjectResultSetInterface.php new file mode 100644 index 00000000..f73b82ce --- /dev/null +++ b/src/ResultSet/ArrayObjectResultSetInterface.php @@ -0,0 +1,17 @@ +buffer) { + if ($this->buffer === null) { $this->buffer = -2; // implicitly disable buffering from here on } elseif (is_array($this->buffer) && isset($this->buffer[$this->position])) { return $this->buffer[$this->position]; @@ -77,7 +77,7 @@ public function setObjectPrototype(object $objectPrototype): ResultSetInterface /** {@inheritDoc} */ #[Override] - public function setRowPrototype(object $rowPrototype): ResultSetInterface + public function setRowPrototype(object $rowPrototype): ResultSetInterface&HydratingResultSetInterface { $this->rowPrototype = $rowPrototype; return $this; diff --git a/src/ResultSet/HydratingResultSetInterface.php b/src/ResultSet/HydratingResultSetInterface.php new file mode 100644 index 00000000..011a90b5 --- /dev/null +++ b/src/ResultSet/HydratingResultSetInterface.php @@ -0,0 +1,15 @@ +returnType && is_array($data)) { + if ($this->returnType === ResultSetReturnType::ArrayObject && is_array($data)) { $ao = clone $this->getRowPrototype(); $ao->exchangeArray($data); @@ -49,7 +49,7 @@ public function current(): array|ArrayObject|RowPrototypeInterface|null /** * @deprecated use getRowPrototype() */ - public function getArrayObjectPrototype(): ArrayObject|RowPrototypeInterface + public function getArrayObjectPrototype(): ArrayObject { return $this->getRowPrototype(); } @@ -64,7 +64,7 @@ public function getReturnType(): ResultSetReturnType /** {@inheritDoc} */ #[Override] - public function getRowPrototype(): ArrayObject|RowPrototypeInterface + public function getRowPrototype(): ArrayObject { return $this->rowPrototype; } @@ -74,17 +74,29 @@ public function getRowPrototype(): ArrayObject|RowPrototypeInterface * * @deprecated use setRowPrototype() */ - public function setArrayObjectPrototype(ArrayObject|RowPrototypeInterface $arrayObjectPrototype): ResultSetInterface + public function setArrayObjectPrototype(ArrayObject $arrayObjectPrototype): ResultSetInterface&ArrayObjectResultSetInterface { return $this->setRowPrototype($arrayObjectPrototype); } /** {@inheritDoc} */ #[Override] - public function setRowPrototype(ArrayObject|RowPrototypeInterface $rowPrototype): ResultSetInterface + public function setRowPrototype(ArrayObject $rowPrototype): ResultSetInterface&ArrayObjectResultSetInterface { $this->rowPrototype = $rowPrototype; return $this; } + + /** {@inheritDoc} */ + #[Override] + public function toArray(): array + { + $return = []; + foreach ($this as $row) { + $return[] = $row instanceof ArrayObject ? $row->getArrayCopy() : $row; + } + + return $return; + } } diff --git a/src/ResultSet/ResultSetInterface.php b/src/ResultSet/ResultSetInterface.php index bf6e9483..dc0a3525 100644 --- a/src/ResultSet/ResultSetInterface.php +++ b/src/ResultSet/ResultSetInterface.php @@ -4,7 +4,6 @@ namespace PhpDb\ResultSet; -use ArrayObject; use Countable; use Iterator; @@ -17,27 +16,13 @@ interface ResultSetInterface extends Iterator, Countable */ public function getFieldCount(): int; - /** - * Get the row object prototype - */ - public function getRowPrototype(): ?object; - /** * Can be anything iterable|array */ - public function initialize(iterable $dataSource): self; - - /** - * Set the row object prototype - * - * @throws Exception\InvalidArgumentException - */ - public function setRowPrototype(ArrayObject|RowPrototypeInterface $rowPrototype): self; + public function initialize(iterable $dataSource): ResultSetInterface; /** * Get all rows as an array - * - * @return RowPrototypeInterface[]|ArrayObject[]|array[] */ public function toArray(): array; } diff --git a/test/unit/ResultSet/AbstractResultSetIntegrationTest.php b/test/unit/ResultSet/AbstractResultSetIntegrationTest.php index 3cde3079..df7045cb 100644 --- a/test/unit/ResultSet/AbstractResultSetIntegrationTest.php +++ b/test/unit/ResultSet/AbstractResultSetIntegrationTest.php @@ -58,7 +58,7 @@ public function testCurrentCallsDataSourceCurrentOnceWithBuffer(): void protected function setUp(): void { $this->resultSet = $this->getMockBuilder(AbstractResultSet::class) - ->onlyMethods(['setRowPrototype', 'getRowPrototype']) + ->onlyMethods(['toArray']) ->getMock(); } } diff --git a/test/unit/ResultSet/AbstractResultSetTest.php b/test/unit/ResultSet/AbstractResultSetTest.php index 208d6b62..fc1401b7 100644 --- a/test/unit/ResultSet/AbstractResultSetTest.php +++ b/test/unit/ResultSet/AbstractResultSetTest.php @@ -19,7 +19,6 @@ use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use stdClass; use TypeError; use function assert; @@ -35,7 +34,6 @@ #[CoversMethod(AbstractResultSet::class, 'valid')] #[CoversMethod(AbstractResultSet::class, 'rewind')] #[CoversMethod(AbstractResultSet::class, 'count')] -#[CoversMethod(AbstractResultSet::class, 'toArray')] final class AbstractResultSetTest extends TestCase { protected MockObject|AbstractResultSet $resultSet; @@ -387,7 +385,7 @@ public function testMultipleRewindBufferIterations(): void assert($stub instanceof PDOStatement); // to suppress IDE type warnings $stub->expects($this->any()) ->method('fetch') - ->willReturnCallback(static function () use ($data) { + ->willReturnCallback(function () use ($data) { $r = $data->current(); $data->next(); return $r; @@ -482,50 +480,6 @@ public function getIterator(): ArrayIterator self::assertSame(0, $resultSet->key()); } - /** - * @throws Exception - */ - public function testToArray(): void - { - $resultSet = $this->createResultSetMock(); - $resultSet->initialize(new ArrayIterator([ - ['id' => 1, 'name' => 'one'], - ['id' => 2, 'name' => 'two'], - ['id' => 3, 'name' => 'three'], - ])); - // Verify toArray() returns all rows as array - self::assertEquals( - [ - ['id' => 1, 'name' => 'one'], - ['id' => 2, 'name' => 'two'], - ['id' => 3, 'name' => 'three'], - ], - $resultSet->toArray(), - ); - } - - public function testToArrayConvertsArrayObjectsViaGetArrayCopy(): void - { - $resultSet = $this->createResultSetMock(); - $resultSet->initialize([ - new ArrayObject(['id' => 1, 'name' => 'one']), - ]); - - $result = $resultSet->toArray(); - - self::assertSame([['id' => 1, 'name' => 'one']], $result); - } - - public function testToArrayThrowsOnNonCastableRows(): void - { - $resultSet = $this->createResultSetMock(); - $resultSet->initialize(new ArrayIterator([new stdClass()])); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('cannot be cast to an array'); - $resultSet->toArray(); - } - /** * @throws Exception */ @@ -587,7 +541,7 @@ protected function setUp(): void private function createResultSetMock(): MockObject|AbstractResultSet { return $this->getMockBuilder(AbstractResultSet::class) - ->onlyMethods(['setRowPrototype', 'getRowPrototype']) + ->onlyMethods(['toArray']) ->getMock(); } } diff --git a/test/unit/ResultSet/ResultSetIntegrationTest.php b/test/unit/ResultSet/ResultSetIntegrationTest.php index e5f63094..8f2aaa31 100644 --- a/test/unit/ResultSet/ResultSetIntegrationTest.php +++ b/test/unit/ResultSet/ResultSetIntegrationTest.php @@ -56,7 +56,7 @@ public function getArrayDataSource(int $count): ArrayIterator for ($i = 0; $i < $count; $i++) { $array[] = [ 'id' => $i, - 'title' => "title {$i}", + 'title' => 'title ' . $i, ]; } @@ -323,24 +323,6 @@ public function testToArrayCreatesArrayOfArraysRepresentingRows(): void self::assertEquals($dataSource->getArrayCopy(), $test, var_export($test, true)); } - /** - * @throws RandomException - * @throws \Exception - */ - public function testToArrayRaisesExceptionForRowsThatAreNotArraysOrArrayCastable(): void - { - $count = random_int(3, 75); - $dataSource = $this->getArrayDataSource($count); - foreach ($dataSource as $index => $row) { - $dataSource[$index] = (object) $row; - } - - // Verify toArray() throws exception for non-array-castable objects - $this->resultSet->initialize($dataSource); - $this->expectException(RuntimeException::class); - $this->resultSet->toArray(); - } - /** * @throws \Exception */ From 0b830fecca72fc10bf87a48ce05e52ea25e3e5b4 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Fri, 7 Aug 2026 12:59:12 -0500 Subject: [PATCH 2/5] Add ArrayResultSet Dedicated ResultSet implementation for plain-array rows, decoupled from ArrayObjectResultSetInterface's row-prototype capability entirely. --- src/ResultSet/ArrayResultSet.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/ResultSet/ArrayResultSet.php diff --git a/src/ResultSet/ArrayResultSet.php b/src/ResultSet/ArrayResultSet.php new file mode 100644 index 00000000..2f93eb4e --- /dev/null +++ b/src/ResultSet/ArrayResultSet.php @@ -0,0 +1,22 @@ + Date: Fri, 7 Aug 2026 12:59:35 -0500 Subject: [PATCH 3/5] Add RowPrototypeResultSet; migrate RowGateway to populate() - New RowPrototypeResultSet/RowPrototypeResultSetInterface: dedicated, ArrayObject-free ResultSet implementation for RowPrototypeInterface prototypes (e.g. RowGateway) - Rename RowPrototypeInterface::exchangeArray() to populate(), returning RowPrototypeInterface. Avoids the ArrayObject-associated vocabulary bleeding across the ArrayObjectResultSet/RowPrototypeResultSet split; whether population is mutating or returns a new instance is left as an implementation detail of the prototype, not the ResultSet - AbstractRowGateway::populate() return type widened to static to satisfy RowGatewayInterface's new populate() contract (behavior unchanged, it already returned $this) - RowGatewayFeature migrated from ResultSet (now ArrayObject-only) to RowPrototypeResultSet, calling setRowPrototype() directly --- src/ResultSet/RowPrototypeInterface.php | 10 ++-- src/ResultSet/RowPrototypeResultSet.php | 59 +++++++++++++++++++ .../RowPrototypeResultSetInterface.php | 15 +++++ src/RowGateway/AbstractRowGateway.php | 26 ++++---- .../Feature/RowGatewayFeature.php | 20 ++++--- .../Feature/RowGatewayFeatureTest.php | 22 ++++--- 6 files changed, 117 insertions(+), 35 deletions(-) create mode 100644 src/ResultSet/RowPrototypeResultSet.php create mode 100644 src/ResultSet/RowPrototypeResultSetInterface.php 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 @@ +execute(); $rowsAffected = $result->getAffectedRows(); - if (1 === $rowsAffected) { + if ($rowsAffected === 1) { $this->primaryKeyData = null; } @@ -67,7 +67,6 @@ public function delete(): int * * @return array */ - #[Override] public function exchangeArray(array $array): array { $oldData = $this->data; @@ -93,15 +92,15 @@ public function initialize(): void $this->featureSet->setRowGateway($this); $this->featureSet->apply('preInitialize', []); - if (null === $this->table) { + if ($this->table === null) { throw new Exception\RuntimeException('This row object does not have a valid table set.'); } - if (null === $this->primaryKeyColumn) { + if ($this->primaryKeyColumn === null) { throw new Exception\RuntimeException('This row object does not have a primary key column set.'); } - if (null === $this->sql) { + if ($this->sql === null) { throw new Exception\RuntimeException('This row object does not have a Sql object set.'); } @@ -165,12 +164,12 @@ public function offsetUnset($offset): static /** * Populate Data */ - public function populate(array $rowData, bool $rowExistsInDatabase = false): RowGatewayInterface + public function populate(array $rowData, bool $rowExistsInDatabase = false): static { $this->initialize(); $this->data = $rowData; - if (true === $rowExistsInDatabase) { + if ($rowExistsInDatabase === true) { $this->processPrimaryKeyData(); } else { $this->primaryKeyData = null; @@ -181,7 +180,7 @@ public function populate(array $rowData, bool $rowExistsInDatabase = false): Row public function rowExistsInDatabase(): bool { - return null !== $this->primaryKeyData; + return $this->primaryKeyData !== null; } #[Override] @@ -212,11 +211,9 @@ public function save(): int if ($isPkModified) { foreach ($this->primaryKeyColumn as $pkColumn) { - if ($data[$pkColumn] === $this->primaryKeyData[$pkColumn]) { - continue; + if ($data[$pkColumn] !== $this->primaryKeyData[$pkColumn]) { + $where[$pkColumn] = $data[$pkColumn]; } - - $where[$pkColumn] = $data[$pkColumn]; } } } else { @@ -249,7 +246,6 @@ public function save(): int return $rowsAffected; } - #[Override] public function toArray(): array { return $this->data; @@ -264,7 +260,7 @@ protected function processPrimaryKeyData(): void 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", + 'While processing primary key data, a known key ' . $column . ' was not found in the data array', ); } $this->primaryKeyData[$column] = $this->data[$column]; @@ -279,7 +275,7 @@ public function __get(string $name): mixed if (array_key_exists($name, $this->data)) { return $this->data[$name]; } - throw new Exception\InvalidArgumentException("Not a valid column in this row: {$name}"); + throw new Exception\InvalidArgumentException('Not a valid column in this row: ' . $name); } public function __isset(string $name): bool diff --git a/src/TableGateway/Feature/RowGatewayFeature.php b/src/TableGateway/Feature/RowGatewayFeature.php index 95cc8161..7a224145 100644 --- a/src/TableGateway/Feature/RowGatewayFeature.php +++ b/src/TableGateway/Feature/RowGatewayFeature.php @@ -4,10 +4,11 @@ namespace PhpDb\TableGateway\Feature; -use PhpDb\ResultSet\ResultSet; +use PhpDb\ResultSet\RowPrototypeResultSet; use PhpDb\RowGateway\RowGateway; use PhpDb\RowGateway\RowGatewayInterface; use PhpDb\TableGateway\Exception; +use PhpDb\TableGateway\Feature\MetadataFeature; use function is_string; @@ -24,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, ); } @@ -41,17 +45,17 @@ public function postInitialize(): void $this->tableGateway->table, $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, ); - if (null === $metadata || ! isset($metadata->sharedData['metadata'])) { + 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.', @@ -63,7 +67,7 @@ public function postInitialize(): void $this->tableGateway->table, $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 7fdbb033..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; @@ -71,7 +71,7 @@ public function testPostInitializeThrowsExceptionForNonResultSet(): void public function testPostInitializeThrowsExceptionWhenMetadataHasNoMetadataKey(): void { - $resultSet = new ResultSet(); + $resultSet = $this->createInitialResultSet(); // Create a MetadataFeature mock without the metadata key in sharedData $metadataFeature = $this->getMockBuilder(MetadataFeature::class) @@ -101,7 +101,7 @@ public function testPostInitializeThrowsExceptionWhenMetadataHasNoMetadataKey(): public function testPostInitializeThrowsExceptionWhenNoMetadataAndNoPrimaryKey(): void { - $resultSet = new ResultSet(); + $resultSet = $this->createInitialResultSet(); $featureSet = $this->createMock(FeatureSet::class); $featureSet->expects($this->once()) @@ -122,7 +122,7 @@ public function testPostInitializeThrowsExceptionWhenNoMetadataAndNoPrimaryKey() public function testPostInitializeWithMetadataFeature(): void { - $resultSet = new ResultSet(); + $resultSet = $this->createInitialResultSet(); // Create a MetadataFeature mock with primary key in sharedData $metadataFeature = $this->getMockBuilder(MetadataFeature::class) @@ -154,7 +154,7 @@ public function testPostInitializeWithMetadataFeature(): void public function testPostInitializeWithRowGatewayInstance(): void { - $resultSet = new ResultSet(); + $resultSet = $this->createInitialResultSet(); /** @var RowGatewayInterface&MockObject $rowGateway */ $rowGateway = $this->createMock(RowGatewayInterface::class); @@ -171,7 +171,7 @@ public function testPostInitializeWithRowGatewayInstance(): void public function testPostInitializeWithStringPrimaryKey(): void { - $resultSet = new ResultSet(); + $resultSet = $this->createInitialResultSet(); $tableGateway = $this->createTableGatewayMock($resultSet); $feature = new RowGatewayFeature('id'); @@ -183,6 +183,14 @@ public function testPostInitializeWithStringPrimaryKey(): void self::assertInstanceOf(RowGatewayInterface::class, $prototype); } + /** + * RowPrototypeResultSet requires a prototype up front; postInitialize() always replaces it. + */ + private function createInitialResultSet(): RowPrototypeResultSet + { + return new RowPrototypeResultSet($this->createMock(RowGatewayInterface::class)); + } + private function createTableGatewayMock( ResultSetInterface $resultSetPrototype, ?FeatureSet $featureSet = null, @@ -203,7 +211,7 @@ private function createTableGatewayMock( $resultSetProperty = new ReflectionProperty(AbstractTableGateway::class, 'resultSetPrototype'); $resultSetProperty->setValue($tableGateway, $resultSetPrototype); - if (null !== $featureSet) { + if ($featureSet !== null) { $featureSetProperty = new ReflectionProperty(AbstractTableGateway::class, 'featureSet'); $featureSetProperty->setValue($tableGateway, $featureSet); } From 075961edfd34569c701210e67242133fba037a01 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Sun, 23 Aug 2026 19:44:19 -0500 Subject: [PATCH 4/5] Regenerate mago lint and analyzer baselines --- analyzer-baseline.toml | 122 +++++++++++++++++++++++++----------- lint-baseline.toml | 138 +++++++++++++++++++++++++++++++---------- 2 files changed, 190 insertions(+), 70 deletions(-) diff --git a/analyzer-baseline.toml b/analyzer-baseline.toml index 3e000633..26938f84 100644 --- a/analyzer-baseline.toml +++ b/analyzer-baseline.toml @@ -1524,12 +1524,6 @@ code = "missing-constant-type" message = "Class constant `DEFAULT_SCHEMA` is missing a type hint." count = 1 -[[issues]] -file = "src/ResultSet/AbstractResultSet.php" -code = "ambiguous-object-method-access" -message = "Cannot statically verify method call on a generic `object` type." -count = 3 - [[issues]] file = "src/ResultSet/AbstractResultSet.php" code = "imprecise-type" @@ -1566,12 +1560,6 @@ code = "invalid-type-cast" message = "Casting `mixed` to `array`." count = 1 -[[issues]] -file = "src/ResultSet/AbstractResultSet.php" -code = "less-specific-nested-return-statement" -message = '''Returned type `array{}|non-empty-list` is less specific than the declared return type `array|PhpDb\ResultSet\RowPrototypeInterface|array>` for function `PhpDb\ResultSet\AbstractResultSet::toArray` due to nested 'mixed'.''' -count = 1 - [[issues]] file = "src/ResultSet/AbstractResultSet.php" code = "missing-api-or-internal" @@ -1656,6 +1644,24 @@ code = "property-type-coercion" message = 'A value of a less specific type `Traversable` is being assigned to property `$dataSource` (Iterator|IteratorAggregate|PhpDb\Adapter\Driver\ResultInterface|null).' count = 1 +[[issues]] +file = "src/ResultSet/ArrayObjectResultSetInterface.php" +code = "missing-api-or-internal" +message = 'Interface `PhpDb\ResultSet\ArrayObjectResultSetInterface` is missing an `@api` or `@internal` annotation.' +count = 1 + +[[issues]] +file = "src/ResultSet/ArrayResultSet.php" +code = "class-must-be-final" +message = 'Class `PhpDb\ResultSet\ArrayResultSet` should be declared `final`.' +count = 1 + +[[issues]] +file = "src/ResultSet/ArrayResultSet.php" +code = "imprecise-type" +message = "Type `array` in return type of `toArray` is imprecise, equivalent to `array`." +count = 1 + [[issues]] file = "src/ResultSet/Exception/ExceptionInterface.php" code = "missing-api-or-internal" @@ -1682,8 +1688,8 @@ count = 1 [[issues]] file = "src/ResultSet/HydratingResultSet.php" -code = "less-specific-argument" -message = 'Argument type mismatch for argument #1 of `PhpDb\ResultSet\HydratingResultSet::setRowPrototype`: expected `ArrayObject|PhpDb\ResultSet\RowPrototypeInterface`, but provided type `object` is less specific.' +code = "imprecise-type" +message = "Type `array` in return type of `toArray` is imprecise, equivalent to `array`." count = 1 [[issues]] @@ -1722,6 +1728,12 @@ code = "possibly-null-argument" message = 'Argument #1 of method `Laminas\Hydrator\ExtractionInterface::extract` is possibly `null`, but parameter type `object` does not accept it.' count = 1 +[[issues]] +file = "src/ResultSet/HydratingResultSetInterface.php" +code = "missing-api-or-internal" +message = 'Interface `PhpDb\ResultSet\HydratingResultSetInterface` is missing an `@api` or `@internal` annotation.' +count = 1 + [[issues]] file = "src/ResultSet/ResultSet.php" code = "class-must-be-final" @@ -1736,20 +1748,20 @@ count = 1 [[issues]] file = "src/ResultSet/ResultSet.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\ResultSet\ResultSet::getReturnType`: expected `enum(PhpDb\ResultSet\ResultSetReturnType)`, but found `enum(PhpDb\ResultSet\ResultSetReturnType)|string`.' +code = "imprecise-type" +message = "Type `array` in return type of `toArray` is imprecise, equivalent to `array`." count = 1 [[issues]] file = "src/ResultSet/ResultSet.php" code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\ResultSet\ResultSet::getRowPrototype`: expected `ArrayObject|PhpDb\ResultSet\RowPrototypeInterface`, but found `ArrayObject|PhpDb\ResultSet\RowPrototypeInterface|null`.' +message = 'Invalid return type for function `PhpDb\ResultSet\ResultSet::getReturnType`: expected `enum(PhpDb\ResultSet\ResultSetReturnType)`, but found `enum(PhpDb\ResultSet\ResultSetReturnType)|string`.' count = 1 [[issues]] file = "src/ResultSet/ResultSet.php" code = "less-specific-return-statement" -message = 'Returned type `array|null|object` is less specific than the declared return type `ArrayObject|PhpDb\ResultSet\RowPrototypeInterface|array|null` for function `PhpDb\ResultSet\ResultSet::current`.' +message = 'Returned type `array|null|object` is less specific than the declared return type `ArrayObject|array|null` for function `PhpDb\ResultSet\ResultSet::current`.' count = 1 [[issues]] @@ -1765,9 +1777,9 @@ message = "Class constant `TYPE_ARRAY` is missing a type hint." count = 1 [[issues]] -file = "src/ResultSet/ResultSet.php" -code = "nullable-return-statement" -message = 'Function `PhpDb\ResultSet\ResultSet::getRowPrototype` is declared to return `ArrayObject|PhpDb\ResultSet\RowPrototypeInterface` but possibly returns a nullable value (inferred as `ArrayObject|PhpDb\ResultSet\RowPrototypeInterface|null`).' +file = "src/ResultSet/ResultSetInterface.php" +code = "imprecise-type" +message = "Type `array` in return type of `toArray` is imprecise, equivalent to `array`." count = 1 [[issues]] @@ -1791,25 +1803,49 @@ count = 1 [[issues]] file = "src/ResultSet/RowPrototypeInterface.php" code = "imprecise-type" -message = "Type `array` in parameter `$array` is imprecise, equivalent to `array`." +message = "Type `array` in parameter `$data` is imprecise, equivalent to `array`." count = 1 [[issues]] file = "src/ResultSet/RowPrototypeInterface.php" code = "imprecise-type" -message = "Type `array` in return type of `exchangeArray` is imprecise, equivalent to `array`." +message = "Type `array` in return type of `toArray` is imprecise, equivalent to `array`." count = 1 [[issues]] file = "src/ResultSet/RowPrototypeInterface.php" +code = "missing-api-or-internal" +message = 'Interface `PhpDb\ResultSet\RowPrototypeInterface` is missing an `@api` or `@internal` annotation.' +count = 1 + +[[issues]] +file = "src/ResultSet/RowPrototypeResultSet.php" +code = "class-must-be-final" +message = 'Class `PhpDb\ResultSet\RowPrototypeResultSet` should be declared `final`.' +count = 1 + +[[issues]] +file = "src/ResultSet/RowPrototypeResultSet.php" +code = "imprecise-type" +message = "Type `array` in return type of `current` is imprecise, equivalent to `array`." +count = 1 + +[[issues]] +file = "src/ResultSet/RowPrototypeResultSet.php" code = "imprecise-type" message = "Type `array` in return type of `toArray` is imprecise, equivalent to `array`." count = 1 [[issues]] -file = "src/ResultSet/RowPrototypeInterface.php" +file = "src/ResultSet/RowPrototypeResultSet.php" +code = "less-specific-return-statement" +message = 'Returned type `null|object` is less specific than the declared return type `PhpDb\ResultSet\RowPrototypeInterface|array|null` for function `PhpDb\ResultSet\RowPrototypeResultSet::current`.' +count = 1 + +[[issues]] +file = "src/ResultSet/RowPrototypeResultSetInterface.php" code = "missing-api-or-internal" -message = 'Interface `PhpDb\ResultSet\RowPrototypeInterface` is missing an `@api` or `@internal` annotation.' +message = 'Interface `PhpDb\ResultSet\RowPrototypeResultSetInterface` is missing an `@api` or `@internal` annotation.' count = 1 [[issues]] @@ -1848,6 +1884,12 @@ code = "imprecise-type" message = "Type `array` in return type of `toArray` is imprecise, equivalent to `array`." count = 1 +[[issues]] +file = "src/RowGateway/AbstractRowGateway.php" +code = "incompatible-parameter-name" +message = 'Parameter #1 of `PhpDb\RowGateway\AbstractRowGateway::populate()` is named `$rowData` but parent `PhpDb\ResultSet\RowPrototypeInterface::populate()` names it `$data`' +count = 1 + [[issues]] file = "src/RowGateway/AbstractRowGateway.php" code = "incompatible-parameter-type" @@ -1890,6 +1932,18 @@ code = "missing-api-or-internal" message = 'Abstract class `PhpDb\RowGateway\AbstractRowGateway` is missing an `@api` or `@internal` annotation.' count = 1 +[[issues]] +file = "src/RowGateway/AbstractRowGateway.php" +code = "missing-override-attribute" +message = 'Missing `#[Override]` attribute on overriding method `PhpDb\RowGateway\AbstractRowGateway::populate`.' +count = 1 + +[[issues]] +file = "src/RowGateway/AbstractRowGateway.php" +code = "missing-override-attribute" +message = 'Missing `#[Override]` attribute on overriding method `PhpDb\RowGateway\AbstractRowGateway::toArray`.' +count = 1 + [[issues]] file = "src/RowGateway/AbstractRowGateway.php" code = "missing-template-parameter" @@ -1920,6 +1974,12 @@ code = "mixed-method-access" message = "Attempting to access a method on a non-object type (`mixed`)." count = 14 +[[issues]] +file = "src/RowGateway/AbstractRowGateway.php" +code = "mixed-operand" +message = "Invalid middle operand: type `mixed` cannot be reliably used in string concatenation." +count = 1 + [[issues]] file = "src/RowGateway/AbstractRowGateway.php" code = "mixed-operand" @@ -5010,12 +5070,6 @@ code = "missing-constant-type" message = "Class constant `TYPE_VALUE` is missing a type hint." count = 1 -[[issues]] -file = "src/Sql/TableIdentifier.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Sql\TableIdentifier` should be declared `final`.' -count = 1 - [[issues]] file = "src/Sql/Update.php" code = "class-must-be-final" @@ -5988,12 +6042,6 @@ 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" diff --git a/lint-baseline.toml b/lint-baseline.toml index 99046f37..d4aa363a 100644 --- a/lint-baseline.toml +++ b/lint-baseline.toml @@ -258,12 +258,6 @@ code = "halstead" message = "Method has a high halstead difficulty" count = 2 -[[issues]] -file = "src/ResultSet/AbstractResultSet.php" -code = "kan-defect" -message = "Class has a high kan defect score." -count = 1 - [[issues]] file = "src/ResultSet/AbstractResultSet.php" code = "no-else-clause" @@ -274,7 +268,7 @@ count = 1 file = "src/ResultSet/AbstractResultSet.php" code = "no-else-clause" message = "Avoid `elseif` clauses." -count = 2 +count = 3 [[issues]] file = "src/ResultSet/AbstractResultSet.php" @@ -282,6 +276,24 @@ code = "no-isset" message = "Use of the `isset` construct." count = 2 +[[issues]] +file = "src/ResultSet/AbstractResultSet.php" +code = "no-redundant-else" +message = "The `if` branch always terminates; the trailing branches can be extracted." +count = 1 + +[[issues]] +file = "src/ResultSet/AbstractResultSet.php" +code = "yoda-conditions" +message = "Use Yoda condition style for safer comparisons" +count = 5 + +[[issues]] +file = "src/ResultSet/ArrayObjectResultSetInterface.php" +code = "prefer-self-return-type" +message = "Return type `ArrayObjectResultSetInterface` refers to the enclosing class; use `self` instead." +count = 1 + [[issues]] file = "src/ResultSet/HydratingResultSet.php" code = "halstead" @@ -300,6 +312,42 @@ code = "no-isset" message = "Use of the `isset` construct." count = 1 +[[issues]] +file = "src/ResultSet/HydratingResultSet.php" +code = "yoda-conditions" +message = "Use Yoda condition style for safer comparisons" +count = 1 + +[[issues]] +file = "src/ResultSet/HydratingResultSetInterface.php" +code = "prefer-self-return-type" +message = "Return type `HydratingResultSetInterface` refers to the enclosing class; use `self` instead." +count = 1 + +[[issues]] +file = "src/ResultSet/ResultSet.php" +code = "yoda-conditions" +message = "Use Yoda condition style for safer comparisons" +count = 1 + +[[issues]] +file = "src/ResultSet/ResultSetInterface.php" +code = "prefer-self-return-type" +message = "Return type `ResultSetInterface` refers to the enclosing class; use `self` instead." +count = 1 + +[[issues]] +file = "src/ResultSet/RowPrototypeInterface.php" +code = "prefer-self-return-type" +message = "Return type `RowPrototypeInterface` refers to the enclosing class; use `self` instead." +count = 1 + +[[issues]] +file = "src/ResultSet/RowPrototypeResultSetInterface.php" +code = "prefer-self-return-type" +message = "Return type `RowPrototypeResultSetInterface` refers to the enclosing class; use `self` instead." +count = 1 + [[issues]] file = "src/RowGateway/AbstractRowGateway.php" code = "cyclomatic-complexity" @@ -342,12 +390,30 @@ code = "no-isset" message = "Use of the `isset` construct." count = 1 +[[issues]] +file = "src/RowGateway/AbstractRowGateway.php" +code = "prefer-early-continue" +message = "Consider using early continue pattern to reduce nesting." +count = 1 + +[[issues]] +file = "src/RowGateway/AbstractRowGateway.php" +code = "string-style" +message = "String concatenation can be replaced with interpolation." +count = 2 + [[issues]] file = "src/RowGateway/AbstractRowGateway.php" code = "too-many-methods" message = "Class has too many methods." count = 1 +[[issues]] +file = "src/RowGateway/AbstractRowGateway.php" +code = "yoda-conditions" +message = "Use Yoda condition style for safer comparisons" +count = 6 + [[issues]] file = "src/RowGateway/Feature/FeatureSet.php" code = "too-many-methods" @@ -960,6 +1026,18 @@ code = "no-isset" message = "Use of the `isset` construct." count = 2 +[[issues]] +file = "src/TableGateway/Feature/RowGatewayFeature.php" +code = "no-redundant-use" +message = 'Redundant import: `MetadataFeature` is already in the current namespace `PhpDb\TableGateway\Feature`.' +count = 1 + +[[issues]] +file = "src/TableGateway/Feature/RowGatewayFeature.php" +code = "yoda-conditions" +message = "Use Yoda condition style for safer comparisons" +count = 1 + [[issues]] file = "src/TableGateway/Feature/SequenceFeature.php" code = "strict-behavior" @@ -2914,7 +2992,13 @@ count = 1 file = "test/unit/ResultSet/AbstractResultSetTest.php" code = "assertion-style" message = "Inconsistent assertions style." -count = 47 +count = 45 + +[[issues]] +file = "test/unit/ResultSet/AbstractResultSetTest.php" +code = "prefer-static-closure" +message = "This closure does not use `$this` and should be declared static." +count = 1 [[issues]] file = "test/unit/ResultSet/AbstractResultSetTest.php" @@ -3072,24 +3156,6 @@ code = "prefer-test-attribute" message = "Use `#[Test]` attribute instead of `test` prefix on method `testRewindWithNonIteratorDataSource`." count = 1 -[[issues]] -file = "test/unit/ResultSet/AbstractResultSetTest.php" -code = "prefer-test-attribute" -message = "Use `#[Test]` attribute instead of `test` prefix on method `testToArrayConvertsArrayObjectsViaGetArrayCopy`." -count = 1 - -[[issues]] -file = "test/unit/ResultSet/AbstractResultSetTest.php" -code = "prefer-test-attribute" -message = "Use `#[Test]` attribute instead of `test` prefix on method `testToArrayThrowsOnNonCastableRows`." -count = 1 - -[[issues]] -file = "test/unit/ResultSet/AbstractResultSetTest.php" -code = "prefer-test-attribute" -message = "Use `#[Test]` attribute instead of `test` prefix on method `testToArray`." -count = 1 - [[issues]] file = "test/unit/ResultSet/AbstractResultSetTest.php" code = "prefer-test-attribute" @@ -3220,7 +3286,7 @@ count = 27 file = "test/unit/ResultSet/ResultSetIntegrationTest.php" code = "literal-named-argument" message = "Literal argument `75` should be passed as a named argument for clarity." -count = 3 +count = 2 [[issues]] file = "test/unit/ResultSet/ResultSetIntegrationTest.php" @@ -3360,12 +3426,6 @@ code = "prefer-test-attribute" message = "Use `#[Test]` attribute instead of `test` prefix on method `testToArrayCreatesArrayOfArraysRepresentingRows`." count = 1 -[[issues]] -file = "test/unit/ResultSet/ResultSetIntegrationTest.php" -code = "prefer-test-attribute" -message = "Use `#[Test]` attribute instead of `test` prefix on method `testToArrayRaisesExceptionForRowsThatAreNotArraysOrArrayCastable`." -count = 1 - [[issues]] file = "test/unit/ResultSet/ResultSetIntegrationTest.php" code = "prefer-test-attribute" @@ -3384,6 +3444,12 @@ code = "strict-assertions" message = "Use strict assertions in PHPUnit tests." count = 2 +[[issues]] +file = "test/unit/ResultSet/ResultSetIntegrationTest.php" +code = "string-style" +message = "String concatenation can be replaced with interpolation." +count = 1 + [[issues]] file = "test/unit/RowGateway/AbstractRowGatewayTest.php" code = "assertion-style" @@ -9078,6 +9144,12 @@ code = "prefer-test-attribute" message = "Use `#[Test]` attribute instead of `test` prefix on method `testPostInitializeWithStringPrimaryKey`." count = 1 +[[issues]] +file = "test/unit/TableGateway/Feature/RowGatewayFeatureTest.php" +code = "yoda-conditions" +message = "Use Yoda condition style for safer comparisons" +count = 1 + [[issues]] file = "test/unit/TableGateway/Feature/SequenceFeatureTest.php" code = "assertion-style" From 75ab5ca32ae48bc717751f856fb0f7eff7e33d92 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Sun, 23 Aug 2026 20:29:57 -0500 Subject: [PATCH 5/5] Add unit tests and CoversMethod metadata to cover ResultSet patch code --- test/unit/ResultSet/ArrayResultSetTest.php | 34 ++++++++ .../ResultSet/ResultSetIntegrationTest.php | 4 + .../ResultSet/RowPrototypeResultSetTest.php | 83 +++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 test/unit/ResultSet/ArrayResultSetTest.php create mode 100644 test/unit/ResultSet/RowPrototypeResultSetTest.php diff --git a/test/unit/ResultSet/ArrayResultSetTest.php b/test/unit/ResultSet/ArrayResultSetTest.php new file mode 100644 index 00000000..75b46a9c --- /dev/null +++ b/test/unit/ResultSet/ArrayResultSetTest.php @@ -0,0 +1,34 @@ +initialize([ + ['id' => 1, 'name' => 'one'], + ['id' => 2, 'name' => 'two'], + ]); + + static::assertSame( + [ + ['id' => 1, 'name' => 'one'], + ['id' => 2, 'name' => 'two'], + ], + $resultSet->toArray(), + ); + } +} diff --git a/test/unit/ResultSet/ResultSetIntegrationTest.php b/test/unit/ResultSet/ResultSetIntegrationTest.php index 8f2aaa31..bbe05321 100644 --- a/test/unit/ResultSet/ResultSetIntegrationTest.php +++ b/test/unit/ResultSet/ResultSetIntegrationTest.php @@ -32,6 +32,10 @@ #[CoversMethod(ResultSet::class, 'getReturnType')] #[CoversMethod(ResultSet::class, '__construct')] #[CoversMethod(ResultSet::class, 'getArrayObjectPrototype')] +#[CoversMethod(ResultSet::class, 'getRowPrototype')] +#[CoversMethod(ResultSet::class, 'setArrayObjectPrototype')] +#[CoversMethod(ResultSet::class, 'setRowPrototype')] +#[CoversMethod(ResultSet::class, 'toArray')] #[Group('unit')] final class ResultSetIntegrationTest extends TestCase { diff --git a/test/unit/ResultSet/RowPrototypeResultSetTest.php b/test/unit/ResultSet/RowPrototypeResultSetTest.php new file mode 100644 index 00000000..4a588c78 --- /dev/null +++ b/test/unit/ResultSet/RowPrototypeResultSetTest.php @@ -0,0 +1,83 @@ +createRowPrototype(); + $resultSet = new RowPrototypeResultSet($prototype); + $resultSet->initialize(new ArrayIterator([null])); + + static::assertNull($resultSet->current()); + } + + #[Test] + public function currentReturnsPopulatedCloneOfPrototypeForArrayRow(): void + { + $prototype = $this->createRowPrototype(); + $resultSet = new RowPrototypeResultSet($prototype); + $resultSet->initialize(new ArrayIterator([ + ['id' => 1, 'name' => 'one'], + ])); + + $current = $resultSet->current(); + + static::assertInstanceOf(RowPrototypeInterface::class, $current); + static::assertNotSame($prototype, $current); + static::assertSame(['id' => 1, 'name' => 'one'], $current->toArray()); + } + + #[Test] + public function toArrayConvertsPrototypeRowsToArrays(): void + { + $prototype = $this->createRowPrototype(); + $resultSet = new RowPrototypeResultSet($prototype); + $resultSet->initialize(new ArrayIterator([ + ['id' => 1], + ['id' => 2], + ])); + + static::assertSame( + [ + ['id' => 1], + ['id' => 2], + ], + $resultSet->toArray(), + ); + } + + private function createRowPrototype(): RowPrototypeInterface + { + return new class implements RowPrototypeInterface { + private array $data = []; + + public function populate(array $data): RowPrototypeInterface + { + $this->data = $data; + + return $this; + } + + public function toArray(): array + { + return $this->data; + } + }; + } +}