<?php declare(strict_types=1);
use function regexInArray;
use BadMethodCallException; use Query\Drivers\DriverInterface;
class QueryBuilder implements QueryBuilderInterface {
protected $explain = FALSE;
public function __construct(DriverInterface $driver, QueryParser $parser)
$this->state = new State();
public function __destruct()
public function __call(string $name, array $params) if (method_exists($this->driver, $name)) return \call_user_func_array([$this->driver, $name], $params);
throw new BadMethodCallException('Method does not exist');
public function select(string $fields): QueryBuilderInterface $fieldsArray = explode(',', $fields); $fieldsArray = array_map('mb_trim', $fieldsArray);
foreach ($fieldsArray as $key => $field) if (stripos($field, 'as') !== FALSE) $fieldsArray[$key] = preg_split('` as `i', $field); $fieldsArray[$key] = array_map('mb_trim', $fieldsArray[$key]);
$safeArray = $this->driver->quoteIdent($fieldsArray);
for($i = 0, $c = count($safeArray); $i < $c; $i++) if (\is_array($safeArray[$i])) $safeArray[$i] = implode(' AS ', $safeArray[$i]);
$this->state->appendSelectString(implode(', ', $safeArray));
public function selectMax(string $field, $as=FALSE): QueryBuilderInterface $this->state->appendSelectString(' MAX'.$this->_select($field, $as));
public function selectMin(string $field, $as=FALSE): QueryBuilderInterface $this->state->appendSelectString(' MIN'.$this->_select($field, $as));
public function selectAvg(string $field, $as=FALSE): QueryBuilderInterface $this->state->appendSelectString(' AVG'.$this->_select($field, $as));
public function selectSum(string $field, $as=FALSE): QueryBuilderInterface $this->state->appendSelectString(' SUM'.$this->_select($field, $as));
public function returning(string $fields = '*'): QueryBuilderInterface
public function distinct(): QueryBuilderInterface $this->state->setSelectString(' DISTINCT' . $this->state->getSelectString());
public function explain(): QueryBuilderInterface
public function from(string $tblname): QueryBuilderInterface $identArray = explode(' ', \mb_trim($tblname)); $identArray = array_map('\\mb_trim', $identArray);
$identArray[0] = $this->driver->quoteTable($identArray[0]); $identArray = $this->driver->quoteIdent($identArray);
$this->state->setFromString(implode(' ', $identArray));
public function like(string $field, $val, string $pos='both'): QueryBuilderInterface return $this->_like($field, $val, $pos);
public function orLike(string $field, $val, string $pos='both'): QueryBuilderInterface return $this->_like($field, $val, $pos, 'LIKE', 'OR');
public function notLike(string $field, $val, string $pos='both'): QueryBuilderInterface return $this->_like($field, $val, $pos, 'NOT LIKE');
public function orNotLike(string $field, $val, string $pos='both'): QueryBuilderInterface return $this->_like($field, $val, $pos, 'NOT LIKE', 'OR');
public function having($key, $val=[]): QueryBuilderInterface return $this->_having($key, $val);
public function orHaving($key, $val=[]): QueryBuilderInterface return $this->_having($key, $val, 'OR');
public function where($key, $val=[], $escape=NULL): QueryBuilderInterface return $this->_whereString($key, $val);
public function orWhere($key, $val=[]): QueryBuilderInterface return $this->_whereString($key, $val, 'OR');
public function whereIn($field, $val=[]): QueryBuilderInterface return $this->_whereIn($field, $val);
public function orWhereIn($field, $val=[]): QueryBuilderInterface return $this->_whereIn($field, $val, 'IN', 'OR');
public function whereNotIn($field, $val=[]): QueryBuilderInterface return $this->_whereIn($field, $val, 'NOT IN');
public function orWhereNotIn($field, $val=[]): QueryBuilderInterface return $this->_whereIn($field, $val, 'NOT IN', 'OR');
public function set($key, $val = NULL): QueryBuilderInterface
$keys = array_keys($pairs); $values = array_values($pairs);
$this->state->appendSetArrayKeys($keys); $this->state->appendValues($values);
$this->state->setSetArrayKeys( array_map([$this->driver, '_quote'], $this->state->getSetArrayKeys())
$setString = implode('=?,', $this->state->getSetArrayKeys());
$this->state->setSetString($setString);
public function join(string $table, string $condition, string $type=''): QueryBuilderInterface $table = explode(' ', mb_trim($table)); $table[0] = $this->driver->quoteTable($table[0]); $table = $this->driver->quoteIdent($table); $table = implode(' ', $table);
$parsedCondition = $this->parser->compileJoin($condition); $condition = $table . ' ON ' . $parsedCondition;
$this->state->appendMap("\n" . strtoupper($type) . ' JOIN ', $condition, 'join');
public function groupBy($field): QueryBuilderInterface if ( ! is_scalar($field)) $newGroupArray = array_map([$this->driver, 'quoteIdent'], $field); $this->state->setGroupArray( array_merge($this->state->getGroupArray(), $newGroupArray) $this->state->appendGroupArray($this->driver->quoteIdent($field));
$this->state->setGroupString(' GROUP BY ' . implode(',', $this->state->getGroupArray()));
public function orderBy(string $field, string $type=''): QueryBuilderInterface if (stripos($type, 'rand') !== FALSE) $rand = $this->driver->getSql()->random();
$field = $this->driver->quoteIdent($field); $this->state->setOrderArray($field, $type);
foreach($this->state->getOrderArray() as $k => $v) $orderClauses[] = $k . ' ' . strtoupper($v);
$orderString = ! isset($rand) ? "\nORDER BY ".implode(', ', $orderClauses)
$this->state->setOrderString($orderString);
public function limit(int $limit, $offset=FALSE): QueryBuilderInterface $this->state->setLimit($limit); $this->state->setOffset($offset);
public function groupStart(): QueryBuilderInterface $conj = empty($this->state->getQueryMap()) ? ' WHERE ' : ' ';
$this->state->appendMap($conj, '(', 'group_start');
public function notGroupStart(): QueryBuilderInterface $conj = empty($this->state->getQueryMap()) ? ' WHERE ' : ' AND ';
$this->state->appendMap($conj, ' NOT (', 'group_start');
public function orGroupStart(): QueryBuilderInterface $this->state->appendMap('', ' OR (', 'group_start');
public function orNotGroupStart(): QueryBuilderInterface $this->state->appendMap('', ' OR NOT (', 'group_start');
public function groupEnd(): QueryBuilderInterface $this->state->appendMap('', ')', 'group_end');
public function get(string $table='', $limit=FALSE, $offset=FALSE): PDOStatement
$this->limit($limit, $offset);
return $this->_run('get', $table);
public function getWhere(string $table, $where=[], $limit=FALSE, $offset=FALSE): PDOStatement
return $this->get($table, $limit, $offset);
public function countAll(string $table): int $sql = 'SELECT * FROM '.$this->driver->quoteTable($table); $res = $this->driver->query($sql); return (int) count($res->fetchAll());
public function countAllResults(string $table='', bool $reset = TRUE): int
$result = $this->_run('get', $table, NULL, NULL, $reset); $rows = $result->fetchAll();
return (int) count($rows);
public function insert(string $table, $data=[]): PDOStatement
return $this->_run('insert', $table);
public function insertBatch(string $table, $data=[]): PDOStatement [$sql, $data] = $this->driver->insertBatch($table, $data);
? $this->_run('', $table, $sql, $data)
public function update(string $table, $data=[]): PDOStatement
return $this->_run('update', $table);
public function updateBatch(string $table, array $data, string $where): ?int if (empty($table) || empty($data) || empty($where))
[$sql, $data, $affectedRows] = $this->driver->updateBatch($table, $data, $where);
$this->_run('', $table, $sql, $data);
public function delete(string $table, $where=''): PDOStatement
return $this->_run('delete', $table);
public function getCompiledSelect(string $table='', bool $reset=TRUE): string
return $this->_getCompile('select', $table, $reset);
public function getCompiledInsert(string $table, bool $reset=TRUE): string return $this->_getCompile('insert', $table, $reset);
public function getCompiledUpdate(string $table='', bool $reset=TRUE): string return $this->_getCompile('update', $table, $reset);
public function getCompiledDelete(string $table='', bool $reset=TRUE): string return $this->_getCompile('delete', $table, $reset);
public function resetQuery(): void $this->state = new State();
protected function _select(string $field, $as = FALSE): string $field = $this->driver->quoteIdent($field);
$as = $this->driver->quoteIdent($as); return "({$field}) AS {$as} ";
protected function _getCompile(string $type, string $table, bool $reset): string $sql = $this->_compile($type, $table);
protected function _like(string $field, $val, string $pos, string $like='LIKE', string $conj='AND'): QueryBuilderInterface $field = $this->driver->quoteIdent($field);
$like = $field. " {$like} ?";
elseif ($pos === 'after')
$conj = empty($this->state->getQueryMap()) ? ' WHERE ' : " {$conj} "; $this->state->appendMap($conj, $like, 'like');
$this->state->appendWhereValues($val);
protected function _having($key, $values=[], string $conj='AND'): QueryBuilderInterface $where = $this->_where($key, $values);
foreach($where as $f => $val) $fArray = explode(' ', trim($f));
$item = $this->driver->quoteIdent($fArray[0]);
$item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?";
$this->state->appendHavingMap([ 'conjunction' => empty($this->state->getHavingMap())
protected function _where($key, $val=[]): array
foreach($pairs as $k => $v) $this->state->appendWhereValues($v);
protected function _whereString($key, $values=[], string $defaultConj='AND'): QueryBuilderInterface foreach($this->_where($key, $values) as $f => $val) $queryMap = $this->state->getQueryMap();
$fArray = explode(' ', trim($f));
$item = $this->driver->quoteIdent($fArray[0]);
$item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?"; $lastItem = end($queryMap);
$conjunctionList = array_column($queryMap, 'conjunction'); if (empty($queryMap) || ( ! regexInArray($conjunctionList, "/^ ?\n?WHERE/i"))) elseif ($lastItem['type'] === 'group_start') $conj = " {$defaultConj} ";
$this->state->appendMap($conj, $item, 'where');
protected function _whereIn($key, $val=[], string $in='IN', string $conj='AND'): QueryBuilderInterface $key = $this->driver->quoteIdent($key); $params = array_fill(0, count($val), '?'); $this->state->appendWhereValues($val);
$conjunction = empty($this->state->getQueryMap()) ? ' WHERE ' : " {$conj} "; $str = $key . " {$in} (".implode(',', $params).') ';
$this->state->appendMap($conjunction, $str, 'where_in');
protected function _run(string $type, string $table, string $sql=NULL, array $vals=NULL, bool $reset=TRUE): PDOStatement $sql = $this->_compile($type, $table);
$vals = array_merge($this->state->getValues(), $this->state->getWhereValues());
$startTime = microtime(TRUE);
? $this->driver->query($sql) : $this->driver->prepareExecute($sql, $vals);
$endTime = microtime(TRUE); $totalTime = number_format($endTime - $startTime, 5);
$this->_appendQuery($vals, $sql, (int) $totalTime);
protected function _appendQuery(array $values, string $sql, int $totalTime): void $evals = \is_array($values) ? $values : []; $esql = str_replace('?', '%s', $sql);
? htmlentities($this->driver->quote($v), ENT_NOQUOTES, 'utf-8')
array_unshift($evals, $esql);
'sql' => sprintf(...$evals)
$this->queries['total_time'] += $totalTime;
$this->driver->setLastQuery($sql);
protected function _compileType(string $type='', string $table=''): string $setArrayKeys = $this->state->getSetArrayKeys(); $paramCount = count($setArrayKeys); $params = array_fill(0, $paramCount, '?'); $sql = "INSERT INTO {$table} (" . implode(',', $setArrayKeys) . ")\nVALUES (".implode(',', $params).')';
$setString = $this->state->getSetString(); $sql = "UPDATE {$table}\nSET {$setString}";
$sql = "DELETE FROM {$table}";
$fromString = $this->state->getFromString(); $selectString = $this->state->getSelectString();
$sql = "SELECT * \nFROM {$fromString}";
if ( ! empty($selectString)) $sql = str_replace('*', $selectString, $sql);
protected function _compile(string $type='', string $table=''): string $sql = $this->_compileType($type, $this->driver->quoteTable($table));
foreach($clauses as $clause) $func = 'get' . ucFirst($clause); $param = $this->state->$func(); $sql .= $q['conjunction'] . $q['string'];
$limit = $this->state->getLimit(); $sql = $this->driver->getSql()->limit($sql, $limit, $this->state->getOffset());
if ($this->explain === TRUE) $sql = $this->driver->getSql()->explain($sql);
|