Iterators
AnyDataset provides several iterator classes and interfaces to help you navigate through your data. These iterators implement PHP's IteratorAggregate, so they work with foreach, iterator_to_array() and yield from, and they also have valid(), current(), key() and next() to step the rows by hand.
Iterator Interfaces
IteratorInterface
The base interface for all iterators in AnyDataset:
interface IteratorInterface extends IteratorAggregate
{
// Step the rows by hand (a foreach uses getIterator())
public function valid(): bool;
public function current(): mixed;
public function key(): mixed;
public function next(): void;
public function rewind(): void; // does nothing: forward only
/**
* Get an array representation of the iterator.
*/
public function toArray(array $fields = []): array;
/**
* Get an array of the underlying entities.
*/
public function toEntities(): array;
/**
* Get the first element of the iterator, or null if empty.
*/
public function first(): mixed;
/**
* Get the first element of the iterator, or throw an exception if empty.
*/
public function firstOrFail(): mixed;
/**
* Check if the iterator has any elements.
*/
public function exists(): bool;
/**
* Check if the iterator has any elements, or throw an exception if empty.
*/
public function existsOrFail(): bool;
}
Iterator Classes
GenericIterator
The abstract base class of every iterator. A subclass only returns the generator of its rows in rows():
abstract class GenericIterator implements IteratorInterface
{
// The generator of the rows, from a static function (see below)
abstract protected function rows(): Generator;
// A foreach runs on the generators themselves: there is no PHP method call for each row
public function getIterator(): Generator;
public function valid(): bool;
public function current(): mixed;
public function key(): mixed;
public function next(): void;
public function rewind(): void; // does nothing
public function toArray(array $fields = []): array;
// toEntities(), first(), firstOrFail(), exists(), existsOrFail()
}
The iterators move forward only: rewind() does nothing, because sources like a database cursor or a file
handle cannot go back. A foreach continues from the current row: after next(), or after another foreach
stopped with break. An iterator already walked has no more rows. To read the rows again, get a new iterator
from the dataset (getIterator()).
rows() must return the generator of a static function, not yield itself:
class MyIterator extends GenericIterator
{
public function __construct(private array $items)
{
}
protected function rows(): Generator
{
return self::walk($this->items);
}
private static function walk(array $items): Generator
{
foreach ($items as $item) {
yield new Row($item);
}
}
}
A generator of a method keeps the iterator while it is not finished, and the iterator keeps the generator: the
iterator would stay in memory, with its file or cursor open, until the garbage collector finds the cycle. From a
static function, the generator is destroyed with the iterator, and a finally in it releases what it opened
(first(), a foreach with break...). A rows() that yields itself throws LogicException.
A complete example, with a file opened and closed: How to write your own dataset and iterator.
AnyIterator
A concrete implementation of GenericIterator that works with arrays of Row objects:
class AnyIterator extends GenericIterator
{
public function __construct(array $list);
}
Using Iterators
Basic Iteration
<?php
use ByJG\AnyDataset\Core\AnyDataset;
$dataset = new AnyDataset($data);
$iterator = $dataset->getIterator();
// Using while loop
while ($iterator->valid()) {
$row = $iterator->current();
echo $row->get('name') . "\n";
$iterator->next();
}
// Or using foreach (preferred), with a new iterator: the one above has no more rows
foreach ($dataset->getIterator() as $row) {
echo $row->get('name') . "\n";
}
Reading forward only
An iterator reads its rows once, from the first to the last, like the database cursor or the file it may be
reading. rewind() does nothing. What that means in practice, with rows John, Mary, Ana:
<?php
// Look at the first row, then loop: the loop starts at that same row
$iterator = $dataset->getIterator();
$first = $iterator->current(); // John (reading it does not move)
foreach ($iterator as $row) { /* John, Mary, Ana */ }
// Stop, then continue: a foreach starts where the iterator is
$iterator = $dataset->getIterator();
foreach ($iterator as $row) {
if ($row->get('name') === 'Mary') {
break;
}
}
foreach ($iterator as $row) { /* Mary, Ana */ }
// After the last row there is nothing more: ask the dataset for a new iterator
$iterator = $dataset->getIterator();
$iterator->toArray(); // the 3 rows
$iterator->toArray(); // []
$dataset->getIterator()->toArray(); // the 3 rows again
- Stopping early releases the source. A database cursor or a file is released at the last row, or as soon
as the iterator is dropped: after
first(),exists(),getScalar(), or aforeachwithbreak, once the iterator goes out of scope. - Large results: walk them with
foreach, which keeps one row at a time;toArray()andtoEntities()load all the rows. - SPL wrappers: the iterators are an
IteratorAggregate(aTraversable), not anIterator. A wrapper that requires anIteratortakes them throughIteratorIterator:
<?php
// Rows 2 and 3 only
foreach (new LimitIterator(new IteratorIterator($dataset->getIterator()), 1, 2) as $row) {
// Mary, Ana
}
To read a source of your own with an iterator, see How to write your own dataset and iterator.
Filtered Iteration
You can use IteratorFilter to filter the results:
<?php
use ByJG\AnyDataset\Core\AnyDataset;
use ByJG\AnyDataset\Core\IteratorFilter;
use ByJG\AnyDataset\Core\Enum\Relation;
$dataset = new AnyDataset($data);
$filter = new IteratorFilter();
$filter->and('age', Relation::GreaterThan, 30);
$iterator = $dataset->getIterator($filter);
foreach ($iterator as $row) {
// This will only iterate over rows where age > 30
echo $row->get('name') . " is " . $row->get('age') . " years old\n";
}
Converting to Array
You can convert an iterator to an array:
Use toArray() with specific field names to extract only the data you need, reducing memory usage for large datasets.
<?php
use ByJG\AnyDataset\Core\AnyDataset;
$dataset = new AnyDataset($data);
$iterator = $dataset->getIterator();
// Get all data as an array of associative arrays
$allData = $iterator->toArray();
// Get only specific fields
$namesAndAges = $iterator->toArray(['name', 'age']);
Getting the underlying entities (objects)
If your rows were created from objects (models), you can retrieve those original entities:
<?php
use ByJG\AnyDataset\Core\AnyDataset;
$dataset = new AnyDataset($data);
$iterator = $dataset->getIterator();
$entities = $iterator->toEntities();
Getting the First Element
You can retrieve the first element from an iterator without manually iterating:
<?php
use ByJG\AnyDataset\Core\AnyDataset;
$dataset = new AnyDataset($data);
$iterator = $dataset->getIterator();
// Get the first element, or null if empty
$first = $iterator->first();
if ($first !== null) {
echo "First item found\n";
}
With Objects
When your dataset contains objects, first() returns the underlying entity:
<?php
use ByJG\AnyDataset\Core\AnyDataset;
class User {
public $id;
public $name;
public function __construct($id, $name) {
$this->id = $id;
$this->name = $name;
}
}
$dataset = new AnyDataset();
$dataset->appendRow(new User(1, "John"));
$dataset->appendRow(new User(2, "Jane"));
$firstUser = $dataset->getIterator()->first();
// $firstUser is a User object
echo $firstUser->name; // "John"
With Arrays
When your dataset contains arrays, first() returns the array:
<?php
use ByJG\AnyDataset\Core\AnyDataset;
$dataset = new AnyDataset();
$dataset->appendRow(['id' => 1, 'name' => 'John']);
$dataset->appendRow(['id' => 2, 'name' => 'Jane']);
$firstRow = $dataset->getIterator()->first();
// $firstRow is an array
echo $firstRow['name']; // "John"
Getting the First Element or Fail
Use firstOrFail() when you want to ensure there's at least one element:
<?php
use ByJG\AnyDataset\Core\AnyDataset;
use ByJG\AnyDataset\Core\Exception\NotFoundException;
$dataset = new AnyDataset();
try {
$first = $dataset->getIterator()->firstOrFail();
} catch (NotFoundException $e) {
echo "No results found: " . $e->getMessage();
}
Checking if Iterator Has Elements
You can check if an iterator contains any elements:
<?php
use ByJG\AnyDataset\Core\AnyDataset;
$dataset = new AnyDataset($data);
$iterator = $dataset->getIterator();
if ($iterator->exists()) {
echo "Iterator has elements\n";
} else {
echo "Iterator is empty\n";
}
Checking if Iterator Has Elements or Fail
Use existsOrFail() when you want to ensure the iterator is not empty:
<?php
use ByJG\AnyDataset\Core\AnyDataset;
use ByJG\AnyDataset\Core\Exception\NotFoundException;
$dataset = new AnyDataset($data);
try {
$dataset->getIterator()->existsOrFail();
echo "Iterator has elements\n";
} catch (NotFoundException $e) {
echo "Iterator is empty: " . $e->getMessage();
}
Combining with Filters
These methods work seamlessly with filters:
<?php
use ByJG\AnyDataset\Core\AnyDataset;
use ByJG\AnyDataset\Core\IteratorFilter;
use ByJG\AnyDataset\Core\Enum\Relation;
$dataset = new AnyDataset();
$dataset->appendRow(['name' => 'John', 'age' => 25]);
$dataset->appendRow(['name' => 'Jane', 'age' => 35]);
$dataset->appendRow(['name' => 'Bob', 'age' => 45]);
$filter = IteratorFilter::getInstance()
->and('age', Relation::GreaterThan, 30);
// Get first person over 30
$first = $dataset->getIterator($filter)->first();
echo $first['name']; // "Jane"
// Check if anyone is over 30
if ($dataset->getIterator($filter)->exists()) {
echo "Found people over 30\n";
}