Query.php

PHP

Path: src/Database/Query.php

<?php

namespace mini\Database;

use Closure;
use IteratorAggregate;
use Countable;
use Traversable;
use mini\Table\Predicate;

/**
 * User-facing query class for reading data
 *
 * Wraps PartialQuery with a clean, read-focused API. Mutations go through
 * DatabaseInterface methods (update, delete) rather than the query itself.
 *
 * ```php
 * $query = db()->query('SELECT * FROM users WHERE active = true');
 *
 * // Filtering
 * $admins = $query->eq('role', 'admin');
 *
 * // Shaping
 * $recent = $query->order('created_at DESC')->limit(10);
 *
 * // Fetching
 * foreach ($recent as $user) { ... }
 * $user = $query->eq('id', 5)->one();
 *
 * // Mutations go through database
 * db()->update($query, ['status' => 'verified']);
 * db()->delete($query->eq('spam', true));
 * ```
 */
final class Query implements IteratorAggregate, Countable
{
    /**
     * @param PartialQuery $pq The underlying query
     * @param Closure(PartialQuery): Query $wrap Factory to wrap derived queries
     */
    public function __construct(
        private PartialQuery $pq,
        private Closure $wrap
    ) {}

    // =========================================================================
    // Filtering
    // =========================================================================

    public function eq(string $column, int|float|string|null $value): static
    {
        return ($this->wrap)($this->pq->eq($column, $value));
    }

    public function lt(string $column, int|float|string $value): static
    {
        return ($this->wrap)($this->pq->lt($column, $value));
    }

    public function lte(string $column, int|float|string $value): static
    {
        return ($this->wrap)($this->pq->lte($column, $value));
    }

    public function gt(string $column, int|float|string $value): static
    {
        return ($this->wrap)($this->pq->gt($column, $value));
    }

    public function gte(string $column, int|float|string $value): static
    {
        return ($this->wrap)($this->pq->gte($column, $value));
    }

    public function like(string $column, string $pattern): static
    {
        return ($this->wrap)($this->pq->like($column, $pattern));
    }

    public function in(string $column, array|Query $values): static
    {
        if ($values instanceof Query) {
            return ($this->wrap)($this->pq->in($column, $values->pq));
        }
        return ($this->wrap)($this->pq->in($column, $values));
    }

    public function or(Predicate $a, Predicate $b, Predicate ...$more): static
    {
        return ($this->wrap)($this->pq->or($a, $b, ...$more));
    }

    public function where(string $sql, array $params = []): static
    {
        return ($this->wrap)($this->pq->where($sql, $params));
    }

    // =========================================================================
    // Shaping
    // =========================================================================

    public function columns(string ...$columns): static
    {
        return ($this->wrap)($this->pq->columns(...$columns));
    }

    public function order(?string $spec): static
    {
        return ($this->wrap)($this->pq->order($spec));
    }

    public function limit(int $n): static
    {
        return ($this->wrap)($this->pq->limit($n));
    }

    public function offset(int $n): static
    {
        return ($this->wrap)($this->pq->offset($n));
    }

    public function distinct(): static
    {
        return ($this->wrap)($this->pq->distinct());
    }

    // =========================================================================
    // Fetching
    // =========================================================================

    /**
     * Get first row or null
     */
    public function one(): mixed
    {
        return $this->pq->one();
    }

    /**
     * Get first row or throw
     *
     * @throws \RuntimeException if no rows found
     */
    public function first(): object
    {
        $result = $this->pq->one();
        if ($result === null) {
            throw new \RuntimeException('No rows found');
        }
        return $result;
    }

    /**
     * Get all rows as array
     */
    public function all(): array
    {
        return iterator_to_array($this->pq);
    }

    /**
     * Check if any rows exist
     */
    public function exists(): bool
    {
        return $this->pq->exists();
    }

    /**
     * Load a single row by primary key
     */
    public function load(int|string $id): ?object
    {
        return $this->pq->load($id);
    }

    // =========================================================================
    // Iteration & Counting
    // =========================================================================

    public function getIterator(): Traversable
    {
        return $this->pq->getIterator();
    }

    public function count(): int
    {
        return $this->pq->count();
    }

    // =========================================================================
    // Hydration
    // =========================================================================

    /**
     * Hydrate results into entity instances
     */
    public function withEntityClass(string $class, array $constructorArgs = []): static
    {
        return ($this->wrap)($this->pq->withEntityClass($class, $constructorArgs ?: false));
    }

    /**
     * Transform each row with a custom hydrator
     */
    public function withHydrator(Closure $hydrator): static
    {
        return ($this->wrap)($this->pq->withHydrator($hydrator));
    }

    // =========================================================================
    // Debugging
    // =========================================================================

    public function __toString(): string
    {
        return (string) $this->pq;
    }
}