Files
Batflat/inc/core/lib/QueryBuilder.php
2018-03-12 13:23:44 +01:00

898 lines
21 KiB
PHP

<?php
/**
* This file is part of Batflat ~ the lightweight, fast and easy CMS
*
* @author Paweł Klockiewicz <klockiewicz@sruu.pl>
* @author Wojciech Król <krol@sruu.pl>
* @copyright 2017 Paweł Klockiewicz, Wojciech Król <Sruu.pl>
* @license https://batflat.org/license
* @link https://batflat.org
*/
namespace Inc\Core\Lib;
/**
* Batflat QueryBuilder class
*/
class QueryBuilder
{
protected static $db = null;
protected static $last_sqls = [];
protected static $options = [];
protected $table = null;
protected $columns = [];
protected $joins = [];
protected $conditions = [];
protected $condition_binds = [];
protected $sets = [];
protected $set_binds = [];
protected $orders = [];
protected $group_by = [];
protected $having = [];
protected $limit = '';
protected $offset = '';
/**
* constructor
*
* @param string $table
*/
public function __construct($table = null)
{
if ($table) {
$this->table = $table;
}
}
/**
* PDO instance
*
* @return PDO
*/
public static function pdo()
{
return static::$db;
}
/**
* last SQL queries
*
* @return array SQLs array
*/
public static function lastSqls()
{
return static::$last_sqls;
}
/**
* creates connection with database
*
* Qb::connect($dsn); // default user, password and options
* Qb::connect($dsn, $user); // default password and options
* Qb::connect($dsn, $user, $pass); // default options
* Qb::connect($dsn, $user, $pass, $options);
* Qb::connect($dsn, $options);
* Qb::connect($dsn, $user, $options);
*
* @param string $dsn
* @param string $user
* @param string $pass
* @param array $options
* primary_key: primary column name, default: 'id'
* error_mode: default: \PDO::ERRMODE_EXCEPTION
* json_options: default: JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT
*/
public static function connect($dsn, $user = '', $pass = '', $options = [])
{
if (is_array($user)) {
$options = $user;
$user = '';
$pass = '';
} elseif (is_array($pass)) {
$options = $pass;
$pass = '';
}
static::$options = array_merge([
'primary_key' => 'id',
'error_mode' => \PDO::ERRMODE_EXCEPTION,
'json_options' => JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT,
], $options);
static::$db = new \PDO($dsn, $user, $pass);
static::$db->setAttribute(\PDO::ATTR_ERRMODE, static::$options['error_mode']);
if (strpos($dsn, 'sqlite') !== false) {
static::$db->exec("pragma synchronous = off;");
}
}
/**
* close connection with database
*/
public static function close()
{
static::$db = null;
}
/**
* get or set options
*
* @param string $name
* @param mixed $value
*/
public static function config($name, $value = null)
{
if ($value === null) {
return static::$options[$name];
} else {
static::$options[$name] = $value;
}
}
/**
* SELECT
*
* select('column1')->select('column2') // SELECT column1, column2
* select(['column1', 'column2', ...]) // SELECT column1, column2, ...
* select(['alias1' => 'column1', 'column2', ...]) // SELECT column1 AS alias1, column2, ...
*
* @param string|array $columns
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function select($columns)
{
if (!is_array($columns)) {
$columns = array($columns);
}
foreach ($columns as $alias => $column) {
if (!is_numeric($alias)) {
$column .= " AS $alias";
}
array_push($this->columns, $column);
}
return $this;
}
/**
* INNER JOIN
*
* @param string $table
* @param string $condition
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function join($table, $condition)
{
array_push($this->joins, "INNER JOIN $table ON $condition");
return $this;
}
/**
* LEFT OUTER JOIN
*
* @param string $table
* @param string $condition
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function leftJoin($table, $condition)
{
array_push($this->joins, "LEFT JOIN $table ON $condition");
return $this;
}
/**
* HAVING
*
* having(aggregate_function, operator, value) // HAVING aggregate_function (=, <, >, <=, >=, <>) value
* having(aggregate_function, value) // HAVING aggregate_function = value
*
* @param string $aggregate_function
* @param mixed $value
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function having($aggregate_function, $operator, $value = null, $ao = 'AND')
{
if ($value === null) {
$value = $operator;
$operator = '=';
}
if (is_array($value)) {
$qs = '(' . implode(',', array_fill(0, count($value), '?')) . ')';
if (empty($this->having)) {
array_push($this->having, "$aggregate_function $operator $qs");
} else {
array_push($this->having, "$ao $aggregate_function $operator $qs");
}
foreach ($value as $v) {
array_push($this->condition_binds, $v);
}
} else {
if (empty($this->having)) {
array_push($this->having, "$aggregate_function $operator ?");
} else {
array_push($this->having, "$ao $aggregate_function $operator ?");
}
array_push($this->condition_binds, $value);
}
return $this;
}
public function orHaving($aggregate_function, $operator, $value = null)
{
return $this->having($aggregate_function, $operator, $value, 'OR');
}
/**
* WHERE
*
* where(column, operator, value) // WHERE column (=, <, >, <=, >=, <>) value
* where(column, value) // WHERE column = value
* where(value) // WHERE id = value
* where(function($st) {
* $st->where()...
* })
*
* @param mixed $column
* @param mixed $value
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function where($column, $operator = null, $value = null, $ao = 'AND')
{
// Where group
if (!is_string($column) && is_callable($column)) {
if (empty($this->conditions) || strpos(end($this->conditions), '(') !== false) {
array_push($this->conditions, '(');
} else {
array_push($this->conditions, $ao.' (');
}
call_user_func($column, $this);
array_push($this->conditions, ')');
return $this;
}
if ($operator === null) {
$value = $column;
$column = static::$options['primary_key'];
$operator = '=';
} elseif ($value === null) {
$value = $operator;
$operator = '=';
}
if (is_array($value)) {
foreach ($value as $v) {
array_push($this->condition_binds, $v);
}
$value = '(' . implode(',', array_fill(0, count($value), '?')) . ')';
} else {
array_push($this->condition_binds, $value);
}
if (empty($this->conditions) || strpos(end($this->conditions), '(') !== false) {
array_push($this->conditions, "$column $operator ?");
} else {
array_push($this->conditions, "$ao $column $operator ?");
}
return $this;
}
/**
* OR WHERE
*
* orWhere(column, operator, value) // WHERE column (=, <, >, <=, >=, <>) value
* orWhere(column, value) // WHERE column = value
* orWhere(value) // WHERE id = value
* orWhere(function($st) {
* $st->where()...
* })
*
* @param mixed $column
* @param mixed $value
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function orWhere($column, $operator = null, $value = null)
{
return $this->where($column, $operator, $value, 'OR');
}
/**
* WHERE IS NULL
*
* @param string $column
* @param string $ao
* @return \Inc\Core\Lib\QueryBuilder
*/
public function isNull($column, $ao = 'AND')
{
if (is_array($column)) {
foreach ($column as $c) {
$this->isNull($c, $ao);
}
return $this;
}
if (empty($this->conditions) || strpos(end($this->conditions), '(') !== false) {
array_push($this->conditions, "$column IS NULL");
} else {
array_push($this->conditions, "$ao $column IS NULL");
}
return $this;
}
/**
* WHERE IS NOT NULL
*
* @param string $column
* @param string $ao
* @return \Inc\Core\Lib\QueryBuilder
*/
public function isNotNull($column, $ao = 'AND')
{
if (is_array($column)) {
foreach ($column as $c) {
$this->isNotNull($c, $ao);
}
return $this;
}
if (empty($this->conditions) || strpos(end($this->conditions), '(') !== false) {
array_push($this->conditions, "$column IS NOT NULL");
} else {
array_push($this->conditions, "$ao $column IS NOT NULL");
}
return $this;
}
/**
* OR WHERE IS NULL
*
* @param string $column
* @return \Inc\Core\Lib\QueryBuilder
*/
public function orIsNull($column)
{
return $this->isNull($column, 'OR');
}
/**
* OR WHERE IS NOT NULL
*
* @param string $column
* @return \Inc\Core\Lib\QueryBuilder
*/
public function orIsNotNull($column)
{
return $this->isNotNull($column, 'OR');
}
/**
* WHERE LIKE
*
* @param string $column
* @param mixed $value
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function like($column, $value)
{
$this->where($column, 'LIKE', $value);
return $this;
}
/**
* WHERE OR LIKE
*
* @param string $column
* @param mixed $value
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function orLike($column, $value)
{
$this->where($column, 'LIKE', $value, 'OR');
return $this;
}
/**
* WHERE NOT LIKE
*
* @param string $column
* @param mixed $value
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function notLike($column, $value)
{
$this->where($column, 'NOT LIKE', $value);
return $this;
}
/**
* WHERE OR NOT LIKE
*
* @param string $column
* @param mixed $value
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function orNotLike($column, $value)
{
$this->where($column, 'NOT LIKE', $value, 'OR');
return $this;
}
/**
* WHERE IN
*
* @param string $column
* @param array $values
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function in($column, $values)
{
$this->where($column, 'IN', $values);
return $this;
}
/**
* WHERE OR IN
*
* @param string $column
* @param array $values
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function orIn($column, $values)
{
$this->where($column, 'IN', $values, 'OR');
return $this;
}
/**
* WHERE NOT IN
*
* @param string $column
* @param array $values
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function notIn($column, $values)
{
$this->where($column, 'NOT IN', $values);
return $this;
}
/**
* WHERE OR NOT IN
*
* @param string $column
* @param array $values
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function orNotIn($column, $values)
{
$this->where($column, 'NOT IN', $values, 'OR');
return $this;
}
/**
* get or set column value
*
* @param string $column
* @param mixed $value
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function set($column, $value = null)
{
if (is_array($column)) {
$sets = $column;
} else {
$sets = [$column => $value];
}
$this->sets += $sets;
return $this;
}
/**
* UPDATE or INSERT
*
* @param string $column
* @param mixed $value
*
* @return integer / boolean
*/
public function save($column = null, $value = null)
{
if ($column) {
$this->set($column, $value);
}
$st = $this->_build();
if ($lid = static::$db->lastInsertId()) {
return $lid;
} else {
return $st;
}
}
/**
* UPDATE
*
* @param string $column
* @param mixed $value
*
* @return boolean
*/
public function update($column = null, $value = null)
{
if ($column) {
$this->set($column, $value);
}
return $this->_build(['only_update' => true]);
}
/**
* ORDER BY ASC
*
* @param string $column
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function asc($column)
{
array_push($this->orders, "$column ASC");
return $this;
}
/**
* ORDER BY DESC
*
* @param string $column
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function desc($column)
{
array_push($this->orders, "$column DESC");
return $this;
}
/**
* GROUP BY
*
* @param mixed $column
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function group($columns)
{
if (is_array($columns)) {
foreach ($columns as $column) {
array_push($this->group_by, "$column");
}
} else {
array_push($this->group_by, "$columns");
}
return $this;
}
/**
* LIMIT
*
* @param integer $num
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function limit($num)
{
$this->limit = " LIMIT $num";
return $this;
}
/**
* OFFSET
*
* @param integer $num
*
* @return \Inc\Core\Lib\QueryBuilder
*/
public function offset($num)
{
$this->offset = " OFFSET $num";
return $this;
}
/**
* create array with all rows
*
* @return array
*/
public function toArray()
{
$st = $this->_build();
return $st->fetchAll(\PDO::FETCH_ASSOC);
}
/**
* create object with all rows
*
* @return \stdObject[]
*/
public function toObject()
{
$st = $this->_build();
return $st->fetchAll(\PDO::FETCH_OBJ);
}
/**
* create JSON array with all rows
*
* @return string
*/
public function toJson()
{
$rows = $this->toArray();
return json_encode($rows, static::$options['json_options']);
}
/**
* create array with one row
*
* @param string $column
* @param mixed $value
*
* @return array
*/
public function oneArray($column = null, $value = null)
{
if ($column !== null) {
$this->where($column, $value);
}
$st = $this->_build();
return $st->fetch(\PDO::FETCH_ASSOC);
}
/**
* create object with one row
*
* @param string $column
* @param mixed $value
*
* @return \stdObject
*/
public function oneObject($column = null, $value = null)
{
if ($column !== null) {
$this->where($column, $value);
}
$st = $this->_build();
return $st->fetch(\PDO::FETCH_OBJ);
}
/**
* create JSON array with one row
*
* @param string $column
* @param mixed $value
*
* @return string
*/
public function oneJson($column = null, $value = null)
{
if ($column !== null) {
$this->where($column, $value);
}
$row = $this->oneArray();
return json_encode($row, static::$options['json_options']);
}
/**
* returns rows count
*
* @return integer
*/
public function count()
{
$st = $this->_build('count');
return $st->fetchColumn();
}
/**
* Last inserted id
*
* @return integer
*/
public function lastInsertId()
{
return static::$db->lastInsertId();
}
/**
* DELETE
*
* @param string $column
* @param mixed $value
*/
public function delete($column = null, $value = null)
{
if ($column !== null) {
$this->where($column, $value);
}
$st = $this->_build('delete');
return $st->rowCount();
}
/**
* Create SQL query
*
* @param $type `default`, `delete`, `count`
*
* @return string
*/
public function toSql($type = 'default')
{
$sql = '';
$sql_where = '';
$sql_having = '';
// build conditions
$conditions = implode(' ', $this->conditions);
$conditions = str_replace(['( ', ' )'], ['(', ')'], $conditions);
if ($conditions) {
$sql_where .= " WHERE $conditions";
}
// build having
$having = implode(' ', $this->having);
if ($having) {
$sql_having .= " HAVING $having";
}
// if some columns have set value then UPDATE or INSERT
if ($this->sets) {
// get table columns
$table_cols = $this->_getColumns();
// Update updated_at column if exists
if (in_array('updated_at', $table_cols) && !array_key_exists('updated_at', $this->sets)) {
$this->set('updated_at', time());
}
// if there are some conditions then UPDATE
if (!empty($this->conditions)) {
$insert = false;
$columns = implode('=?,', array_keys($this->sets)) . '=?';
$this->set_binds = array_values($this->sets);
$sql = "UPDATE $this->table SET $columns";
$sql .= $sql_where;
return $sql;
}
// if there aren't conditions, then INSERT
else {
// Update created_at column if exists
if (in_array('created_at', $table_cols) && !array_key_exists('created_at', $this->sets)) {
$this->set('created_at', time());
}
$columns = implode(',', array_keys($this->sets));
$this->set_binds = array_values($this->sets);
$qs = implode(',', array_fill(0, count($this->sets), '?'));
$sql = "INSERT INTO $this->table($columns) VALUES($qs)";
$this->condition_binds = array();
return $sql;
}
} else {
if ($type == 'delete') {
// DELETE
$sql = "DELETE FROM $this->table";
$sql .= $sql_where;
return $sql;
} else {
// SELECT
$columns = implode(',', $this->columns);
if (!$columns) {
$columns = '*';
}
if ($type == 'count') {
$columns = "COUNT($columns) AS count";
}
$sql = "SELECT $columns FROM $this->table";
$joins = implode(' ', $this->joins);
if ($joins) {
$sql .= " $joins";
}
$order = '';
if (count($this->orders) > 0) {
$order = ' ORDER BY ' . implode(',', $this->orders);
}
$group_by = '';
if (count($this->group_by) > 0) {
$group_by = ' GROUP BY ' . implode(',', $this->group_by);
}
$sql .= $sql_where . $group_by . $order . $sql_having . $this->limit . $this->offset;
return $sql;
}
}
return null;
}
/**
* build SQL query
*
* @param array $type `default`, `delete`, `count`
*
* @return PDOStatement
*/
protected function _build($type = 'default')
{
return $this->_query($this->toSql($type));
}
/**
* execute SQL query
*
* @param string $sql
*
* @return PDOStatement
*/
protected function _query($sql)
{
$binds = array_merge($this->set_binds, $this->condition_binds);
$st = static::$db->prepare($sql);
foreach ($binds as $key => $bind) {
$pdo_param = \PDO::PARAM_STR;
if (is_numeric($bind)) {
$pdo_param = \PDO::PARAM_INT;
}
$st->bindValue($key+1, $bind, $pdo_param);
}
$st->execute();
static::$last_sqls[] = $sql;
return $st;
}
/**
* Get current table columns
*
* @return array
*/
protected function _getColumns()
{
$q = $this->pdo()->query("PRAGMA table_info(".$this->table.")")->fetchAll();
return array_column($q, 'name');
}
}