[], self::CONDITION_HAVING => []]; private array $groups = []; private array $orders = []; private ?array $limit; public function __construct(IConnection $connection, ?string $table = null) { $this->connection = $connection; if ($table !== null) { $this->table = $table; } } public function setIdName(string $idName): Select { $this->idName = $idName; return $this; } public function setTableAliases(array $tableAliases): Select { $this->tableAliases = array_merge($this->tableAliases, $tableAliases); return $this; } public function setDerivedTableAlias(string $tableAlias): Select { return $this->setTableAliases([Select::DERIVED_TABLE_KEY => $tableAlias]); } public function from(string $table): Select { $this->table = $table; return $this; } public function columns(array $columns): Select { $this->columns = array_merge($this->columns, $columns); return $this; } public function innerJoin($table, $column1, string $relation, $column2): Select { $this->addJoin('INNER', $table, $column1, $relation, $column2); return $this; } public function leftJoin($table, $column1, string $relation, $column2): Select { $this->addJoin('LEFT', $table, $column1, $relation, $column2); return $this; } public function whereId($value): Select { $this->addWhereCondition('AND', $this->idName, '=', $value); return $this; } public function where($column, string $relation = null, $value = null): Select { $this->addWhereCondition('AND', $column, $relation, $value); return $this; } public function orWhere($column, string $relation = null, $value = null): Select { $this->addWhereCondition('OR', $column, $relation, $value); return $this; } public function having($column, string $relation = null, $value = null): Select { $this->addHavingCondition('AND', $column, $relation, $value); return $this; } public function orHaving($column, string $relation = null, $value = null): Select { $this->addHavingCondition('OR', $column, $relation, $value); return $this; } public function groupBy($column): Select { $this->groups[] = $column; return $this; } public function orderBy($column, string $type = 'ASC'): Select { $this->orders[] = [$column, $type]; return $this; } public function limit(int $limit, int $offset = 0): Select { $this->limit = [$limit, $offset]; return $this; } public function resetLimit(): void { $this->limit = null; } public function paginate(int $page, int $itemsPerPage): Select { $this->limit($itemsPerPage, ($page - 1) * $itemsPerPage); return $this; } public function execute(): IResultSet { list($query, $params) = $this->generateQuery(); return $this->connection->prepare($query)->execute($params); } public function count(): int { if (count($this->groups) > 0 || count($this->conditions[self::CONDITION_HAVING]) > 0) { $orders = $this->orders; $this->orders = []; list($query, $params) = $this->generateQuery(); $result = $this->connection->prepare('SELECT COUNT(*) num_rows FROM (' . $query . ') x') ->execute($params) ->fetch(IResultSet::FETCH_NUM); $this->orders = $orders; return $result[0]; } else { $columns = $this->columns; $orders = $this->orders; $this->columns = [new RawExpression('COUNT(*) num_rows')]; $this->orders = []; list($query, $params) = $this->generateQuery(); $result = $this->connection->prepare($query) ->execute($params) ->fetch(IResultSet::FETCH_NUM); $this->columns = $columns; $this->orders = $orders; return $result[0]; } } private function isDerivedTable(): bool { return array_key_exists(Select::DERIVED_TABLE_KEY, $this->tableAliases); } private function addJoin(string $type, $table, $column1, string $relation, $column2): void { $this->joins[] = [$type, $table, $column1, $relation, $column2]; } private function addWhereCondition(string $logic, $column, string $relation, $value): void { $this->conditions[self::CONDITION_WHERE][] = [$logic, $column, $relation, $value]; } private function addHavingCondition(string $logic, $column, string $relation, $value): void { $this->conditions[self::CONDITION_HAVING][] = [$logic, $column, $relation, $value]; } private function generateQuery(): array { list($tableQuery, $tableParams) = $this->generateTable($this->table, true); $queryString = 'SELECT ' . $this->generateColumns() . ' FROM ' . $tableQuery; if (count($this->joins) > 0) { list($joinQuery, $joinParams) = $this->generateJoins(); $queryString .= ' ' . $joinQuery; } else { $joinParams = []; } if (count($this->conditions[self::CONDITION_WHERE]) > 0) { list($wheres, $whereParams) = $this->generateConditions(self::CONDITION_WHERE); $queryString .= ' WHERE ' . $wheres; } else { $whereParams = []; } if (count($this->groups) > 0) { $queryString .= ' GROUP BY ' . $this->generateGroupBy(); } if (count($this->conditions[self::CONDITION_HAVING]) > 0) { list($havings, $havingParams) = $this->generateConditions(self::CONDITION_HAVING); $queryString .= ' HAVING ' . $havings; } else { $havingParams = []; } if (count($this->orders) > 0) { $queryString .= ' ORDER BY ' . $this->generateOrderBy(); } if (isset($this->limit)) { $queryString .= ' LIMIT ' . $this->limit[1] . ', ' . $this->limit[0]; } if($this->isDerivedTable()) { $queryString = '(' . $queryString . ') AS ' . $this->tableAliases[Select::DERIVED_TABLE_KEY]; } return [$queryString, array_merge($tableParams, $joinParams, $whereParams, $havingParams)]; } private function generateTable($table, bool $defineAlias = false): array { $params = []; if ($table instanceof RawExpression) { return [(string) $table, $params]; } if($table instanceof Select) { return $table->generateQuery(); } if (isset($this->tableAliases[$table])) { $queryString = ($defineAlias ? Utils::backtick($this->tableAliases[$table]) . ' ' . Utils::backtick($table) : Utils::backtick($table)); return [$queryString, $params]; } return [Utils::backtick($table), $params]; } private function generateColumn($column): string { if ($column instanceof RawExpression) { return (string) $column; } if (is_array($column)) { $out = ''; if ($column[0]) { list($tableName, $params) = $this->generateTable($column[0]); $out .= $tableName . '.'; } $out .= Utils::backtick($column[1]); if (!empty($column[2])) { $out .= ' ' . Utils::backtick($column[2]); } return $out; } else { return Utils::backtick($column); } } private function generateColumns(): string { $columns = $this->columns; array_walk($columns, function (&$value, $key) { $value = $this->generateColumn($value); }); return implode(',', $columns); } private function generateJoins(): array { $joinQueries = []; $params = []; foreach($this->joins as $join) { list($joinQueryFragment, $paramsFragment) = $this->generateTable($join[1], true); $joinQueries[] = $join[0] . ' JOIN ' . $joinQueryFragment . ' ON ' . $this->generateColumn($join[2]) . ' ' . $join[3] . ' ' . $this->generateColumn($join[4]); $params = array_merge($params, $paramsFragment); } return [implode(' ', $joinQueries), $params]; } private function generateConditions(int $type): array { $conditions = ''; $params = []; foreach ($this->conditions[$type] as $condition) { list($logic, $column, $relation, $value) = $condition; if ($column instanceof Closure) { list($conditionsStringFragment, $paramsFragment) = $this->generateComplexConditionFragment($type, $column); } else { list($conditionsStringFragment, $paramsFragment) = $this->generateConditionFragment($condition); } if ($conditions !== '') { $conditions .= ' ' . $logic . ' '; } $conditions .= $conditionsStringFragment; $params = array_merge($params, $paramsFragment); } return [$conditions, $params]; } private function generateConditionFragment(array $condition): array { list($logic, $column, $relation, $value) = $condition; if ($column instanceof RawExpression) { return [(string) $column, []]; } $conditionsString = $this->generateColumn($column) . ' '; if ($value === null) { return [$conditionsString . ($relation == '=' ? 'IS NULL' : 'IS NOT NULL'), []]; } $conditionsString .= strtoupper($relation) . ' ';; switch ($relation = strtolower($relation)) { case 'between': $params = [$value[0], $value[1]]; $conditionsString .= '? AND ?'; break; case 'in': case 'not in': $params = $value; if (count($value) > 0) { $conditionsString .= '(' . implode(', ', array_fill(0, count($value), '?')) . ')'; } else { $conditionsString = $relation == 'in' ? '0' : '1'; } break; default: $params = [$value]; $conditionsString .= '?'; } return [$conditionsString, $params]; } private function generateComplexConditionFragment(int $type, Closure $conditionCallback): array { $instance = new self($this->connection, $this->table); $instance->tableAliases = $this->tableAliases; $conditionCallback($instance); list($conditions, $params) = $instance->generateConditions($type); return ['(' . $conditions . ')', $params]; } private function generateGroupBy(): string { $groups = $this->groups; array_walk($groups, function (&$value, $key) { $value = $this->generateColumn($value); }); return implode(',', $groups); } private function generateOrderBy(): string { $orders = $this->orders; array_walk($orders, function (&$value, $key) { $value = $this->generateColumn($value[0]) . ' ' . strtoupper($value[1]); }); return implode(',', $orders); } }