OrTable.php
PHP
Path: src/Table/Wrappers/OrTable.php
<?php
namespace mini\Table\Wrappers;
use mini\Table\AbstractTable;
use mini\Table\Contracts\TableInterface;
use mini\Table\Predicate;
use mini\Table\Utility\TablePropertiesTrait;
use Traversable;
/**
* Applies OR predicates to source table in-memory
*
* Materializes source and yields rows matching any of the predicates.
*
* ```php
* new OrTable($source, Predicate::eq('x', 1), Predicate::eq('y', 2))
* ```
*/
class OrTable extends AbstractTableWrapper
{
/** @var Predicate[] */
private array $predicates;
public function __construct(
AbstractTable $source,
Predicate ...$predicates,
) {
// Filter out empty predicates
$this->predicates = array_values(array_filter(
$predicates,
fn($p) => !$p->isEmpty()
));
// Absorb source's limit/offset - we apply them after filtering
$this->limit = $source->getLimit();
$this->offset = $source->getOffset();
if ($this->limit !== null) {
$source = $source->limit(null);
}
if ($this->offset !== 0) {
$source = $source->offset(0);
}
parent::__construct($source);
}
/**
* Test if a row matches any predicate
*/
public function test(object $row): bool
{
foreach ($this->predicates as $predicate) {
if ($predicate->test($row)) {
return true;
}
}
return false;
}
/**
* Get all predicates
*
* @return Predicate[]
*/
public function getPredicates(): array
{
return $this->predicates;
}
protected function materialize(string ...$additionalColumns): Traversable
{
// If no predicates, yield nothing
if (empty($this->predicates)) {
return;
}
$skipped = 0;
$emitted = 0;
$limit = $this->getLimit();
$offset = $this->getOffset();
foreach (parent::materialize(...$additionalColumns) as $id => $row) {
if (!$this->test($row)) {
continue;
}
if ($skipped < $offset) {
$skipped++;
continue;
}
yield $id => $row;
$emitted++;
if ($limit !== null && $emitted >= $limit) {
return;
}
}
}
public function count(): int
{
return iterator_count($this);
}
public function has(object $member): bool
{
// Short-circuit: if member doesn't match any OR predicate, it's not in result
if (!$this->test($member)) {
return false;
}
return parent::has($member);
}
// -------------------------------------------------------------------------
// Limit/offset stored locally, not pushed to source
// -------------------------------------------------------------------------
public function limit(?int $n): TableInterface
{
if ($this->limit === $n) {
return $this;
}
$c = clone $this;
$c->limit = $n;
return $c;
}
public function offset(int $n): TableInterface
{
if ($this->offset === $n) {
return $this;
}
$c = clone $this;
$c->offset = $n;
return $c;
}
// -------------------------------------------------------------------------
// Don't push or() down - wrap in another OrTable or handle locally
// -------------------------------------------------------------------------
/**
* Restore absorbed pagination to a replacement table
*/
private function withPagination(TableInterface $table): TableInterface
{
if ($this->limit !== null) {
$table = $table->limit($this->limit);
}
if ($this->offset !== 0) {
$table = $table->offset($this->offset);
}
return $table;
}
public function or(Predicate $a, Predicate $b, Predicate ...$more): TableInterface
{
// Merge predicates into a new OrTable, restoring absorbed pagination
$allPredicates = [...$this->predicates, $a, $b, ...$more];
return $this->withPagination(new self($this->source, ...$allPredicates));
}
}