From 9ef73f1430a00861feed98e6db0dcd9c24d75013 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Fri, 7 Aug 2026 12:58:49 -0500 Subject: [PATCH] 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 | 214 +++---- .../ArrayObjectResultSetInterface.php | 17 + .../Exception/ExceptionInterface.php | 4 +- .../Exception/InvalidArgumentException.php | 4 +- src/ResultSet/Exception/RuntimeException.php | 4 +- src/ResultSet/HydratingResultSet.php | 71 ++- src/ResultSet/HydratingResultSetInterface.php | 15 + src/ResultSet/ResultSet.php | 76 +-- src/ResultSet/ResultSetInterface.php | 19 +- .../AbstractResultSetIntegrationTest.php | 28 +- test/unit/ResultSet/AbstractResultSetTest.php | 522 ++++++++---------- .../ResultSet/ResultSetIntegrationTest.php | 392 +++++++------ 12 files changed, 644 insertions(+), 722 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 b909f4c1..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 @@ -43,70 +40,63 @@ abstract class AbstractResultSet implements ResultSetInterface protected int $position = 0; /** - * Set the data source for the result set - * - * @throws InvalidArgumentException|Exception + * @throws RuntimeException */ - #[Override] - public function initialize(iterable $dataSource): ResultSetInterface + public function buffer(): ResultSetInterface { - // reset buffering - if (is_array($this->buffer)) { + if ($this->buffer === -2) { + throw new RuntimeException('Buffering must be enabled before iteration is started'); + } elseif ($this->buffer === null) { $this->buffer = []; - } - - if ($dataSource instanceof ResultInterface) { - $this->fieldCount = $dataSource->getFieldCount(); - $this->dataSource = $dataSource; - if ($dataSource->isBuffered()) { - $this->buffer = -1; - } - - if (is_array($this->buffer)) { + if ($this->dataSource instanceof ResultInterface) { $this->dataSource->rewind(); } - - return $this; - } - - if (is_array($dataSource)) { - // its safe to get numbers from an array - $first = current($dataSource); - reset($dataSource); - $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) { - /** @phpstan-ignore assign.propertyType */ - $this->dataSource = $dataSource->getIterator(); - } else { - /** @phpstan-ignore assign.propertyType */ - $this->dataSource = $dataSource; } return $this; } /** - * @throws RuntimeException + * Countable: return count of rows */ - public function buffer(): ResultSetInterface + #[Override] + #[ReturnTypeWillChange] + public function count(): ?int { - if ($this->buffer === -2) { - throw new RuntimeException('Buffering must be enabled before iteration is started'); - } elseif ($this->buffer === null) { - $this->buffer = []; - if ($this->dataSource instanceof ResultInterface) { - $this->dataSource->rewind(); - } + if ($this->count !== null) { + return $this->count; } - return $this; + if ($this->dataSource instanceof Countable) { + $this->count = count($this->dataSource); + } + + return $this->count; } - public function isBuffered(): bool + /** + * Iterator: get current item + */ + #[Override] + public function current(): array|object|null { - return $this->buffer === -1 || is_array($this->buffer); + if (-1 === $this->buffer) { + // datasource was an array when the resultset was initialized + return $this->dataSource->current(); + } + + 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]; + } + + $data = $this->dataSource->current(); + if (is_array($this->buffer)) { + $this->buffer[$this->position] = $data; + } + + return is_array($data) ? $data : null; } /** @@ -150,20 +140,53 @@ public function getFieldCount(): int } /** - * Iterator: move pointer to next item + * Set the data source for the result set + * + * @throws InvalidArgumentException|Exception */ #[Override] - public function next(): void + public function initialize(iterable $dataSource): ResultSetInterface { - if ($this->buffer === null) { - $this->buffer = -2; // implicitly disable buffering from here on + // reset buffering + if (is_array($this->buffer)) { + $this->buffer = []; } - if (! is_array($this->buffer) || $this->position === $this->dataSource->key()) { - $this->dataSource->next(); + if ($dataSource instanceof ResultInterface) { + $this->fieldCount = $dataSource->getFieldCount(); + $this->dataSource = $dataSource; + if ($dataSource->isBuffered()) { + $this->buffer = -1; + } + + if (is_array($this->buffer)) { + $this->dataSource->rewind(); + } + + return $this; } - $this->position++; + if (is_array($dataSource)) { + // its safe to get numbers from an array + $first = current($dataSource); + reset($dataSource); + $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) { + /** @phpstan-ignore assign.propertyType */ + $this->dataSource = $dataSource->getIterator(); + } else { + /** @phpstan-ignore assign.propertyType */ + $this->dataSource = $dataSource; + } + + return $this; + } + + public function isBuffered(): bool + { + return $this->buffer === -1 || is_array($this->buffer); } /** @@ -176,41 +199,20 @@ public function key(): int } /** - * Iterator: get current item + * Iterator: move pointer to next item */ #[Override] - public function current(): array|object|null + public function next(): void { - if (-1 === $this->buffer) { - // datasource was an array when the resultset was initialized - return $this->dataSource->current(); - } - 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]; } - $data = $this->dataSource->current(); - if (is_array($this->buffer)) { - $this->buffer[$this->position] = $data; - } - - return is_array($data) ? $data : null; - } - - /** - * Iterator: is pointer valid? - */ - #[Override] - public function valid(): bool - { - if (is_array($this->buffer) && isset($this->buffer[$this->position])) { - return true; + if (! is_array($this->buffer) || $this->position === $this->dataSource->key()) { + $this->dataSource->next(); } - return $this->dataSource->valid(); + $this->position++; } /** @@ -227,53 +229,15 @@ public function rewind(): void } /** - * Countable: return count of rows - */ - #[Override] - #[ReturnTypeWillChange] - public function count(): ?int - { - if ($this->count !== null) { - return $this->count; - } - - if ($this->dataSource instanceof Countable) { - $this->count = count($this->dataSource); - } - - return $this->count; - } - - /** - * Cast result set to array of arrays - * - * @throws RuntimeException If any row is not castable to an array. + * Iterator: is pointer valid? */ #[Override] - public function toArray(): array + public function valid(): bool { - $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(); + if (is_array($this->buffer) && isset($this->buffer[$this->position])) { + return true; } - return $return; + return $this->dataSource->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 @@ +hydrator = $hydrator; - return $this; + 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]; + } + $data = $this->dataSource->current(); + $current = is_array($data) ? $this->getHydrator()->hydrate($data, clone $this->getRowPrototype()) : null; + + if (is_array($this->buffer)) { + $this->buffer[$this->position] = $current; + } + + return $current; } /** @@ -36,12 +47,10 @@ public function getHydrator(): HydratorInterface return $this->hydrator ??= new ArraySerializableHydrator(); } - /** {@inheritDoc} */ - #[Override] - public function setRowPrototype(object $rowPrototype): ResultSetInterface + /** @deprecated use getRowPrototype() */ + public function getObjectPrototype(): ?object { - $this->rowPrototype = $rowPrototype; - return $this; + return $this->getRowPrototype(); } /** {@inheritDoc} */ @@ -51,37 +60,27 @@ public function getRowPrototype(): object return $this->rowPrototype ??= new ArrayObject(); } - /** @deprecated use setRowPrototype() */ - public function setObjectPrototype(object $objectPrototype): ResultSetInterface + /** + * Set the hydrator to use for each row object + */ + public function setHydrator(HydratorInterface $hydrator): ResultSetInterface { - return $this->setRowPrototype($objectPrototype); + $this->hydrator = $hydrator; + return $this; } - /** @deprecated use getRowPrototype() */ - public function getObjectPrototype(): ?object + /** @deprecated use setRowPrototype() */ + public function setObjectPrototype(object $objectPrototype): ResultSetInterface { - return $this->getRowPrototype(); + return $this->setRowPrototype($objectPrototype); } - /** - * Iterator: get current item - */ + /** {@inheritDoc} */ #[Override] - public function current(): ?object + public function setRowPrototype(object $rowPrototype): ResultSetInterface&HydratingResultSetInterface { - 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]; - } - $data = $this->dataSource->current(); - $current = is_array($data) ? $this->getHydrator()->hydrate($data, clone $this->getRowPrototype()) : null; - - if (is_array($this->buffer)) { - $this->buffer[$this->position] = $current; - } - - return $current; + $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)) { $this->returnType = ResultSetReturnType::from($this->returnType); } } - /** {@inheritDoc} */ + /** + * Iterator: get current item + */ #[Override] - public function setRowPrototype(ArrayObject|RowPrototypeInterface $rowPrototype): ResultSetInterface + public function current(): array|ArrayObject|null { - $this->rowPrototype = $rowPrototype; + $data = parent::current(); - return $this; + if ($this->returnType === ResultSetReturnType::ArrayObject && is_array($data)) { + $ao = clone $this->getRowPrototype(); + $ao->exchangeArray($data); + + return $ao; + } + + return $data; } - /** {@inheritDoc} */ - #[Override] - public function getRowPrototype(): ArrayObject|RowPrototypeInterface + /** + * @deprecated use getRowPrototype() + */ + public function getArrayObjectPrototype(): ArrayObject { - return $this->rowPrototype; + return $this->getRowPrototype(); } /** @@ -52,22 +62,11 @@ public function getReturnType(): ResultSetReturnType return $this->returnType; } - /** - * Iterator: get current item - */ + /** {@inheritDoc} */ #[Override] - public function current(): array|ArrayObject|RowPrototypeInterface|null + public function getRowPrototype(): ArrayObject { - $data = parent::current(); - - if ($this->returnType === ResultSetReturnType::ArrayObject && is_array($data)) { - $ao = clone $this->getRowPrototype(); - $ao->exchangeArray($data); - - return $ao; - } - - return $data; + return $this->rowPrototype; } /** @@ -75,16 +74,29 @@ public function current(): array|ArrayObject|RowPrototypeInterface|null * * @deprecated use setRowPrototype() */ - public function setArrayObjectPrototype(ArrayObject|RowPrototypeInterface $arrayObjectPrototype): ResultSetInterface + public function setArrayObjectPrototype(ArrayObject $arrayObjectPrototype): ResultSetInterface&ArrayObjectResultSetInterface { return $this->setRowPrototype($arrayObjectPrototype); } - /** - * @deprecated use getRowPrototype() - */ - public function getArrayObjectPrototype(): ArrayObject|RowPrototypeInterface + /** {@inheritDoc} */ + #[Override] + public function setRowPrototype(ArrayObject $rowPrototype): ResultSetInterface&ArrayObjectResultSetInterface { - return $this->getRowPrototype(); + $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 9a3245cc..dc0a3525 100644 --- a/src/ResultSet/ResultSetInterface.php +++ b/src/ResultSet/ResultSetInterface.php @@ -4,17 +4,11 @@ namespace PhpDb\ResultSet; -use ArrayObject; use Countable; use Iterator; interface ResultSetInterface extends Iterator, Countable { - /** - * Can be anything iterable|array - */ - public function initialize(iterable $dataSource): ResultSetInterface; - /** * Field terminology is more correct as information coming back * from the database might be a column, and/or the result of an @@ -23,21 +17,12 @@ public function initialize(iterable $dataSource): ResultSetInterface; public function getFieldCount(): int; /** - * Set the row object prototype - * - * @throws Exception\InvalidArgumentException - */ - public function setRowPrototype(ArrayObject|RowPrototypeInterface $rowPrototype): ResultSetInterface; - - /** - * Get the row object prototype + * Can be anything iterable|array */ - public function getRowPrototype(): ?object; + 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 39d09e82..df7045cb 100644 --- a/test/unit/ResultSet/AbstractResultSetIntegrationTest.php +++ b/test/unit/ResultSet/AbstractResultSetIntegrationTest.php @@ -17,20 +17,6 @@ final class AbstractResultSetIntegrationTest extends TestCase { protected MockObject|AbstractResultSet $resultSet; - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - * - * @throws Exception - */ - #[Override] - protected function setUp(): void - { - $this->resultSet = $this->getMockBuilder(AbstractResultSet::class) - ->onlyMethods(['setRowPrototype', 'getRowPrototype']) - ->getMock(); - } - /** * @throws \Exception */ @@ -61,4 +47,18 @@ public function testCurrentCallsDataSourceCurrentOnceWithBuffer(): void $this->resultSet->current(); self::assertEquals($value1, $value2); } + + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + * + * @throws Exception + */ + #[Override] + protected function setUp(): void + { + $this->resultSet = $this->getMockBuilder(AbstractResultSet::class) + ->onlyMethods(['toArray']) + ->getMock(); + } } diff --git a/test/unit/ResultSet/AbstractResultSetTest.php b/test/unit/ResultSet/AbstractResultSetTest.php index 38476880..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,70 +34,10 @@ #[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; - private function createResultSetMock(): MockObject|AbstractResultSet - { - return $this->getMockBuilder(AbstractResultSet::class) - ->onlyMethods(['setRowPrototype', 'getRowPrototype']) - ->getMock(); - } - - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - #[Override] - protected function setUp(): void - { - $this->resultSet = $this->createResultSetMock(); - } - - /** - * @throws Exception - */ - public function testInitialize(): void - { - $resultSet = $this->createResultSetMock(); - - // Verify initialize() accepts array data and returns fluent interface - self::assertSame($resultSet, $resultSet->initialize([ - ['id' => 1, 'name' => 'one'], - ['id' => 2, 'name' => 'two'], - ['id' => 3, 'name' => 'three'], - ])); - - // Verify invalid data type throws exception - $this->expectException(TypeError::class); - /** @noinspection ALL */ - $resultSet->initialize('foo'); - } - - /** - * @throws Exception - */ - public function testInitializeDoesNotCallCount(): void - { - $resultSet = $this->createResultSetMock(); - $result = $this->getMockBuilder(ResultInterface::class)->onlyMethods([])->getMock(); - $result->expects($this->never())->method('count'); - // Initialize with result and verify count() is never called - $resultSet->initialize($result); - } - - /** - * @throws Exception - */ - public function testInitializeWithEmptyArray(): void - { - $resultSet = $this->createResultSetMock(); - // Verify initialize() accepts empty array - self::assertSame($resultSet, $resultSet->initialize([])); - } - /** * @throws Exception */ @@ -121,20 +60,13 @@ public function testBuffer(): void $resultSet->buffer(); } - public function testIsBuffered(): void - { - $resultSet = $this->createResultSetMock(); - // Verify buffering is disabled by default - self::assertFalse($resultSet->isBuffered()); - $resultSet->buffer(); - // Verify buffering is enabled after buffer() call - self::assertTrue($resultSet->isBuffered()); - } - /** + * Test multiple iterations with buffer + * * @throws Exception */ - public function testGetDataSource(): void + #[Group('issue-6845')] + public function testBufferIterations(): void { $resultSet = $this->createResultSetMock(); $resultSet->initialize(new ArrayIterator([ @@ -142,61 +74,61 @@ public function testGetDataSource(): void ['id' => 2, 'name' => 'two'], ['id' => 3, 'name' => 'three'], ])); - // Verify getDataSource() returns the initialized iterator - self::assertInstanceOf(ArrayIterator::class, $resultSet->getDataSource()); + $resultSet->buffer(); + + // Iterate through rows and verify data + $data = $resultSet->current(); + self::assertEquals(1, $data['id']); + $resultSet->next(); + $data = $resultSet->current(); + self::assertEquals(2, $data['id']); + + // Rewind and iterate again to verify buffering allows rewind + $resultSet->rewind(); + $data = $resultSet->current(); + self::assertEquals(1, $data['id']); + $resultSet->next(); + $data = $resultSet->current(); + self::assertEquals(2, $data['id']); + $resultSet->next(); + $data = $resultSet->current(); + self::assertEquals(3, $data['id']); } /** * @throws Exception */ - public function testGetFieldCount(): void + public function testCount(): void { $resultSet = $this->createResultSetMock(); $resultSet->initialize(new ArrayIterator([ ['id' => 1, 'name' => 'one'], + ['id' => 2, 'name' => 'two'], + ['id' => 3, 'name' => 'three'], ])); - // Verify getFieldCount() returns number of columns in current row - self::assertEquals(2, $resultSet->getFieldCount()); + // Verify count() returns total number of rows + self::assertEquals(3, $resultSet->count()); } - /** - * @throws Exception - */ - public function testNext(): void + public function testCountReturnsCachedResult(): void { - $rows = [ - ['id' => 1, 'name' => 'one'], - ['id' => 2, 'name' => 'two'], - ['id' => 3, 'name' => 'three'], - ]; - $resultSet = $this->createResultSetMock(); - $resultSet->initialize(new ArrayIterator($rows)); + $resultSet->initialize([['id' => 1], ['id' => 2]]); - // Verify next() advances iterator position - self::assertSame(0, $resultSet->key()); - $resultSet->next(); - self::assertSame(1, $resultSet->key()); + $first = $resultSet->count(); + $second = $resultSet->count(); + + self::assertSame(2, $first); + self::assertSame($first, $second); } - /** - * @throws Exception - */ - public function testKey(): void + public function testCountReturnsNullForUncountableDataSource(): void { $resultSet = $this->createResultSetMock(); - $resultSet->initialize(new ArrayIterator([ - ['id' => 1, 'name' => 'one'], - ['id' => 2, 'name' => 'two'], - ['id' => 3, 'name' => 'three'], - ])); - // Verify key() returns current iterator position - $resultSet->next(); - self::assertEquals(1, $resultSet->key()); - $resultSet->next(); - self::assertEquals(2, $resultSet->key()); - $resultSet->next(); - self::assertEquals(3, $resultSet->key()); + $iterator = new NoRewindIterator(new ArrayIterator([['id' => 1]])); + $resultSet->initialize($iterator); + + self::assertNull($resultSet->count()); } /** @@ -214,53 +146,34 @@ public function testCurrent(): void self::assertEquals(['id' => 1, 'name' => 'one'], $resultSet->current()); } - /** - * @throws Exception - */ - public function testValid(): void + public function testCurrentReturnsBufferedDataOnSecondPass(): void { $resultSet = $this->createResultSetMock(); $resultSet->initialize(new ArrayIterator([ ['id' => 1, 'name' => 'one'], ['id' => 2, 'name' => 'two'], - ['id' => 3, 'name' => 'three'], ])); - // Verify valid() returns true when iterator is at valid position - self::assertTrue($resultSet->valid()); - $resultSet->next(); - $resultSet->next(); - $resultSet->next(); - // Verify valid() returns false after iterating past last element - self::assertFalse($resultSet->valid()); - } + $resultSet->buffer(); - /** - * @throws Exception - */ - public function testRewindResetsIteratorPosition(): void - { - $rows = [ - ['id' => 1, 'name' => 'one'], - ['id' => 2, 'name' => 'two'], - ['id' => 3, 'name' => 'three'], - ]; - - $this->resultSet->initialize(new ArrayIterator($rows)); - - // Move forward to ensure position changes - $this->resultSet->next(); - self::assertSame(1, $this->resultSet->key()); - - // Verify rewind() resets iterator position and current row - $this->resultSet->rewind(); - self::assertSame(0, $this->resultSet->key()); - self::assertEquals($rows[0], $this->resultSet->current()); + $firstPass = []; + foreach ($resultSet as $row) { + $firstPass[] = $row; + } + + $resultSet->rewind(); + + $secondPass = []; + foreach ($resultSet as $row) { + $secondPass[] = $row; + } + + self::assertEquals($firstPass, $secondPass); } /** * @throws Exception */ - public function testCount(): void + public function testGetDataSource(): void { $resultSet = $this->createResultSetMock(); $resultSet->initialize(new ArrayIterator([ @@ -268,74 +181,102 @@ public function testCount(): void ['id' => 2, 'name' => 'two'], ['id' => 3, 'name' => 'three'], ])); - // Verify count() returns total number of rows - self::assertEquals(3, $resultSet->count()); + // Verify getDataSource() returns the initialized iterator + self::assertInstanceOf(ArrayIterator::class, $resultSet->getDataSource()); } /** * @throws Exception */ - public function testToArray(): void + public function testGetFieldCount(): 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() - ); + // Verify getFieldCount() returns number of columns in current row + self::assertEquals(2, $resultSet->getFieldCount()); } - public function testCurrentReturnsBufferedDataOnSecondPass(): void + public function testGetFieldCountReturnsCachedValue(): void { $resultSet = $this->createResultSetMock(); - $resultSet->initialize(new ArrayIterator([ - ['id' => 1, 'name' => 'one'], - ['id' => 2, 'name' => 'two'], - ])); - $resultSet->buffer(); + $resultSet->initialize([['a' => 1, 'b' => 2]]); - $firstPass = []; - foreach ($resultSet as $row) { - $firstPass[] = $row; - } + $first = $resultSet->getFieldCount(); + $second = $resultSet->getFieldCount(); - $resultSet->rewind(); + self::assertSame(2, $first); + self::assertSame($first, $second); + } - $secondPass = []; - foreach ($resultSet as $row) { - $secondPass[] = $row; - } + public function testGetFieldCountReturnsZeroForEmptyIterator(): void + { + $resultSet = $this->createResultSetMock(); + $resultSet->initialize(new ArrayIterator([])); - self::assertEquals($firstPass, $secondPass); + self::assertSame(0, $resultSet->getFieldCount()); } - public function testToArrayConvertsArrayObjectsViaGetArrayCopy(): void + public function testGetFieldCountReturnsZeroWithNoDataSource(): void { $resultSet = $this->createResultSetMock(); - $resultSet->initialize([ - new ArrayObject(['id' => 1, 'name' => 'one']), - ]); - $result = $resultSet->toArray(); + self::assertSame(0, $resultSet->getFieldCount()); + } - self::assertSame([['id' => 1, 'name' => 'one']], $result); + public function testGetFieldCountWithCountableRow(): void + { + $resultSet = $this->createResultSetMock(); + $resultSet->initialize(new ArrayIterator([new ArrayObject(['a' => 1, 'b' => 2, 'c' => 3])])); + + self::assertSame(3, $resultSet->getFieldCount()); } - public function testGetFieldCountReturnsZeroForEmptyIterator(): void + /** + * @throws Exception + */ + public function testInitialize(): void { $resultSet = $this->createResultSetMock(); - $resultSet->initialize(new ArrayIterator([])); - self::assertSame(0, $resultSet->getFieldCount()); + // Verify initialize() accepts array data and returns fluent interface + self::assertSame($resultSet, $resultSet->initialize([ + ['id' => 1, 'name' => 'one'], + ['id' => 2, 'name' => 'two'], + ['id' => 3, 'name' => 'three'], + ])); + + // Verify invalid data type throws exception + $this->expectException(TypeError::class); + /** @noinspection ALL */ + $resultSet->initialize('foo'); + } + + /** + * @throws Exception + */ + public function testInitializeDoesNotCallCount(): void + { + $resultSet = $this->createResultSetMock(); + $result = $this->getMockBuilder(ResultInterface::class)->onlyMethods([])->getMock(); + $result->expects($this->never())->method('count'); + // Initialize with result and verify count() is never called + $resultSet->initialize($result); + } + + /** + * @throws Exception + */ + public function testInitializeResetsBufferWhenAlreadyBuffered(): void + { + $resultSet = $this->createResultSetMock(); + $resultSet->initialize(new ArrayIterator([['id' => 1]])); + $resultSet->buffer(); + + $resultSet->initialize(new ArrayIterator([['id' => 2]])); + + self::assertSame(2, $resultSet->current()['id']); } public function testInitializeWithBufferedResultInterface(): void @@ -350,34 +291,65 @@ public function testInitializeWithBufferedResultInterface(): void self::assertTrue($resultSet->isBuffered()); } - public function testCountReturnsNullForUncountableDataSource(): void + /** + * @throws Exception + */ + public function testInitializeWithEmptyArray(): void { $resultSet = $this->createResultSetMock(); - $iterator = new NoRewindIterator(new ArrayIterator([['id' => 1]])); - $resultSet->initialize($iterator); + // Verify initialize() accepts empty array + self::assertSame($resultSet, $resultSet->initialize([])); + } - self::assertNull($resultSet->count()); + /** + * @throws Exception + */ + public function testInitializeWithIteratorAggregate(): void + { + $resultSet = $this->createResultSetMock(); + $aggregate = new class implements IteratorAggregate { + public function getIterator(): ArrayIterator + { + return new ArrayIterator([['id' => 1], ['id' => 2]]); + } + }; + + $resultSet->initialize($aggregate); + + self::assertSame(1, $resultSet->current()['id']); } - public function testValidReturnsFalseAfterLastElement(): void + /** + * @throws Exception + */ + public function testInitializeWithResultInterfaceRewindsWhenBuffered(): void { $resultSet = $this->createResultSetMock(); - $resultSet->initialize(new ArrayIterator([ - ['id' => 1], - ])); + $resultSet->initialize(new ArrayIterator([['id' => 1]])); + $resultSet->buffer(); - self::assertTrue($resultSet->valid()); - $resultSet->next(); - self::assertFalse($resultSet->valid()); + $result = $this->createMock(ResultInterface::class); + $result->method('getFieldCount')->willReturn(2); + $result->method('isBuffered')->willReturn(false); + $result->expects(self::once())->method('rewind'); + + $resultSet->initialize($result); + } + + public function testIsBuffered(): void + { + $resultSet = $this->createResultSetMock(); + // Verify buffering is disabled by default + self::assertFalse($resultSet->isBuffered()); + $resultSet->buffer(); + // Verify buffering is enabled after buffer() call + self::assertTrue($resultSet->isBuffered()); } /** - * Test multiple iterations with buffer - * * @throws Exception */ - #[Group('issue-6845')] - public function testBufferIterations(): void + public function testKey(): void { $resultSet = $this->createResultSetMock(); $resultSet->initialize(new ArrayIterator([ @@ -385,25 +357,13 @@ public function testBufferIterations(): void ['id' => 2, 'name' => 'two'], ['id' => 3, 'name' => 'three'], ])); - $resultSet->buffer(); - - // Iterate through rows and verify data - $data = $resultSet->current(); - self::assertEquals(1, $data['id']); + // Verify key() returns current iterator position $resultSet->next(); - $data = $resultSet->current(); - self::assertEquals(2, $data['id']); - - // Rewind and iterate again to verify buffering allows rewind - $resultSet->rewind(); - $data = $resultSet->current(); - self::assertEquals(1, $data['id']); + self::assertEquals(1, $resultSet->key()); $resultSet->next(); - $data = $resultSet->current(); - self::assertEquals(2, $data['id']); + self::assertEquals(2, $resultSet->key()); $resultSet->next(); - $data = $resultSet->current(); - self::assertEquals(3, $data['id']); + self::assertEquals(3, $resultSet->key()); } /** @@ -463,38 +423,47 @@ public function testMultipleRewindBufferIterations(): void /** * @throws Exception */ - public function testInitializeResetsBufferWhenAlreadyBuffered(): void + public function testNext(): void { - $resultSet = $this->createResultSetMock(); - $resultSet->initialize(new ArrayIterator([['id' => 1]])); - $resultSet->buffer(); + $rows = [ + ['id' => 1, 'name' => 'one'], + ['id' => 2, 'name' => 'two'], + ['id' => 3, 'name' => 'three'], + ]; - $resultSet->initialize(new ArrayIterator([['id' => 2]])); + $resultSet = $this->createResultSetMock(); + $resultSet->initialize(new ArrayIterator($rows)); - self::assertSame(2, $resultSet->current()['id']); + // Verify next() advances iterator position + self::assertSame(0, $resultSet->key()); + $resultSet->next(); + self::assertSame(1, $resultSet->key()); } /** * @throws Exception */ - public function testInitializeWithResultInterfaceRewindsWhenBuffered(): void + public function testRewindResetsIteratorPosition(): void { - $resultSet = $this->createResultSetMock(); - $resultSet->initialize(new ArrayIterator([['id' => 1]])); - $resultSet->buffer(); + $rows = [ + ['id' => 1, 'name' => 'one'], + ['id' => 2, 'name' => 'two'], + ['id' => 3, 'name' => 'three'], + ]; - $result = $this->createMock(ResultInterface::class); - $result->method('getFieldCount')->willReturn(2); - $result->method('isBuffered')->willReturn(false); - $result->expects(self::once())->method('rewind'); + $this->resultSet->initialize(new ArrayIterator($rows)); - $resultSet->initialize($result); + // Move forward to ensure position changes + $this->resultSet->next(); + self::assertSame(1, $this->resultSet->key()); + + // Verify rewind() resets iterator position and current row + $this->resultSet->rewind(); + self::assertSame(0, $this->resultSet->key()); + self::assertEquals($rows[0], $this->resultSet->current()); } - /** - * @throws Exception - */ - public function testInitializeWithIteratorAggregate(): void + public function testRewindWithNonIteratorDataSource(): void { $resultSet = $this->createResultSetMock(); $aggregate = new class implements IteratorAggregate { @@ -505,35 +474,42 @@ public function getIterator(): ArrayIterator }; $resultSet->initialize($aggregate); + $resultSet->next(); + $resultSet->rewind(); - self::assertSame(1, $resultSet->current()['id']); - } - - public function testGetFieldCountReturnsCachedValue(): void - { - $resultSet = $this->createResultSetMock(); - $resultSet->initialize([['a' => 1, 'b' => 2]]); - - $first = $resultSet->getFieldCount(); - $second = $resultSet->getFieldCount(); - - self::assertSame(2, $first); - self::assertSame($first, $second); + self::assertSame(0, $resultSet->key()); } - public function testGetFieldCountReturnsZeroWithNoDataSource(): void + /** + * @throws Exception + */ + public function testValid(): void { $resultSet = $this->createResultSetMock(); - - self::assertSame(0, $resultSet->getFieldCount()); + $resultSet->initialize(new ArrayIterator([ + ['id' => 1, 'name' => 'one'], + ['id' => 2, 'name' => 'two'], + ['id' => 3, 'name' => 'three'], + ])); + // Verify valid() returns true when iterator is at valid position + self::assertTrue($resultSet->valid()); + $resultSet->next(); + $resultSet->next(); + $resultSet->next(); + // Verify valid() returns false after iterating past last element + self::assertFalse($resultSet->valid()); } - public function testGetFieldCountWithCountableRow(): void + public function testValidReturnsFalseAfterLastElement(): void { $resultSet = $this->createResultSetMock(); - $resultSet->initialize(new ArrayIterator([new ArrayObject(['a' => 1, 'b' => 2, 'c' => 3])])); + $resultSet->initialize(new ArrayIterator([ + ['id' => 1], + ])); - self::assertSame(3, $resultSet->getFieldCount()); + self::assertTrue($resultSet->valid()); + $resultSet->next(); + self::assertFalse($resultSet->valid()); } public function testValidWithNonIteratorDataSource(): void @@ -552,42 +528,20 @@ public function getIterator(): ArrayIterator self::assertTrue($resultSet->valid()); } - public function testRewindWithNonIteratorDataSource(): void - { - $resultSet = $this->createResultSetMock(); - $aggregate = new class implements IteratorAggregate { - public function getIterator(): ArrayIterator - { - return new ArrayIterator([['id' => 1], ['id' => 2]]); - } - }; - - $resultSet->initialize($aggregate); - $resultSet->next(); - $resultSet->rewind(); - - self::assertSame(0, $resultSet->key()); - } - - public function testCountReturnsCachedResult(): void + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + */ + #[Override] + protected function setUp(): void { - $resultSet = $this->createResultSetMock(); - $resultSet->initialize([['id' => 1], ['id' => 2]]); - - $first = $resultSet->count(); - $second = $resultSet->count(); - - self::assertSame(2, $first); - self::assertSame($first, $second); + $this->resultSet = $this->createResultSetMock(); } - public function testToArrayThrowsOnNonCastableRows(): void + private function createResultSetMock(): MockObject|AbstractResultSet { - $resultSet = $this->createResultSetMock(); - $resultSet->initialize(new ArrayIterator([new stdClass()])); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('cannot be cast to an array'); - $resultSet->toArray(); + return $this->getMockBuilder(AbstractResultSet::class) + ->onlyMethods(['toArray']) + ->getMock(); } } diff --git a/test/unit/ResultSet/ResultSetIntegrationTest.php b/test/unit/ResultSet/ResultSetIntegrationTest.php index 4987fb20..8f2aaa31 100644 --- a/test/unit/ResultSet/ResultSetIntegrationTest.php +++ b/test/unit/ResultSet/ResultSetIntegrationTest.php @@ -37,56 +37,6 @@ final class ResultSetIntegrationTest extends TestCase { protected ResultSet $resultSet; - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - #[Override] - protected function setUp(): void - { - $this->resultSet = new ResultSet(); - } - - public function testRowObjectPrototypeIsPopulatedByRowObjectByDefault(): void - { - // Verify default row object prototype is ArrayObject - $row = $this->resultSet->getArrayObjectPrototype(); - self::assertInstanceOf('ArrayObject', $row); - } - - public function testRowObjectPrototypeIsMutable(): void - { - $row1 = new ArrayObject(['test1' => 'value1']); - $row2 = new ArrayObject(['test2' => 'value2']); - - // First mutation - $this->resultSet->setArrayObjectPrototype($row1); - - // Verify the first mutation occurred - self::assertSame($row1, $this->resultSet->getArrayObjectPrototype()); - - // Second mutation to verify mutability - $this->resultSet->setArrayObjectPrototype($row2); - - // Verify the instance was actually mutated - self::assertSame($row2, $this->resultSet->getArrayObjectPrototype()); - self::assertNotSame($row1, $this->resultSet->getArrayObjectPrototype()); - } - - public function testRowObjectPrototypeMayBePassedToConstructor(): void - { - $row = new ArrayObject(); - // Verify prototype can be passed to constructor - $resultSet = new ResultSet(ResultSet::TYPE_ARRAYOBJECT, $row); - self::assertSame($row, $resultSet->getArrayObjectPrototype()); - } - - public function testReturnTypeIsObjectByDefault(): void - { - // Verify default return type is ArrayObject - self::assertEquals(ResultSetReturnType::ArrayObject, $this->resultSet->getReturnType()); - } - /** @psalm-return array */ public static function invalidReturnTypes(): array { @@ -100,29 +50,32 @@ public static function invalidReturnTypes(): array ]; } - #[DataProvider('invalidReturnTypes')] - public function testSettingInvalidReturnTypeRaisesException(mixed $type): void + public function getArrayDataSource(int $count): ArrayIterator { - // Verify invalid return type throws TypeError - $this->expectException(TypeError::class); - new ResultSet(ResultSet::TYPE_ARRAYOBJECT, $type); - } + $array = []; + for ($i = 0; $i < $count; $i++) { + $array[] = [ + 'id' => $i, + 'title' => 'title ' . $i, + ]; + } - public function testDataSourceIsNullByDefault(): void - { - // Verify data source is null before initialization - self::assertNull($this->resultSet->getDataSource()); + return new ArrayIterator($array); } /** + * @throws Exception * @throws \Exception */ - public function testCanProvideIteratorAsDataSource(): void + public function testBufferCalledAfterIterationThrowsException(): void { - $it = new SplStack(); - // Initialize with iterator and verify it is stored as data source - $this->resultSet->initialize($it); - self::assertSame($it, $this->resultSet->getDataSource()); + $this->resultSet->initialize($this->createMock(ResultInterface::class)); + $this->resultSet->current(); + + // Verify buffer() throws exception when called after iteration has started + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Buffering must be enabled before iteration is started'); + $this->resultSet->buffer(); } /** @@ -161,77 +114,12 @@ public function testCanProvideIteratorAggregateAsDataSource(): void /** * @throws \Exception */ - #[DataProvider('invalidReturnTypes')] - public function testInvalidDataSourceRaisesException(mixed $dataSource): void - { - if (is_array($dataSource)) { - $this->expectNotToPerformAssertions(); - // this is valid - return; - } - - // Verify invalid data source throws TypeError - $this->expectException(TypeError::class); - $this->resultSet->initialize($dataSource); - } - - public function testFieldCountIsZeroWithNoDataSourcePresent(): void - { - // Verify field count is 0 when no data source is set - self::assertEquals(0, $this->resultSet->getFieldCount()); - } - - public function getArrayDataSource(int $count): ArrayIterator - { - $array = []; - for ($i = 0; $i < $count; $i++) { - $array[] = [ - 'id' => $i, - 'title' => 'title ' . $i, - ]; - } - - return new ArrayIterator($array); - } - - /** - * @throws \Exception - */ - public function testFieldCountRepresentsNumberOfFieldsInARowOfData(): void - { - $resultSet = new ResultSet(ResultSet::TYPE_ARRAY); - $dataSource = $this->getArrayDataSource(10); - // Verify field count matches number of columns in row data - $resultSet->initialize($dataSource); - self::assertEquals(2, $resultSet->getFieldCount()); - } - - /** - * @throws \Exception - */ - public function testWhenReturnTypeIsArrayThenIterationReturnsArrays(): void - { - $resultSet = new ResultSet(ResultSet::TYPE_ARRAY); - $dataSource = $this->getArrayDataSource(10); - $resultSet->initialize($dataSource); - // Iterate and verify each row is returned as array - foreach ($resultSet as $index => $row) { - self::assertEquals($dataSource[$index], $row); - } - } - - /** - * @throws \Exception - */ - public function testWhenReturnTypeIsObjectThenIterationReturnsRowObjects(): void + public function testCanProvideIteratorAsDataSource(): void { - $dataSource = $this->getArrayDataSource(10); - $this->resultSet->initialize($dataSource); - // Iterate and verify each row is returned as ArrayObject - foreach ($this->resultSet as $index => $row) { - self::assertInstanceOf('ArrayObject', $row); - self::assertEquals($dataSource[$index], $row->getArrayCopy()); - } + $it = new SplStack(); + // Initialize with iterator and verify it is stored as data source + $this->resultSet->initialize($it); + self::assertSame($it, $this->resultSet->getDataSource()); } /** @@ -247,36 +135,57 @@ public function testCountReturnsCountOfRows(): void self::assertEquals($count, $this->resultSet->count()); } - /** - * @throws RandomException - * @throws \Exception - */ - public function testToArrayRaisesExceptionForRowsThatAreNotArraysOrArrayCastable(): void + public function testCurrentClonesRowPrototypeOnEachCall(): void { - $count = random_int(3, 75); - $dataSource = $this->getArrayDataSource($count); - foreach ($dataSource as $index => $row) { - $dataSource[$index] = (object) $row; - } + $resultSet = new ResultSet(ResultSetReturnType::ArrayObject); + $resultSet->initialize([ + ['id' => 1, 'name' => 'one'], + ['id' => 2, 'name' => 'two'], + ]); - // Verify toArray() throws exception for non-array-castable objects - $this->resultSet->initialize($dataSource); - $this->expectException(RuntimeException::class); - $this->resultSet->toArray(); + $first = $resultSet->current(); + $resultSet->next(); + $second = $resultSet->current(); + + self::assertNotSame($first, $second); + } + + public function testCurrentReturnsArrayObjectWhenReturnTypeIsArrayObject(): void + { + $resultSet = new ResultSet(ResultSetReturnType::ArrayObject); + $resultSet->initialize([['id' => 1, 'name' => 'one']]); + + $current = $resultSet->current(); + + self::assertInstanceOf(ArrayObject::class, $current); + self::assertSame(1, $current['id']); + } + + public function testCurrentReturnsArrayWhenReturnTypeIsArray(): void + { + $resultSet = new ResultSet(ResultSetReturnType::Array); + $resultSet->initialize([['id' => 1, 'name' => 'one']]); + + $current = $resultSet->current(); + + self::assertIsArray($current); + self::assertSame(1, $current['id']); } /** - * @throws RandomException + * @throws Exception * @throws \Exception */ - public function testToArrayCreatesArrayOfArraysRepresentingRows(): void + public function testCurrentReturnsNullForNonExistingValues(): void { - $count = random_int(3, 75); - $dataSource = $this->getArrayDataSource($count); - // Verify toArray() returns array representation of all rows - $this->resultSet->initialize($dataSource); - $test = $this->resultSet->toArray(); - self::assertEquals($dataSource->getArrayCopy(), $test, var_export($test, true)); + $mockResult = $this->createMock(ResultInterface::class); + $mockResult->expects($this->once())->method('current')->willReturn('Not an Array'); + + $this->resultSet->initialize($mockResult); + $this->resultSet->buffer(); + + // Verify current() returns null when data source returns non-array value + self::assertNull($this->resultSet->current()); } /** @@ -296,86 +205,159 @@ public function testCurrentWithBufferingCallsDataSourceCurrentOnce(): void $this->resultSet->current(); } + public function testDataSourceIsNullByDefault(): void + { + // Verify data source is null before initialization + self::assertNull($this->resultSet->getDataSource()); + } + + public function testFieldCountIsZeroWithNoDataSourcePresent(): void + { + // Verify field count is 0 when no data source is set + self::assertEquals(0, $this->resultSet->getFieldCount()); + } + /** - * @throws Exception * @throws \Exception */ - public function testBufferCalledAfterIterationThrowsException(): void + public function testFieldCountRepresentsNumberOfFieldsInARowOfData(): void { - $this->resultSet->initialize($this->createMock(ResultInterface::class)); - $this->resultSet->current(); + $resultSet = new ResultSet(ResultSet::TYPE_ARRAY); + $dataSource = $this->getArrayDataSource(10); + // Verify field count matches number of columns in row data + $resultSet->initialize($dataSource); + self::assertEquals(2, $resultSet->getFieldCount()); + } - // Verify buffer() throws exception when called after iteration has started - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Buffering must be enabled before iteration is started'); - $this->resultSet->buffer(); + public function testGetArrayObjectPrototypeDelegatesToGetRowPrototype(): void + { + self::assertSame( + $this->resultSet->getRowPrototype(), + $this->resultSet->getArrayObjectPrototype(), + ); + } + + public function testGetReturnTypeReturnsArrayWhenSetToArray(): void + { + $resultSet = new ResultSet(ResultSetReturnType::Array); + + self::assertSame(ResultSetReturnType::Array, $resultSet->getReturnType()); } /** - * @throws Exception * @throws \Exception */ - public function testCurrentReturnsNullForNonExistingValues(): void + #[DataProvider('invalidReturnTypes')] + public function testInvalidDataSourceRaisesException(mixed $dataSource): void { - $mockResult = $this->createMock(ResultInterface::class); - $mockResult->expects($this->once())->method('current')->willReturn("Not an Array"); + if (is_array($dataSource)) { + $this->expectNotToPerformAssertions(); + // this is valid + return; + } - $this->resultSet->initialize($mockResult); - $this->resultSet->buffer(); + // Verify invalid data source throws TypeError + $this->expectException(TypeError::class); + $this->resultSet->initialize($dataSource); + } - // Verify current() returns null when data source returns non-array value - self::assertNull($this->resultSet->current()); + public function testReturnTypeIsObjectByDefault(): void + { + // Verify default return type is ArrayObject + self::assertEquals(ResultSetReturnType::ArrayObject, $this->resultSet->getReturnType()); } - public function testCurrentReturnsArrayObjectWhenReturnTypeIsArrayObject(): void + public function testRowObjectPrototypeIsMutable(): void { - $resultSet = new ResultSet(ResultSetReturnType::ArrayObject); - $resultSet->initialize([['id' => 1, 'name' => 'one']]); + $row1 = new ArrayObject(['test1' => 'value1']); + $row2 = new ArrayObject(['test2' => 'value2']); - $current = $resultSet->current(); + // First mutation + $this->resultSet->setArrayObjectPrototype($row1); - self::assertInstanceOf(ArrayObject::class, $current); - self::assertSame(1, $current['id']); - } + // Verify the first mutation occurred + self::assertSame($row1, $this->resultSet->getArrayObjectPrototype()); - public function testCurrentReturnsArrayWhenReturnTypeIsArray(): void - { - $resultSet = new ResultSet(ResultSetReturnType::Array); - $resultSet->initialize([['id' => 1, 'name' => 'one']]); + // Second mutation to verify mutability + $this->resultSet->setArrayObjectPrototype($row2); - $current = $resultSet->current(); + // Verify the instance was actually mutated + self::assertSame($row2, $this->resultSet->getArrayObjectPrototype()); + self::assertNotSame($row1, $this->resultSet->getArrayObjectPrototype()); + } - self::assertIsArray($current); - self::assertSame(1, $current['id']); + public function testRowObjectPrototypeIsPopulatedByRowObjectByDefault(): void + { + // Verify default row object prototype is ArrayObject + $row = $this->resultSet->getArrayObjectPrototype(); + self::assertInstanceOf('ArrayObject', $row); } - public function testCurrentClonesRowPrototypeOnEachCall(): void + public function testRowObjectPrototypeMayBePassedToConstructor(): void { - $resultSet = new ResultSet(ResultSetReturnType::ArrayObject); - $resultSet->initialize([ - ['id' => 1, 'name' => 'one'], - ['id' => 2, 'name' => 'two'], - ]); + $row = new ArrayObject(); + // Verify prototype can be passed to constructor + $resultSet = new ResultSet(ResultSet::TYPE_ARRAYOBJECT, $row); + self::assertSame($row, $resultSet->getArrayObjectPrototype()); + } - $first = $resultSet->current(); - $resultSet->next(); - $second = $resultSet->current(); + #[DataProvider('invalidReturnTypes')] + public function testSettingInvalidReturnTypeRaisesException(mixed $type): void + { + // Verify invalid return type throws TypeError + $this->expectException(TypeError::class); + new ResultSet(ResultSet::TYPE_ARRAYOBJECT, $type); + } - self::assertNotSame($first, $second); + /** + * @throws RandomException + * @throws \Exception + */ + public function testToArrayCreatesArrayOfArraysRepresentingRows(): void + { + $count = random_int(3, 75); + $dataSource = $this->getArrayDataSource($count); + // Verify toArray() returns array representation of all rows + $this->resultSet->initialize($dataSource); + $test = $this->resultSet->toArray(); + self::assertEquals($dataSource->getArrayCopy(), $test, var_export($test, true)); } - public function testGetReturnTypeReturnsArrayWhenSetToArray(): void + /** + * @throws \Exception + */ + public function testWhenReturnTypeIsArrayThenIterationReturnsArrays(): void { - $resultSet = new ResultSet(ResultSetReturnType::Array); + $resultSet = new ResultSet(ResultSet::TYPE_ARRAY); + $dataSource = $this->getArrayDataSource(10); + $resultSet->initialize($dataSource); + // Iterate and verify each row is returned as array + foreach ($resultSet as $index => $row) { + self::assertEquals($dataSource[$index], $row); + } + } - self::assertSame(ResultSetReturnType::Array, $resultSet->getReturnType()); + /** + * @throws \Exception + */ + public function testWhenReturnTypeIsObjectThenIterationReturnsRowObjects(): void + { + $dataSource = $this->getArrayDataSource(10); + $this->resultSet->initialize($dataSource); + // Iterate and verify each row is returned as ArrayObject + foreach ($this->resultSet as $index => $row) { + self::assertInstanceOf('ArrayObject', $row); + self::assertEquals($dataSource[$index], $row->getArrayCopy()); + } } - public function testGetArrayObjectPrototypeDelegatesToGetRowPrototype(): void + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + */ + #[Override] + protected function setUp(): void { - self::assertSame( - $this->resultSet->getRowPrototype(), - $this->resultSet->getArrayObjectPrototype() - ); + $this->resultSet = new ResultSet(); } }