Skip to content

Instantly share code, notes, and snippets.

@ollieread
Created March 4, 2026 17:03
Show Gist options
  • Select an option

  • Save ollieread/707824b932e5997ac6e50093c57eeb03 to your computer and use it in GitHub Desktop.

Select an option

Save ollieread/707824b932e5997ac6e50093c57eeb03 to your computer and use it in GitHub Desktop.
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Clauses;
use Engine\Database\Query\Contracts\Expression;
/**
*
*/
final class JoinClause implements Expression
{
/**
* @var list<array{conjunction: 'AND'|'OR', sql: string, bindings: array<int|string, mixed>}>
*/
private array $conditions = [];
/**
* Add an ON condition to the join.
*
* @param string $left
* @param string $operator
* @param string $right
*
* @return $this
*/
public function on(string $left, string $operator, string $right): self
{
$this->conditions[] = [
'conjunction' => 'AND',
'sql' => "{$left} {$operator} {$right}",
'bindings' => [],
];
return $this;
}
/**
* Add an OR ON condition to the join.
*
* @param string $left
* @param string $operator
* @param string $right
*
* @return $this
*/
public function orOn(string $left, string $operator, string $right): self
{
$this->conditions[] = [
'conjunction' => 'OR',
'sql' => "{$left} {$operator} {$right}",
'bindings' => [],
];
return $this;
}
/**
* Add a WHERE condition to the join (bound value, not column reference).
*
* @param string $column
* @param mixed $operatorOrValue
* @param mixed $value
*
* @return $this
*/
public function where(string $column, mixed $operatorOrValue = null, mixed $value = null): self
{
if (func_num_args() === 2) {
$value = $operatorOrValue;
$operator = '=';
} else {
/** @var string $operator */
$operator = $operatorOrValue;
}
$this->conditions[] = [
'conjunction' => 'AND',
'sql' => "{$column} {$operator} ?",
'bindings' => [$value],
];
return $this;
}
/**
* Add an OR WHERE condition to the join.
*
* @param string $column
* @param mixed $operatorOrValue
* @param mixed $value
*
* @return $this
*/
public function orWhere(string $column, mixed $operatorOrValue = null, mixed $value = null): self
{
if (func_num_args() === 2) {
$value = $operatorOrValue;
$operator = '=';
} else {
/** @var string $operator */
$operator = $operatorOrValue;
}
$this->conditions[] = [
'conjunction' => 'OR',
'sql' => "{$column} {$operator} ?",
'bindings' => [$value],
];
return $this;
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
$parts = [];
foreach ($this->conditions as $i => $condition) {
$prefix = $i === 0 ? '' : " {$condition['conjunction']} ";
$parts[] = $prefix . $condition['sql'];
}
return implode('', $parts);
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
$bindings = [];
foreach ($this->conditions as $condition) {
$bindings[] = $condition['bindings'];
}
return array_merge(...$bindings ?: [[]]);
}
public function isEmpty(): bool
{
return empty($this->conditions);
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Clauses;
use Closure;
use Engine\Database\Query\Contracts\Expression;
use Engine\Database\Query\Exceptions\InvalidExpressionException;
use Engine\Database\Query\Expressions;
/**
*
*/
final class WhereClause implements Expression
{
/**
* @var list<array{conjunction: 'AND'|'OR', expression: \Engine\Database\Query\Contracts\Expression, grouped: bool}>
*/
private array $conditions = [];
/**
* Add a condition to the query.
*
* @param 'AND'|'OR' $conjunction
* @param string|\Closure $column
* @param string|null $operator
* @param mixed $value
*
* @return void
*/
private function condition(
string $conjunction,
string|Closure $column,
?string $operator,
mixed $value,
): void
{
if ($column instanceof Closure) {
$clause = new self();
$column($clause);
if ($clause->isEmpty()) {
throw InvalidExpressionException::emptyGroupedCondition();
}
$this->conditions[] = [
'conjunction' => $conjunction,
'expression' => $clause,
'grouped' => true,
];
} else {
/** @var string $operator */
$this->conditions[] = [
'conjunction' => $conjunction,
'expression' => Expressions::whereColumn($operator, $column, $value),
'grouped' => false,
];
}
}
/**
* Add a basic where clause to the query.
*
* @param string|\Closure $column
* @param mixed|null $operatorOrValue
* @param mixed|null $value
*
* @return $this
*/
public function where(string|Closure $column, mixed $operatorOrValue = null, mixed $value = null): self
{
if ($column instanceof Closure) {
$this->condition('AND', $column, null, null);
} else {
if (func_num_args() === 2) {
$value = $operatorOrValue;
$operator = '=';
} else {
/** @var string $operator */
$operator = $operatorOrValue;
}
$this->condition('AND', $column, $operator, $value);
}
return $this;
}
/**
* Add an "or where" clause to the query.
*
* @param string|\Closure $column
* @param mixed|null $operatorOrValue
* @param mixed|null $value
*
* @return $this
*/
public function orWhere(string|Closure $column, mixed $operatorOrValue = null, mixed $value = null): self
{
if ($column instanceof Closure) {
$this->condition('OR', $column, null, null);
} else {
if (func_num_args() === 2) {
$value = $operatorOrValue;
$operator = '=';
} else {
/** @var string $operator */
$operator = $operatorOrValue;
}
$this->condition('OR', $column, $operator, $value);
}
return $this;
}
/**
* Add a "where null" clause to the query.
*
* @param string $column
*
* @return $this
*/
public function whereNull(string $column): self
{
$this->condition('AND', $column, 'IS NULL', null);
return $this;
}
/**
* Add a "where not null" clause to the query.
*
* @param string $column
*
* @return $this
*/
public function orWhereNull(string $column): self
{
$this->condition('OR', $column, 'IS NULL', null);
return $this;
}
/**
* Add a "where not null" clause to the query.
*
* @param string $column
*
* @return $this
*/
public function whereNotNull(string $column): self
{
$this->condition('AND', $column, 'IS NOT NULL', null);
return $this;
}
/**
* Add a "where in" clause to the query.
*
* @param string $column
* @param array<mixed>|\Engine\Database\Query\Contracts\Expression $values
*
* @return $this
*/
public function whereIn(string $column, array|Expression $values): self
{
if (is_array($values) && empty($values)) {
throw InvalidExpressionException::emptyInClause($column);
}
if (is_array($values)) {
$this->condition('AND', $column, 'IN', $values);
} else {
$this->whereRaw("{$column} IN (" . $values->toSql() . ")", $values->getBindings());
}
return $this;
}
/**
* Add a "where not in" clause to the query.
*
* @param string $column
* @param array<mixed>|\Engine\Database\Query\Contracts\Expression $values
*
* @return $this
*/
public function whereNotIn(string $column, array|Expression $values): self
{
if (is_array($values) && empty($values)) {
throw InvalidExpressionException::emptyInClause($column);
}
if (is_array($values)) {
$this->condition('AND', $column, 'NOT IN', $values);
} else {
$this->whereRaw("{$column} NOT IN (" . $values->toSql() . ")", $values->getBindings());
}
return $this;
}
/**
* Add a raw where clause to the query.
*
* @param string $sql
* @param array<int|string, mixed> $bindings
*
* @return $this
*/
public function whereRaw(string $sql, array $bindings = []): self
{
$this->conditions[] = [
'conjunction' => 'AND',
'expression' => Expressions::raw($sql, $bindings),
'grouped' => false,
];
return $this;
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
$parts = [];
foreach ($this->conditions as $i => $condition) {
$prefix = $i === 0 ? '' : " {$condition['conjunction']} ";
$sql = $condition['expression']->toSql();
$parts[] = $prefix . ($condition['grouped'] ? "({$sql})" : $sql);
}
return implode('', $parts);
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
$bindings = [];
foreach ($this->conditions as $condition) {
$bindings[] = $condition['expression']->getBindings();
}
return array_merge(...$bindings);
}
public function isEmpty(): bool
{
return empty($this->conditions);
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Concerns;
use Closure;
use Engine\Database\Query\Clauses\JoinClause;
trait HasJoinClause
{
/**
* @var list<array{type: string, table: string, clause: \Engine\Database\Query\Clauses\JoinClause}>
*/
private array $joins = [];
/**
* Add an inner join to the query.
*
* @param string $table
* @param string|\Closure $first
* @param string|null $operator
* @param string|null $second
*
* @return $this
*/
public function join(string $table, string|Closure $first, ?string $operator = null, ?string $second = null): static
{
return $this->addJoin('INNER', $table, $first, $operator, $second);
}
/**
* Add a left join to the query.
*
* @param string $table
* @param string|\Closure $first
* @param string|null $operator
* @param string|null $second
*
* @return $this
*/
public function leftJoin(string $table, string|Closure $first, ?string $operator = null, ?string $second = null): static
{
return $this->addJoin('LEFT', $table, $first, $operator, $second);
}
/**
* Add a right join to the query.
*
* @param string $table
* @param string|\Closure $first
* @param string|null $operator
* @param string|null $second
*
* @return $this
*/
public function rightJoin(string $table, string|Closure $first, ?string $operator = null, ?string $second = null): static
{
return $this->addJoin('RIGHT', $table, $first, $operator, $second);
}
/**
* Add a cross join to the query.
*
* @param string $table
*
* @return $this
*/
public function crossJoin(string $table): static
{
$this->joins[] = ['type' => 'CROSS', 'table' => $table, 'clause' => new JoinClause()];
return $this;
}
private function addJoin(string $type, string $table, string|Closure $first, ?string $operator, ?string $second): static
{
$clause = new JoinClause();
if ($first instanceof Closure) {
$first($clause);
} else {
/** @var string $operator */
$clause->on($first, $operator, $second);
}
$this->joins[] = ['type' => $type, 'table' => $table, 'clause' => $clause];
return $this;
}
private function buildJoinClause(): string
{
if (empty($this->joins)) {
return '';
}
$parts = [];
foreach ($this->joins as $join) {
$sql = " {$join['type']} JOIN {$join['table']}";
if (! $join['clause']->isEmpty()) {
$sql .= ' ON ' . $join['clause']->toSql();
}
$parts[] = $sql;
}
return implode('', $parts);
}
/**
* @return array<int, mixed>
*/
private function getJoinBindings(): array
{
$bindings = [];
foreach ($this->joins as $join) {
$bindings = array_merge($bindings, $join['clause']->getBindings());
}
return $bindings;
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Concerns;
trait HasLimitClause
{
private ?int $limitValue = null;
private ?int $offsetValue = null;
/**
* Set the limit for the query.
*
* @param int $limit
*
* @return $this
*/
public function limit(int $limit): static
{
$this->limitValue = $limit;
return $this;
}
/**
* Set the offset for the query.
*
* @param int $offset
*
* @return $this
*/
public function offset(int $offset): static
{
$this->offsetValue = $offset;
return $this;
}
private function buildLimitClause(): string
{
$sql = '';
if ($this->limitValue !== null) {
$sql .= " LIMIT {$this->limitValue}";
}
if ($this->offsetValue !== null) {
$sql .= " OFFSET {$this->offsetValue}";
}
return $sql;
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Concerns;
use Engine\Database\Query\Contracts\Expression;
trait HasOrderByClause
{
/**
* @var list<array{column: string|\Engine\Database\Query\Contracts\Expression, direction: string}>
*/
private array $orders = [];
/**
* Add an order by clause to the query.
*
* @param string|\Engine\Database\Query\Contracts\Expression $column
* @param string $direction
*
* @return $this
*/
public function orderBy(string|Expression $column, string $direction = 'asc'): static
{
$this->orders[] = [
'column' => $column,
'direction' => strtolower($direction) === 'desc' ? 'DESC' : 'ASC',
];
return $this;
}
private function buildOrderByClause(): string
{
if (empty($this->orders)) {
return '';
}
$clauses = array_map(function (array $order): string {
$col = $order['column'] instanceof Expression
? $order['column']->toSql()
: $order['column'];
return "{$col} {$order['direction']}";
}, $this->orders);
return ' ORDER BY ' . implode(', ', $clauses);
}
/**
* @return array<int, mixed>
*/
private function getOrderByBindings(): array
{
$bindings = [];
foreach ($this->orders as $order) {
if ($order['column'] instanceof Expression) {
$bindings = array_merge($bindings, $order['column']->getBindings());
}
}
return $bindings;
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Concerns;
use Closure;
use Engine\Database\Query\Clauses\WhereClause;
use Engine\Database\Query\Contracts\Expression;
trait HasWhereClause
{
private(set) protected WhereClause $whereClause {
get => $this->whereClause ?? $this->whereClause = new WhereClause();
}
/**
* Add a basic where clause to the query.
*
* @param string|\Closure $column
* @param mixed|null $operatorOrValue
* @param mixed|null $value
*
* @return $this
*/
public function where(string|Closure $column, mixed $operatorOrValue = null, mixed $value = null): static
{
$this->whereClause->where(...func_get_args());
return $this;
}
/**
* Add an "or where" clause to the query.
*
* @param string|\Closure $column
* @param mixed|null $operatorOrValue
* @param mixed|null $value
*
* @return $this
*/
public function orWhere(string|Closure $column, mixed $operatorOrValue = null, mixed $value = null): static
{
$this->whereClause->orWhere(...func_get_args());
return $this;
}
/**
* Add a "where null" clause to the query.
*
* @param string $column
*
* @return $this
*/
public function whereNull(string $column): static
{
$this->whereClause->whereNull($column);
return $this;
}
/**
* Add a "where not null" clause to the query.
*
* @param string $column
*
* @return $this
*/
public function orWhereNull(string $column): static
{
$this->whereClause->orWhereNull($column);
return $this;
}
/**
* Add a "where not null" clause to the query.
*
* @param string $column
*
* @return $this
*/
public function whereNotNull(string $column): static
{
$this->whereClause->whereNotNull($column);
return $this;
}
/**
* Add a "where in" clause to the query.
*
* @param string $column
* @param array<mixed>|\Engine\Database\Query\Contracts\Expression $values
*
* @return $this
*/
public function whereIn(string $column, array|Expression $values): static
{
$this->whereClause->whereIn($column, $values);
return $this;
}
/**
* Add a "where not in" clause to the query.
*
* @param string $column
* @param array<mixed>|\Engine\Database\Query\Contracts\Expression $values
*
* @return $this
*/
public function whereNotIn(string $column, array|Expression $values): static
{
$this->whereClause->whereNotIn($column, $values);
return $this;
}
/**
* Add a raw where clause to the query.
*
* @param string $sql
* @param array<int|string, mixed> $bindings
*
* @return $this
*/
public function whereRaw(string $sql, array $bindings = []): static
{
$this->whereClause->whereRaw($sql, $bindings);
return $this;
}
protected function hasWhereClause(): bool
{
return $this->whereClause->isEmpty() === false;
}
}
<?php
namespace Engine\Database\Query\Contracts;
interface Expression
{
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string;
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array;
}
<?php
namespace Engine\Database\Query\Contracts;
interface Query extends Expression
{
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query;
use Engine\Database\Query\Concerns\HasWhereClause;
use Engine\Database\Query\Contracts\Query;
/**
*
*/
final class Delete implements Query
{
use HasWhereClause;
public static function from(string $table): self
{
return new self($table);
}
private function __construct(
private string $table,
)
{
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
$where = $this->hasWhereClause() ? ' WHERE ' . $this->whereClause->toSql() : '';
return "DELETE FROM {$this->table}" . $where;
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
return $this->whereClause->getBindings();
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Exceptions;
use InvalidArgumentException;
final class InvalidExpressionException extends InvalidArgumentException
{
public static function emptyGroupedCondition(): self
{
return new self('Grouped condition closure must add at least one condition.');
}
public static function emptyInClause(string $column): self
{
return new self(
sprintf('Cannot use an empty array for an IN clause on column "%s".', $column)
);
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query;
use Engine\Database\Query\Contracts\Expression;
use Engine\Database\Query\Expressions\ColumnEqualTo;
use Engine\Database\Query\Expressions\ColumnGreaterThen;
use Engine\Database\Query\Expressions\ColumnGreaterThenOrEqualTo;
use Engine\Database\Query\Expressions\ColumnIn;
use Engine\Database\Query\Expressions\ColumnIs;
use Engine\Database\Query\Expressions\ColumnIsNotNull;
use Engine\Database\Query\Expressions\ColumnIsNull;
use Engine\Database\Query\Expressions\ColumnLessThan;
use Engine\Database\Query\Expressions\ColumnLessThanOrEqualTo;
use Engine\Database\Query\Expressions\ColumnNotEqualTo;
use Engine\Database\Query\Expressions\ColumnNotIn;
use Engine\Database\Query\Expressions\RawExpression;
final class Expressions
{
public static function whereColumn(string $operator, string $column, mixed $value): Expression
{
return match (strtolower($operator)) {
'=' => ColumnEqualTo::make($column, $value),
'<' => ColumnLessThan::make($column, $value),
'>' => ColumnGreaterThen::make($column, $value),
'<=' => ColumnLessThanOrEqualTo::make($column, $value),
'>=' => ColumnGreaterThenOrEqualTo::make($column, $value),
'is' => ColumnIs::make($column, $value),
'is null' => ColumnIsNull::make($column),
'is not null' => ColumnIsNotNull::make($column),
'!=' => ColumnNotEqualTo::make($column, $value),
'in' => ColumnIn::make($column, $value), // @phpstan-ignore-line
'not in' => ColumnNotIn::make($column, $value), // @phpstan-ignore-line
};
}
/**
* @param string $sql
* @param array<int|string, mixed> $bindings
*
* @return \Engine\Database\Query\Contracts\Expression
*/
public static function raw(string $sql, array $bindings): Expression
{
return RawExpression::make($sql, $bindings);
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Expressions;
use Engine\Database\Query\Contracts\Expression;
final readonly class ColumnEqualTo implements Expression
{
public static function make(string $column, mixed $value): self
{
return new self($column, $value);
}
private function __construct(
private string $column,
private mixed $value
)
{
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
return "{$this->column} = ?";
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
return [$this->value];
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Expressions;
use Engine\Database\Query\Contracts\Expression;
final readonly class ColumnGreaterThen implements Expression
{
public static function make(string $column, mixed $value): self
{
return new self($column, $value);
}
private function __construct(
private string $column,
private mixed $value
)
{
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
return "{$this->column} > ?";
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
return [$this->value];
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Expressions;
use Engine\Database\Query\Contracts\Expression;
final readonly class ColumnGreaterThenOrEqualTo implements Expression
{
public static function make(string $column, mixed $value): self
{
return new self($column, $value);
}
private function __construct(
private string $column,
private mixed $value
)
{
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
return "{$this->column} >= ?";
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
return [$this->value];
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Expressions;
use Engine\Database\Query\Contracts\Expression;
final readonly class ColumnIn implements Expression
{
/**
* @param string $column
* @param array<mixed> $values
*
* @return self
*/
public static function make(string $column, array $values): self
{
return new self($column, $values);
}
/**
* @param string $column
* @param array<mixed> $values
*/
private function __construct(
private string $column,
private array $values
)
{
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
$params = array_fill(0, count($this->values), '?');
$params = implode(', ', $params);
return "{$this->column} IN ({$params})";
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
return $this->values;
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Expressions;
use Engine\Database\Query\Contracts\Expression;
final readonly class ColumnIs implements Expression
{
public static function make(string $column, mixed $value): self
{
return new self($column, $value);
}
private function __construct(
private string $column,
private mixed $value
)
{
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
return "{$this->column} IS ?";
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
return [$this->value];
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Expressions;
use Engine\Database\Query\Contracts\Expression;
final readonly class ColumnIsNotNull implements Expression
{
public static function make(string $column): self
{
return new self($column);
}
private function __construct(
private string $column
)
{
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
return "{$this->column} IS NOT NULL";
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
return [];
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Expressions;
use Engine\Database\Query\Contracts\Expression;
final readonly class ColumnIsNull implements Expression
{
public static function make(string $column): self
{
return new self($column);
}
private function __construct(
private string $column
)
{
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
return "{$this->column} IS NULL";
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
return [];
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Expressions;
use Engine\Database\Query\Contracts\Expression;
final readonly class ColumnLessThan implements Expression
{
public static function make(string $column, mixed $value): self
{
return new self($column, $value);
}
private function __construct(
private string $column,
private mixed $value
)
{
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
return "{$this->column} < ?";
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
return [$this->value];
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Expressions;
use Engine\Database\Query\Contracts\Expression;
final readonly class ColumnLessThanOrEqualTo implements Expression
{
public static function make(string $column, mixed $value): self
{
return new self($column, $value);
}
private function __construct(
private string $column,
private mixed $value
)
{
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
return "{$this->column} <= ?";
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
return [$this->value];
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Expressions;
use Engine\Database\Query\Contracts\Expression;
final readonly class ColumnNotEqualTo implements Expression
{
public static function make(string $column, mixed $value): self
{
return new self($column, $value);
}
private function __construct(
private string $column,
private mixed $value
)
{
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
return "{$this->column} != ?";
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
return [$this->value];
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Expressions;
use Engine\Database\Query\Contracts\Expression;
final readonly class ColumnNotIn implements Expression
{
/**
* @param string $column
* @param array<mixed> $values
*
* @return self
*/
public static function make(string $column, array $values): self
{
return new self($column, $values);
}
/**
* @param string $column
* @param array<mixed> $values
*/
private function __construct(
private string $column,
private array $values
)
{
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
$params = array_fill(0, count($this->values), '?');
$params = implode(', ', $params);
return "{$this->column} NOT IN ({$params})";
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
return $this->values;
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query\Expressions;
use Engine\Database\Query\Contracts\Expression;
final readonly class RawExpression implements Expression
{
/**
* @param string $sql
* @param array<int|string, mixed> $bindings
*
* @return self
*/
public static function make(string $sql, array $bindings): self
{
return new self($sql, $bindings);
}
/**
* @param string $sql
* @param array<int|string, mixed> $bindings
*/
private function __construct(
private string $sql,
private array $bindings
)
{
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
return $this->sql;
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
return $this->bindings;
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query;
use Engine\Database\Query\Contracts\Query;
/**
*
*/
final class Insert implements Query
{
public static function into(string $table): self
{
return new self($table);
}
/**
* @var array<string, mixed>
*/
private array $values = [];
private function __construct(
private string $table,
)
{
}
/**
* Set the values to insert.
*
* @param array<string, mixed> $values
*
* @return $this
*/
public function values(array $values): self
{
$this->values = $values;
return $this;
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
$columns = implode(', ', array_keys($this->values));
$placeholders = implode(', ', array_fill(0, count($this->values), '?'));
return "INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholders})";
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
return array_values($this->values);
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query;
use Engine\Database\Query\Contracts\Expression;
/**
*
*/
final readonly class Raw implements Expression
{
/**
* @param string $sql
* @param array<int|string, mixed> $bindings
*/
public function __construct(
private string $sql,
private array $bindings = [],
)
{
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
return $this->sql;
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
return $this->bindings;
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query;
use Engine\Database\Query\Concerns\HasJoinClause;
use Engine\Database\Query\Concerns\HasLimitClause;
use Engine\Database\Query\Concerns\HasOrderByClause;
use Engine\Database\Query\Concerns\HasWhereClause;
use Engine\Database\Query\Contracts\Expression;
use Engine\Database\Query\Contracts\Query;
/**
*
*/
final class Select implements Query
{
use HasWhereClause;
use HasJoinClause;
use HasOrderByClause;
use HasLimitClause;
public static function from(string|Expression $table): self
{
return new self($table);
}
private bool $distinct = false;
/**
* @var array<string|\Engine\Database\Query\Contracts\Expression>
*/
private array $columns = [];
private function __construct(
private string|Expression $table,
)
{
}
/**
* Set the columns to select.
*
* @param string|\Engine\Database\Query\Contracts\Expression ...$columns
*
* @return $this
*/
public function columns(string|Expression ...$columns): self
{
$this->columns = $columns;
return $this;
}
/**
* Add a column to select.
*
* @param string|\Engine\Database\Query\Contracts\Expression $column
*
* @return $this
*/
public function addColumn(string|Expression $column): self
{
$this->columns[] = $column;
return $this;
}
/**
* Set the query to select distinct rows.
*
* @return $this
*/
public function distinct(): self
{
$this->distinct = true;
return $this;
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
$columns = empty($this->columns) ? '*' : implode(', ', array_map(
fn (string|Expression $col) => $col instanceof Expression ? $col->toSql() : $col,
$this->columns,
));
$distinct = $this->distinct ? 'DISTINCT ' : '';
$table = $this->table instanceof Expression ? '(' . $this->table->toSql() . ')' : $this->table;
$where = $this->hasWhereClause() ? ' WHERE ' . $this->whereClause->toSql() : '';
return "SELECT {$distinct}{$columns} FROM {$table}"
. $this->buildJoinClause()
. $where
. $this->buildOrderByClause()
. $this->buildLimitClause();
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
$bindings = [];
// Table subquery bindings
if ($this->table instanceof Expression) {
$bindings = $this->table->getBindings();
}
// Column expression bindings
foreach ($this->columns as $column) {
if ($column instanceof Expression) {
$bindings = array_merge($bindings, $column->getBindings());
}
}
return array_merge(
$bindings,
$this->getJoinBindings(),
$this->whereClause->getBindings(),
$this->getOrderByBindings(),
);
}
}
<?php
declare(strict_types=1);
namespace Engine\Database\Query;
use Engine\Database\Query\Concerns\HasWhereClause;
use Engine\Database\Query\Contracts\Query;
/**
*
*/
final class Update implements Query
{
use HasWhereClause;
public static function table(string $table): self
{
return new self($table);
}
/**
* @var array<string, mixed>
*/
private array $sets = [];
private function __construct(
private string $table,
)
{
}
/**
* Set the column values to update.
*
* @param array<string, mixed> $values
*
* @return $this
*/
public function set(array $values): self
{
$this->sets = array_merge($this->sets, $values);
return $this;
}
/**
* Get the SQL representation of the expression.
*
* @return string
*/
public function toSql(): string
{
$setClauses = [];
foreach (array_keys($this->sets) as $column) {
$setClauses[] = "{$column} = ?";
}
$where = $this->hasWhereClause() ? ' WHERE ' . $this->whereClause->toSql() : '';
return "UPDATE {$this->table} SET " . implode(', ', $setClauses) . $where;
}
/**
* Get the bindings for the expression.
*
* @return array<int|string, mixed>
*/
public function getBindings(): array
{
return array_merge(
array_values($this->sets),
$this->whereClause->getBindings(),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment