GeneratorTable.php
PHP
Path: src/Table/GeneratorTable.php
<?php
namespace mini\Table;
use Closure;
use mini\Table\Contracts\SetInterface;
use mini\Table\Contracts\TableInterface;
use mini\Table\Types\ColumnType;
use mini\Table\Types\IndexType;
use mini\Table\Types\Operator;
use mini\Table\Utility\EmptyTable;
use mini\Table\Wrappers\FilteredTable;
use mini\Table\Wrappers\OrTable;
use mini\Table\Wrappers\SortedTable;
use Traversable;
/**
* Simple table backed by a generator/closure
*
* The closure must return a generator that yields key => stdClass pairs.
* Column definitions must be provided explicitly.
*
* ```php
* $table = new GeneratorTable(
* function() {
* yield 1 => (object)['id' => 1, 'name' => 'Alice'];
* yield 2 => (object)['id' => 2, 'name' => 'Bob'];
* },
* new ColumnDef('id', ColumnType::Int, IndexType::Primary),
* new ColumnDef('name', ColumnType::Text),
* );
* ```
*
* Small result sets (≤1000 rows) are cached after first iteration for
* efficient repeated access. Larger result sets stream without buffering.
*/
class GeneratorTable extends AbstractTable
{
private Closure $generator;
public function __construct(Closure $generator, ColumnDef ...$columns)
{
if (empty($columns)) {
throw new \InvalidArgumentException('GeneratorTable requires at least one column');
}
$this->generator = $generator;
parent::__construct(...$columns);
}
protected function materialize(string ...$additionalColumns): Traversable
{
$visibleCols = array_keys($this->getColumns());
$cols = array_unique([...$visibleCols, ...$additionalColumns]);
$skipped = 0;
$emitted = 0;
$limit = $this->getLimit();
$offset = $this->getOffset();
foreach (($this->generator)() as $key => $row) {
if ($skipped < $offset) {
$skipped++;
continue;
}
// Project to requested columns
$projected = new \stdClass();
foreach ($cols as $col) {
$projected->$col = $row->$col ?? null;
}
yield $key => $projected;
$emitted++;
if ($limit !== null && $emitted >= $limit) {
return;
}
}
}
public function count(): int
{
return iterator_count($this);
}
// -------------------------------------------------------------------------
// Filter methods - return FilteredTable wrappers
// -------------------------------------------------------------------------
public function eq(string $column, int|float|string|null $value): TableInterface
{
return new FilteredTable($this, $column, Operator::Eq, $value);
}
public function lt(string $column, int|float|string $value): TableInterface
{
return new FilteredTable($this, $column, Operator::Lt, $value);
}
public function lte(string $column, int|float|string $value): TableInterface
{
return new FilteredTable($this, $column, Operator::Lte, $value);
}
public function gt(string $column, int|float|string $value): TableInterface
{
return new FilteredTable($this, $column, Operator::Gt, $value);
}
public function gte(string $column, int|float|string $value): TableInterface
{
return new FilteredTable($this, $column, Operator::Gte, $value);
}
public function in(string $column, SetInterface $values): TableInterface
{
return new FilteredTable($this, $column, Operator::In, $values);
}
public function like(string $column, string $pattern): TableInterface
{
return new FilteredTable($this, $column, Operator::Like, $pattern);
}
public function order(?string $spec): TableInterface
{
$orders = $spec ? OrderDef::parse($spec) : [];
if (empty($orders)) {
return $this;
}
return new SortedTable($this, ...$orders);
}
public function or(Predicate $a, Predicate $b, Predicate ...$more): TableInterface
{
// Filter out empty predicates (match nothing)
$predicates = array_values(array_filter(
[$a, $b, ...$more],
fn($p) => !$p->isEmpty()
));
// No predicates → nothing matches
if (empty($predicates)) {
return EmptyTable::from($this);
}
// In-memory table - use OrTable for single-pass evaluation
return new OrTable($this, ...$predicates);
}
}