Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3e27bbe
Return object rows rather than discarding them
simon-mundy Sep 30, 2026
c087edb
Fill row prototypes from object rows instead of widening current()
simon-mundy Sep 30, 2026
1ecfa9b
Drop the dead array arm from RowPrototypeResultSet::current()
simon-mundy Sep 30, 2026
cd59a9a
Narrow ArrayResultSet::current() to ?array and drop added commentary
simon-mundy Sep 30, 2026
6c556bb
Honour ResultSetReturnType::Prototype in ResultSet::current()
simon-mundy Sep 30, 2026
e135d64
Refuse rows a result set cannot read
simon-mundy Sep 30, 2026
10e3375
Document ObjectResultSet and the row types each set accepts
simon-mundy Sep 30, 2026
329f977
Exclude prefer-self-return-type where self cannot be used
simon-mundy Sep 30, 2026
84134c5
Return null for an exhausted row without asking the data source valid()
simon-mundy Oct 2, 2026
df2c82a
Read rows straight from the data source once buffering is settled
simon-mundy Oct 2, 2026
8a75215
Buffer raw rows and map them afresh on every pass
simon-mundy Oct 2, 2026
e5d0fed
Honour getter overrides and keep hydrated rows across buffered passes
simon-mundy Oct 2, 2026
8fa7cdb
Drop the unreachable buffering branch from the first read and cover t…
simon-mundy Oct 2, 2026
b96d571
Pass Mago on the getter and mapped-row changes
simon-mundy Oct 2, 2026
3698cda
Bring the result set docs in line with the code
simon-mundy Oct 2, 2026
228cccf
Cut per-row work from buffered reads
simon-mundy Oct 2, 2026
7e78d27
Throw UnexpectedValueException for rows a result set cannot read
simon-mundy Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
324 changes: 24 additions & 300 deletions analyzer-baseline.toml

Large diffs are not rendered by default.

41 changes: 38 additions & 3 deletions docs/book/result-set/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use ArrayObject;
class ResultSet extends AbstractResultSet
{
public function __construct(
ResultSetReturnType $returnType = ResultSetReturnType::ArrayObject,
?ArrayObject $rowPrototype = null
ResultSetReturnType|string $returnType = ResultSetReturnType::ArrayObject,
ArrayObject $rowPrototype = new ArrayObject([], ArrayObject::ARRAY_AS_PROPS)
);

public function setRowPrototype(
Expand All @@ -39,6 +39,7 @@ enum ResultSetReturnType: string
{
case ArrayObject = 'arrayobject';
case Array = 'array';
case Prototype = 'prototype';
}
```

Expand All @@ -56,10 +57,12 @@ $resultSet = new ResultSet(ResultSetReturnType::Array);

- `ResultSetReturnType::ArrayObject` (default) - Returns rows as
ArrayObject instances
- `ResultSetReturnType::Prototype` - The same as `ArrayObject`: rows fill a clone
of the row prototype. It is the newer name, matching `setRowPrototype()`
- `ResultSetReturnType::Array` - Returns rows as plain PHP arrays

**`$rowPrototype`** - Custom ArrayObject prototype for row objects
(only used with ArrayObject mode)
(only used with ArrayObject and Prototype modes)

#### Return Type Modes

Expand Down Expand Up @@ -200,6 +203,38 @@ RuntimeException: Buffering must be enabled before iteration is
started
```

### What a Buffer Holds

What a buffered pass hands back depends on the kind of row.

- `ResultSet` and `RowPrototypeResultSet` keep the rows as the data source returned
them and build each row afresh on every pass. A change made to an `ArrayObject` or
row prototype in one pass does not carry into the next.
- `HydratingResultSet` keeps the objects it hydrated on the first pass and hands the
same objects out on every later pass, as laminas-db and earlier PhpDb releases did.
A hydrated row is an entity with an identity of its own, so a change made to it in
one pass is still there in the next, and nothing is hydrated twice.

```php title="Buffered Entities Keep Their Identity"
$resultSet = new HydratingResultSet(new ReflectionHydrator(), new UserEntity());
$resultSet->initialize($result);
$resultSet->buffer();

foreach ($resultSet as $user) {
$user->setName(strtoupper($user->getName()));
}

foreach ($resultSet as $user) {
echo $user->getName(); // upper-cased: the same objects as the first pass
}
```

Rows are built from what `getRowPrototype()` and `getHydrator()` return, so a subclass
that overrides either getter decides how its rows are built. `ResultSet` and
`RowPrototypeResultSet` ask once for each data source. `HydratingResultSet` asks on its
first row and again after `setRowPrototype()` or `setHydrator()`, so a getter whose
answer changes between data sources needs one of those setters called to take effect.

### isBuffered() Method

Checks if the result set is currently buffered:
Expand Down
17 changes: 13 additions & 4 deletions docs/book/result-set/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,16 +205,25 @@ try {
}
```

**toArray() on non-castable rows:**
### UnexpectedValueException

**A row the result set cannot read**, such as a `PDO::FETCH_OBJ` row in a
`ResultSet`, is refused when it is read, whether by iterating, `current()` or
`toArray()`:

```php
use PhpDb\ResultSet\Exception\UnexpectedValueException;

try {
$resultSet->toArray();
} catch (RuntimeException $e) {
printf("Error: Could not convert row to array\n");
$rows = $resultSet->toArray();
} catch (UnexpectedValueException $e) {
printf("Error: %s\n", $e->getMessage());
}
```

It extends SPL's `\UnexpectedValueException`, so `catch (\RuntimeException $e)` and
`catch (\Exception $e)` catch it too, as does `PhpDb\Exception\ExceptionInterface`.

## Troubleshooting

### Property Access Not Working
Expand Down
77 changes: 66 additions & 11 deletions docs/book/result-set/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,33 +69,39 @@ functionality:
namespace PhpDb\ResultSet;

use Iterator;
use IteratorAggregate;
use PhpDb\Adapter\Driver\ResultInterface;

abstract class AbstractResultSet implements Iterator, ResultSetInterface
abstract class AbstractResultSet implements ResultSetInterface
{
public function initialize(
array|Iterator|IteratorAggregate|ResultInterface $dataSource
): ResultSetInterface;
public function getDataSource():
array|Iterator|IteratorAggregate|ResultInterface;
protected const bool HOLDS_MAPPED_ROWS = false;

public function initialize(iterable $dataSource): ResultSetInterface;
public function getDataSource(): ?Iterator;
public function getFieldCount(): int;

public function buffer(): ResultSetInterface;
public function isBuffered(): bool;

public function next(): void;
public function key(): int;
public function current(): mixed;
public function valid(): bool;
public function rewind(): void;

public function count(): int;
public function count(): ?int;

public function toArray(): array;
abstract protected function mapRow(mixed $row): mixed;
protected function resetResolvedConfiguration(): void;
}
```

`ResultSetInterface` extends `Iterator` and `Countable` and adds `toArray()`. Each
concrete result set declares `current()` with its own row type and implements it by
returning `$this->currentRow()`, which serves the row from the buffer or the data source
and passes it through `mapRow()` to give it that shape. A custom result set implements
`current()`, `mapRow()` and `toArray()`. It can redeclare `HOLDS_MAPPED_ROWS` as `true`
to hand out the same row objects on every buffered pass, and override
`resetResolvedConfiguration()` to forget anything it cached when `initialize()` is given
a new data source.

## HydratingResultSet

`PhpDb\ResultSet\HydratingResultSet` is a more flexible `ResultSet` object
Expand Down Expand Up @@ -145,6 +151,55 @@ For more information, see the
documentation to get a better sense of the different strategies that can be
employed in order to populate a target object.

## ObjectResultSet

`PhpDb\ResultSet\ObjectResultSet` is for fetch modes that yield objects, such as
`PDO::FETCH_OBJ` and `PDO::FETCH_LAZY`. Rows leave it as the very instances the driver
produced: nothing is cloned, hydrated or reshaped.

```php title="Using ObjectResultSet with PDO::FETCH_OBJ"
use PDO;
use PhpDb\ResultSet\ObjectResultSet;

$result = $statement->execute();
$result->setFetchMode(PDO::FETCH_OBJ);

$resultSet = new ObjectResultSet();
$resultSet->initialize($result);

foreach ($resultSet as $user) {
printf("%s %s\n", $user->first_name, $user->last_name);
}
```

`toArray()` reads each row's values, either by traversing it or by reading its public
properties. A row that exposes neither — a `PDORow` from `PDO::FETCH_LAZY`, for
instance — throws rather than yielding an empty array, so iterate such a result set
instead of calling `toArray()` on it.

## Choosing a Result Set

A result set fills its rows from row data, meaning an `array` or an `ArrayObject`. It
will not transform a row the driver handed it into some other shape, so the fetch mode
and the result set have to agree; where they do not, the row is refused with a
`PhpDb\ResultSet\Exception\UnexpectedValueException` naming both types.

| Result set | Rows arrive as | Rows leave as |
|---|---|---|
| `ResultSet` | row data, or an `ArrayObject` | a filled `ArrayObject` prototype, an array, or that same `ArrayObject` |
| `ArrayResultSet` | row data | an array |
| `ObjectResultSet` | any object | that same object |
| `HydratingResultSet` | row data | the hydrated row prototype |
| `RowPrototypeResultSet` | row data, or a `RowPrototypeInterface` | a populated `RowPrototypeInterface` |

With PDO that means `FETCH_ASSOC` and `FETCH_NAMED` suit every row-data set.
`FETCH_NUM`, `FETCH_BOTH` and `FETCH_KEY_PAIR` give rows with integer keys, which
`ResultSet`, `ArrayResultSet` and `RowPrototypeResultSet` accept but a hydrator cannot
map to properties, so `HydratingResultSet` fails on them with a `TypeError` from
laminas-hydrator. `FETCH_OBJ` and `FETCH_LAZY` need `ObjectResultSet`. `FETCH_BOUND`
yields only a success flag and binds its columns by reference, so no result set accepts
its rows; read the bound variables instead.

## Data Source Types

The `initialize()` method accepts arrays, `Iterator`, `IteratorAggregate`,
Expand Down
86 changes: 4 additions & 82 deletions lint-baseline.toml
Original file line number Diff line number Diff line change
Expand Up @@ -256,96 +256,18 @@ count = 1
file = "src/ResultSet/AbstractResultSet.php"
code = "halstead"
message = "Method has a high halstead difficulty"
count = 2

[[issues]]
file = "src/ResultSet/AbstractResultSet.php"
code = "no-else-clause"
message = "Avoid `else` clauses."
count = 1

[[issues]]
file = "src/ResultSet/AbstractResultSet.php"
code = "no-else-clause"
message = "Avoid `elseif` clauses."
count = 3

[[issues]]
file = "src/ResultSet/AbstractResultSet.php"
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."
code = "kan-defect"
message = "Class has a high kan defect score."
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"
message = "Method has a high halstead difficulty"
count = 1

[[issues]]
file = "src/ResultSet/HydratingResultSet.php"
code = "no-else-clause"
message = "Avoid `elseif` clauses."
count = 1

[[issues]]
file = "src/ResultSet/HydratingResultSet.php"
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."
code = "too-many-methods"
message = "Class has too many methods."
count = 1

[[issues]]
Expand Down
8 changes: 8 additions & 0 deletions mago.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ baseline = "lint-baseline.toml"
# not apply. "test/" repeats the phpdb-qa-tools default, which this key replaces.
too-many-methods = { exclude = ["test/", "src/Sql/Exception/", "src/TableGateway/Exception/", "src/Adapter/Exception/"] }

# The capability interfaces return `ResultSetInterface&<themselves>`, and PHP rejects
# `self` as part of an intersection type, so this rule's advice cannot be taken there.
prefer-self-return-type = { exclude = [
"src/ResultSet/ArrayObjectResultSetInterface.php",
"src/ResultSet/HydratingResultSetInterface.php",
"src/ResultSet/RowPrototypeResultSetInterface.php",
] }

[analyzer]
baseline = "analyzer-baseline.toml"
class-initializers = [
Expand Down
Loading
Loading