ExceptTable.php
PHP
Path: src/Table/Wrappers/ExceptTable.php
<?php
namespace mini\Table\Wrappers;
use mini\Table\AbstractTable;
use mini\Table\Contracts\SetInterface;
use mini\Table\Contracts\TableInterface;
use mini\Table\Utility\TablePropertiesTrait;
use stdClass;
use Traversable;
/**
* Set difference table (rows in source but NOT in excluded set)
*
* Yields rows from source that don't exist in the excluded set.
* Filter methods push down to source via AbstractTableWrapper.
*
* ```php
* // WHERE id NOT IN (1, 2, 3)
* $table->columns('id')->except(new Set('id', [1, 2, 3]))
*
* // WHERE status != 'inactive'
* $table->except($table->eq('status', 'inactive'))
* ```
*/
class ExceptTable extends AbstractTableWrapper
{
public function __construct(
TableInterface $source,
private SetInterface $excluded,
) {
// Wrap source with pagination in BarrierTable to prevent filter pushdown
// from changing result set membership
if ($source instanceof AbstractTable && ($source->getLimit() !== null || $source->getOffset() > 0)) {
$source = BarrierTable::from($source);
}
parent::__construct($source);
}
// -------------------------------------------------------------------------
// Limit/offset must be 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;
}
protected function materialize(string ...$additionalColumns): Traversable
{
$excluded = $this->excluded;
$excludedCols = array_keys($excluded->getColumns());
$allAdditional = array_unique([...$additionalColumns, ...$excludedCols]);
$skipped = 0;
$emitted = 0;
$limit = $this->getLimit();
$offset = $this->getOffset();
foreach (parent::materialize(...$allAdditional) as $id => $row) {
// Build member object from excluded set's columns
$member = new stdClass();
foreach ($excludedCols as $col) {
$member->$col = $row->$col ?? null;
}
// Skip if in excluded set
if ($excluded->has($member)) {
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 is in excluded set, it's not in result
$excludedCols = array_keys($this->excluded->getColumns());
$excludedMember = new \stdClass();
foreach ($excludedCols as $col) {
if (!property_exists($member, $col)) {
// Can't check exclusion - fall back to parent
return parent::has($member);
}
$excludedMember->$col = $member->$col;
}
if ($this->excluded->has($excludedMember)) {
return false;
}
return parent::has($member);
}
}