TableInterface.php
PHP
Path: src/Table/Contracts/TableInterface.php
<?php
namespace mini\Table\Contracts;
use Countable;
use IteratorAggregate;
use mini\Table\ColumnDef;
use mini\Table\Predicate;
/**
* Interface for tabular data access with filtering, ordering, and pagination
*
* Implementations MUST be immutable - each method returns a new instance
* with the constraint applied, leaving the original unchanged:
*
* ```php
* $all = $table;
* $active = $table->eq('status', 'active'); // $all unchanged
* $sorted = $active->order('name'); // $active unchanged
* ```
*
* This enables safe composition and reuse of filtered views.
*
* Iteration MUST yield row ID as key and row data as stdClass:
*
* ```php
* foreach ($table as $rowId => $row) {
* // $rowId: int|string unique identifier
* // $row: stdClass with column properties
* echo $row->name;
* }
* ```
*
* Using stdClass (not array) ensures column names are always explicit,
* which is required for SetInterface::has() with composite keys.
*
* Row IDs are required for UPDATE/DELETE operations and for deduplication
* when merging results (e.g., OR clauses via union()).
*
* TableInterface extends SetInterface, enabling tables to be used as
* subqueries in IN clauses:
*
* ```php
* $activeUserIds = $users->eq('status', 'active')->columns('id');
* $orders->in('user_id', $activeUserIds);
* ```
*
* @extends IteratorAggregate<int|string, stdClass>
*/
interface TableInterface extends SetInterface, IteratorAggregate, Countable
{
/**
* Get column definitions for this table (SetInterface method)
*
* Returns ColumnDef objects keyed by column name, with index metadata
* for query optimization. Respects column projection - only returns
* columns that are currently visible.
*
* ```php
* $cols = $table->getColumns();
* $names = array_keys($cols); // ['id', 'name', 'email']
* $idIndex = $cols['id']->index; // IndexType::Primary
* $canSort = $cols['name']->canOrder(['name']); // true if indexed
* ```
*
* @return array<string, ColumnDef> Column name => ColumnDef
*/
public function getColumns(): array;
/**
* Get all column definitions regardless of projection
*
* Unlike getColumns(), this returns the full schema even after columns()
* has been called. Used by wrappers that need to filter/sort on columns
* that aren't in the output projection.
*
* ```php
* $projected = $table->columns('id', 'name');
* $projected->getColumns(); // ['id' => ..., 'name' => ...]
* $projected->getAllColumns(); // ['id' => ..., 'name' => ..., 'email' => ..., ...]
* ```
*
* @return array<string, ColumnDef> Column name => ColumnDef
*/
public function getAllColumns(): array;
/**
* Filter rows where column equals value (NULL uses IS NULL semantics)
*/
public function eq(string $column, int|float|string|null $value): TableInterface;
/**
* Filter rows where column is less than value
*/
public function lt(string $column, int|float|string $value): TableInterface;
/**
* Filter rows where column is less than or equal to value
*/
public function lte(string $column, int|float|string $value): TableInterface;
/**
* Filter rows where column is greater than value
*/
public function gt(string $column, int|float|string $value): TableInterface;
/**
* Filter rows where column is greater than or equal to value
*/
public function gte(string $column, int|float|string $value): TableInterface;
/**
* Filter rows where column value is in the given set
*
* The set can be an in-memory Set or another TableInterface (subquery):
*
* ```php
* // In-memory set
* $table->in('status', new Set('status', ['active', 'pending']));
*
* // Subquery
* $userIds = $orders->eq('status', 'shipped')->columns('user_id');
* $users->in('id', $userIds);
* ```
*/
public function in(string $column, SetInterface $values): TableInterface;
/**
* Filter rows where column matches a LIKE pattern
*
* Supports SQL LIKE wildcards:
* - `%` matches any sequence of characters
* - `_` matches any single character
*
* ```php
* $table->like('name', 'John%'); // Starts with "John"
* $table->like('email', '%@gmail.com'); // Ends with "@gmail.com"
* $table->like('code', 'A_B'); // "A" + any char + "B"
* ```
*/
public function like(string $column, string $pattern): TableInterface;
/**
* Return rows that are in this table OR the other table (set union)
*
* Rows are deduplicated by row ID. For OR conditions:
*
* ```php
* // WHERE status = 'active' OR status = 'pending'
* $table->eq('status', 'active')->union($table->eq('status', 'pending'))
* ```
*/
public function union(TableInterface $other): TableInterface;
/**
* Filter rows matching any of the given predicates (OR semantics)
*
* ```php
* // WHERE status = 'active' OR status = 'pending'
* $users->or(
* Predicate::eq('status', 'active'),
* Predicate::eq('status', 'pending')
* );
*
* // WHERE (age < 18) OR (age >= 65 AND status = 'retired')
* $users->or(
* Predicate::lt('age', 18),
* Predicate::gte('age', 65)->andEq('status', 'retired')
* );
*
* // With bind parameters
* $users->or(
* Predicate::eqBind('id', ':id1'),
* Predicate::eqBind('id', ':id2')
* );
* ```
*
* Note: Requires at least 2 predicates - OR semantically needs multiple alternatives.
*/
public function or(Predicate $a, Predicate $b, Predicate ...$more): TableInterface;
/**
* Return rows that are in this table but NOT in the other set (set difference)
*
* Enables all negation operations:
*
* ```php
* // WHERE status != 'inactive'
* $table->except($table->eq('status', 'inactive'))
*
* // WHERE id NOT IN (1, 2, 3)
* $table->columns('id')->except(new Set('id', [1, 2, 3]))
*
* // WHERE name IS NOT NULL
* $table->except($table->eq('name', null))
*
* // INTERSECT (A AND B) = A - (A - B)
* $a->except($a->except($b))
* ```
*/
public function except(SetInterface $other): TableInterface;
/**
* Project to specific columns
*
* Returns a table with only the specified columns. When used with
* a single column, the result can be used as a SetInterface for IN clauses.
*
* ```php
* $table->columns('id', 'name'); // Two columns
* $table->columns('id'); // Single column - usable as Set
* $table->columns('org_id', 'user_id'); // Composite key
* ```
*/
public function columns(string ...$columns): TableInterface;
/**
* Check if value(s) exist in the table's projected columns (SetInterface method)
*
* The member must have properties matching getColumns():
*
* ```php
* $table->columns('id')->has((object)['id' => 123]);
* $table->columns('a', 'b')->has((object)['a' => 1, 'b' => 2]);
* ```
*/
public function has(object $member): bool;
/**
* Set ordering (overwrites previous)
*
* @param string|null $spec Column name(s), optionally suffixed with " ASC" or " DESC"
* Multiple columns: "name ASC, created_at DESC"
* Empty string or null clears ordering
*/
public function order(?string $spec): TableInterface;
/**
* Set maximum number of rows to return (overwrites previous)
*/
public function limit(int $n): TableInterface;
/**
* Set number of rows to skip (overwrites previous)
*/
public function offset(int $n): TableInterface;
/**
* Get current limit (null if unlimited)
*/
public function getLimit(): ?int;
/**
* Get current offset (0 if not set)
*/
public function getOffset(): int;
/**
* Check if the table has any rows
*
* Implementations may optimize this (e.g., SELECT EXISTS(...) for databases).
* Default implementation uses limit(1)->count() > 0.
*
* ```php
* if ($table->eq('status', 'active')->exists()) {
* // At least one active row
* }
* ```
*/
public function exists(): bool;
/**
* Load a single row by its row ID
*
* Returns the row if it exists and matches current filters, null otherwise.
* This enables efficient indexed lookups without iterating.
*
* ```php
* $row = $table->load(123); // Get row with ID 123
* $row = $table->eq('status', 'active')->load(123); // Only if active
* ```
*
* Row IDs correspond to iteration keys:
* ```php
* foreach ($table as $rowId => $row) {
* $same = $table->load($rowId); // $same === $row
* }
* ```
*
* @param string|int $rowId The row identifier
* @return object|null The row as stdClass, or null if not found
*/
public function load(string|int $rowId): ?object;
/**
* Return table with duplicate rows removed
*
* Deduplication is based on the currently visible columns at the point
* distinct() is called. Further column projections do not change which
* rows were considered duplicates:
*
* ```php
* // Distinct on role+name, then project to just role
* $table->columns('role', 'name')->distinct()->columns('role');
* // May have duplicate roles if different names had same role
*
* // Distinct on just role
* $table->columns('role')->distinct();
* // Each role appears once
* ```
*
* Order relative to limit/offset matters:
* ```php
* $table->distinct()->limit(10); // Deduplicate all, then take 10
* $table->limit(10)->distinct(); // Take 10 (with dupes), then deduplicate
* ```
*/
public function distinct(): TableInterface;
/**
* Return table with aliased column names
*
* Used for JOINs where tables need qualified column names to avoid conflicts.
* Column names become "alias.column" format.
*
* ```php
* $u = $users->withAlias('u');
* // Columns: u.id, u.name, u.email
* // Rows: (object) ['u.id' => 123, 'u.name' => 'Frode']
*
* // With column renames
* $u = $users->withAlias('u', ['name' => 'username']);
* // Columns: u.id, u.username, u.email
* ```
*
* @param string|null $tableAlias Prefix for all columns (null = no prefix)
* @param array<string,string> $columnAliases Column renames ['original' => 'alias']
*/
public function withAlias(?string $tableAlias = null, array $columnAliases = []): TableInterface;
/**
* Get a table property
*
* Properties are arbitrary metadata attached to tables. Use hasProperty()
* to distinguish between "not set" and "set to null".
*
* ```php
* $table->getProperty('alias'); // 'u' or null
* ```
*
* @return mixed Property value, or null if not set
*/
public function getProperty(string $name): mixed;
/**
* Check if a property exists (including null values)
*/
public function hasProperty(string $name): bool;
/**
* Return table with property set
*
* Properties can be set to any value including null. Immutable - returns
* a new table instance with the property set.
*
* ```php
* $aliased = $users->withProperty('alias', 'u');
* $aliased->getProperty('alias'); // 'u'
* $users->getProperty('alias'); // null (original unchanged)
* ```
*/
public function withProperty(string $name, mixed $value): TableInterface;
}