DBmodel/src/DBmodel.php

1455 lines
42 KiB
PHP

<?php
/*
Copyright (c) TPsoft.org 2000-2025
Author: Ing. Igor Mino <mino@tpsoft.org>
License: GNU GPL-3.0 or later
Description: This library extends the builtin PDO object by several useful features.
Version: 1.0.0
Milestones:
2015-10-09 23:52 Igor - Created for Beteha
2015-12-01 23:09 Igor - Added LEFT a RIGHT JOIN, added flag DEBUG, userd for print of query
2016-07-28 09:44 Igor - Added return of associative array
2025-05-27 20:43 Igor - Forked from Beteha to standalone public library
*/
namespace TPsoft\DBmodel;
class DBmodel
{
public static DBmodel $instance;
public \PDO $dbh;
public \PDOStatement $stmt;
private $type = 'mysql';
private $oldErrorHandler;
public $debug = false;
public $log = null;
public $tables = array();
public function __construct(string $dsn = null, ?string $username = null, #[\SensitiveParameter] ?string $password = null, ?array $options = null)
{
if (is_null($dsn)) {
if (DBmodel::$instance) {
$this->dbh = DBmodel::$instance->dbh;
} else {
throw new \Exception('DB handler from DBmodel::$instance is null');
}
} else {
$this->dbh = new \PDO($dsn, $username, $password, $options);
DBmodel::$instance = $this;
}
if (is_null($this->dbh)) {
throw new \Exception('DB handler is null');
}
}
public function _debug($msg)
{
if (!is_null($this->log)) {
call_user_func_array($this->log, array($msg));
}
if ((is_bool($this->debug) && $this->debug == true)
|| (is_numeric($this->debug) && $this->debug-- <= 0)
) {
throw new \Exception($msg);
}
}
/* ----------------------------------------------------
* CORE METHODS
*/
public function tableInfoExist($table_id, $info)
{
return (isset($this->tables)
&& isset($this->tables[$table_id])
&& isset($this->tables[$table_id][$info]));
}
public function tableInfo($table_id, $info)
{
if (
!isset($this->tables)
|| !isset($this->tables[$table_id])
|| !isset($this->tables[$table_id][$info])
) {
throw new \Exception('Error: Incorrect table "' . $info . '" for table ID "' . $table_id . '"');
}
return $this->tables[$table_id][$info];
}
public function tableInfoSet($table_id, $info, $content)
{
$this->tables[$table_id][$info] = $content;
}
public function tableName($table_id)
{
return $this->tableInfo($table_id, 'name');
}
public function primaryKeyName($table_id)
{
return $this->tableInfo($table_id, 'primary_key_name');
}
public function allowAttributes($table_id)
{
$allow_attributes = $this->tableInfo($table_id, 'allow_attributes');
if (count($allow_attributes) <= 0) return array();
$keys = array_keys($allow_attributes);
return (strlen($keys[0]) <= 1) ? $allow_attributes : array_keys($allow_attributes);
}
public function typesAttributes($table_id)
{
if ($this->tableInfoExist($table_id, 'allow_attributes_types')) {
return $this->tableInfo($table_id, 'allow_attributes_types');
}
$allow_attributes = $this->tableInfo($table_id, 'allow_attributes');
if (count($allow_attributes) <= 0) return false;
$keys = array_keys($allow_attributes);
if (strlen($keys[0]) <= 1) {
$this->tableInfoSet($table_id, 'allow_attributes_types', false);
return false;
}
$types = array();
foreach ($allow_attributes as $key => $type) {
$type = str_replace(' ', '', $type);
$type = str_replace('(', '|', $type);
$type = str_replace(')', '|', $type);
$type_parts = explode('|', $type);
$rettype = array(
'format' => strtolower(@$type_parts[0]),
'length' => @$type_parts[1]
);
if (strtolower($rettype['format']) == 'enum') {
$allowed_values = explode(',', $rettype['length']);
foreach ($allowed_values as $index => $part) $allowed_values[$index] = trim($part, '"\'');
$rettype['allowed_values'] = $allowed_values;
$rettype['length'] = count($allowed_values);
}
$types[$key] = $rettype;
}
$this->tableInfoSet($table_id, 'allow_attributes_types', $types);
return $types;
}
private function buildWherePrimaryKey($table_id, $primary_key)
{
$primary_key_name = $this->primaryKeyName($table_id);
if (is_array($primary_key_name)) {
if (!is_array($primary_key)) {
throw new \Exception('Error: table ' . $table_id . ' required multikey select');
}
$conds = array();
foreach ($primary_key_name as $name) {
$conds[] = sprintf('`%s` = %s', $name, $this->quote($primary_key[$name]));
}
$where = implode(' AND ', $conds);
} else {
$pk = is_array($primary_key) ? $primary_key[$primary_key_name] : $primary_key;
$where = sprintf('`%s` = %s', $primary_key_name, $this->quote($pk));
}
return $where;
}
public function existTable($table_id)
{
$query = sprintf(
'SHOW TABLES LIKE "%s"',
$this->tableName($table_id)
);
return $this->getOne($query) == $this->tableName($table_id);
}
public function existRecord($table_id, $primary_key)
{
if (
is_null($primary_key)
|| (is_string($primary_key) && strlen(trim($primary_key)) <= 0)
) {
return false;
}
$query = sprintf(
'SELECT COUNT(*)'
. ' FROM %s'
. ' WHERE %s',
$this->tableName($table_id),
$this->buildWherePrimaryKey($table_id, $primary_key)
);
return $this->getOne($query) == 1;
}
public function record($table_id, $primary_key = null, $data = array())
{
$action = null;
$action = (!is_null($primary_key) && is_array($data) && count($data) <= 0) ? 'SELECT' : $action;
$action = (is_null($primary_key) && is_array($data) && count($data) > 0) ? 'INSERT' : $action;
$action = (!is_null($primary_key) && is_array($data) && count($data) > 0) ? 'UPDATE' : $action;
$action = (!is_null($primary_key) && is_null($data)) ? 'DELETE' : $action;
if (is_null($action)) {
$this->_debug('[DBmodel][record]: action = null');
return false;
}
$table = $this->tableName($table_id);
$primary_key_name = $this->primaryKeyName($table_id);
$allow_attributes = $this->allowAttributes($table_id);
$data = $this->fixDecimalPoints($table_id, $data);
$data = $this->fixBooleans($table_id, $data);
$data = $this->fixJSONs($table_id, $data);
$data = $this->fixTypes($table_id, $data);
if ($action == 'SELECT') {
if (!$this->existRecord($table_id, $primary_key)) {
return false;
}
$query = sprintf(
'SELECT *'
. ' FROM %s'
. ' WHERE %s'
. ' LIMIT 1',
$table,
$this->buildWherePrimaryKey($table_id, $primary_key)
);
$this->_debug('[DBmodel][record][SELECT]: ' . $query);
$ret = $this->getRow($query);
$ret = $this->unfixJSONs($table_id, $ret);
if ($ret === false) {
throw new \Exception('Error: unfixJSONs failed');
}
return $ret;
}
if ($action == 'INSERT') {
// check all required primary keys
$primary_key_names = is_array($primary_key_name) ? $primary_key_name : array($primary_key_name);
foreach ($primary_key_names as $pk_name) {
if (
in_array($pk_name, $allow_attributes)
&& (!isset($data[$pk_name])
|| is_null($data[$pk_name])
|| strlen($data[$pk_name]) <= 0)
) {
$this->_debug('[DBmodel][record][INSERT]: missing primary key in data');
return false;
}
}
$keys = '';
$values = '';
$delimiter = '';
foreach ($data as $key => $value) {
$key = trim($key);
if (!in_array($key, $allow_attributes)) {
continue;
}
if (is_array($value)) {
$value = implode(',', array_keys($value));
}
$keys .= $delimiter . '`' . $key . '`';
if (is_string($value) && $value === '`NOW`') $value = date('Y-m-d H:i:s');
$values .= (strlen($value) > 0) ? $delimiter . $this->quote($value) : $delimiter . 'NULL';
$delimiter = ',';
}
$query = sprintf(
'INSERT INTO %s'
. ' (%s)'
. ' VALUES'
. ' (%s)',
$table,
$keys,
$values
);
$this->_debug('[DBmodel][record][INSERT]: ' . $query);
$ret = $this->query($query);
if ($ret === false) {
throw new \Exception($this->errorMessage());
}
if ($this->affectedRows() != 1) {
return false;
}
if (count($primary_key_names) > 1) return true;
if (method_exists($this->dbh, 'getLastInsertID')) {
$last_id = $this->getLastInsertID();
} else {
$last_id = $this->getOne('SELECT last_insert_id()');
}
return $last_id;
}
if ($action == 'UPDATE') {
if (!$this->existRecord($table_id, $primary_key)) {
if (is_array($primary_key)) {
return $this->record($table_id, null, array_merge($data, $primary_key));
}
$this->_debug('Record with primary key = ' . json_encode($primary_key) . ' was not exists');
return false;
}
$set = '';
$delimiter = '';
foreach ($data as $key => $value) {
$key = trim($key);
if (!in_array($key, $allow_attributes)) {
continue;
}
if (is_array($value)) {
$value = implode(',', array_keys($value));
}
$q_val = (strlen($value) > 0) ? $this->quote($value) : 'NULL';
$q_val = (substr($value, 0, 1) == '_' && substr($value, -1) == '_') ? substr($value, 1, -1) : $q_val;
$q_val = (is_string($value) && $value === '`NOW`') ? $this->quote(date('Y-m-d H:i:s')) : $q_val;
$set .= $delimiter . '`' . $key . '` = ' . $q_val;
$delimiter = ',';
}
$query = sprintf(
'UPDATE %s'
. ' SET %s'
. ' WHERE %s'
. ' LIMIT 1',
$table,
$set,
$this->buildWherePrimaryKey($table_id, $primary_key)
);
$this->_debug('[DBmodel][record][UPDATE]: ' . $query);
$ret = $this->query($query);
if ($ret === false) {
throw new \Exception($this->errorMessage());
}
/* data is not always updated, if the same data is updated
if ($this->affectedRows() != 1) {
$this->_debug('Error during uprade: ' . $query);
return false;
}
*/
return $primary_key;
}
if ($action == 'DELETE') {
if (!$this->existRecord($table_id, $primary_key)) {
$this->_debug('Record with primary key = "' . $primary_key . '" was not exists');
return false;
}
$query = sprintf(
'DELETE FROM %s'
. ' WHERE %s'
. ' LIMIT 1',
$table,
$this->buildWherePrimaryKey($table_id, $primary_key)
);
$this->_debug('[DBmodel][record][DELETE]: ' . $query);
$ret = $this->query($query);
if ($ret === false) {
throw new \Exception($this->errorMessage());
}
if ($this->affectedRows() != 1) {
return false;
}
return true;
}
return false;
}
public function recordBy($table_id, $colname, $colvalue)
{
if (
is_null($colname) || is_null($colvalue)
|| !is_string($colname) || strlen(trim($colname)) <= 0
|| !is_string($colvalue) || strlen(trim($colvalue)) <= 0
) {
return false;
}
$data = array($colname => $colvalue);
$data = $this->fixDecimalPoints($table_id, $data);
$data = $this->fixBooleans($table_id, $data);
$data = $this->fixJSONs($table_id, $data);
$data = $this->fixTypes($table_id, $data);
if (!is_array($data) || count($data) <= 0) return false;
$allow_attributes = $this->allowAttributes($table_id);
$where = '';
$delimiter = '';
foreach ($data as $key => $value) {
$key = trim($key);
if (!in_array($key, $allow_attributes)) continue;
if (is_array($value)) $value = implode(',', array_keys($value));
$q_val = (strlen($value) > 0) ? $this->quote($value) : 'NULL';
$q_val = (substr($value, 0, 1) == '_' && substr($value, -1) == '_') ? substr($value, 1, -1) : $q_val;
$q_val = (is_string($value) && $value === '`NOW`') ? $this->quote(date('Y-m-d H:i:s')) : $q_val;
$where .= $delimiter . '`' . $key . '` = ' . $q_val;
$delimiter = ' AND ';
}
$query = sprintf(
'SELECT *'
. ' FROM %s'
. ' WHERE %s',
$this->tableName($table_id),
$where
);
$this->_debug('[DBmodel][recordBy]: ' . $query);
$ret = $this->getRow($query);
$ret = $this->unfixJSONs($table_id, $ret);
return $ret;
}
public function recordEmpty($table_id)
{
$allow = $this->allowAttributes($table_id);
if (!is_array($allow)) {
return false;
}
$ret = array();
foreach ($allow as $a) {
$ret[$a] = null;
}
return $ret;
}
public function count($table_id)
{
$query = sprintf(
'SELECT COUNT(*)'
. ' FROM %s',
$this->tableName($table_id)
);
return $this->getOne($query);
}
public function search($table_id, $tab_hash = 'tab')
{
$qb = new QueryBuilder($this);
return $qb->beginAction('SELECT', $table_id, $tab_hash);
}
public function insert($table_id, $tab_hash = 'tab')
{
$qb = new QueryBuilder($this);
return $qb->beginAction('INSERT', $table_id, $tab_hash);
}
public function update($table_id, $tab_hash = 'tab')
{
$qb = new QueryBuilder($this);
return $qb->beginAction('UPDATE', $table_id, $tab_hash);
}
public function delete($table_id, $tab_hash = 'tab')
{
$qb = new QueryBuilder($this);
return $qb->beginAction('DELETE', $table_id, $tab_hash);
}
public function optimize($table_id)
{
$query = sprintf('OPTIMIZE TABLE `%s`', $this->tableName($table_id));
$this->_debug('[DBmodel][optimize]: ' . $query);
$ret = $this->query($query);
if ($ret === false) {
throw new \Exception($this->errorMessage());
}
return true;
}
public function toGroupBy($arr, $col_name)
{
$ret = array();
foreach ($arr as $row) {
if (!isset($ret[$row[$col_name]])) {
$ret[$row[$col_name]] = array();
}
$ret[$row[$col_name]][] = $row;
}
return $ret;
}
/* ----------------------------------------------------
* BASIC DB METHODS
*/
public function query(string $query, ?int $fetch_mode = null, ?int $colno = null): bool|\PDOStatement
{
$this->_debug('[DBmodel][query]: ' . $query);
if ($fetch_mode == \PDO::FETCH_COLUMN) {
if (is_null($colno)) $colno = 0;
$this->stmt = $this->dbh->query($query, $fetch_mode, $colno);
} else {
$this->stmt = $this->dbh->query($query, $fetch_mode);
}
return $this->stmt;
}
public function getOne(string $query): string|false
{
$result = $this->query($query, \PDO::FETCH_NUM);
if ($result === false) {
return false;
}
$row = $result->fetch(\PDO::FETCH_NUM);
if (!is_array($row) || count($row) <= 0) return false;
return $row[0];
}
public function getRow(string $query): array|false
{
$result = $this->query($query, \PDO::FETCH_ASSOC);
if ($result === false) {
return false;
}
$row = $result->fetch(\PDO::FETCH_ASSOC);
return $row;
}
public function getCol(string $query): array|false
{
$result = $this->query($query, \PDO::FETCH_COLUMN);
if ($result === false) {
return false;
}
$return = [];
while ($row = $result->fetchColumn()) {
$return[] = $row;
}
return $return;
}
public function getAll(string $query): array|false
{
$result = $this->query($query, \PDO::FETCH_ASSOC);
if ($result === false) {
return false;
}
return $result->fetchAll(\PDO::FETCH_ASSOC);
}
public function affectedRows(): int
{
if (
$this->stmt === false
|| $this->stmt === null
)
return 0;
return $this->stmt->rowCount();
}
public function quote(string $value): string
{
return $this->dbh->quote($value);
}
public function getLastInsertID(?string $name = null): ?string
{
if ($this->type === 'pgsql') {
$id = $this->dbh->query('SELECT LASTVAL()')->fetchColumn();
return (string) $id ?: null;
}
return $this->dbh->lastInsertId($name);
}
public function errorMessage(): string
{
return implode(':', $this->dbh->errorInfo());
}
public function getTableColumns($table_name) {
$desc = $this->getAll(sprintf('DESC %s', $table_name));
$desc = $this->arrayKeysToLowerCase($desc);
$pks = array();
$columns = array();
foreach ($desc as $d) {
$colname = strtolower($d['field']);
$types[$colname] = $d['type'];
if ($d['key'] == 'PRI') {
$pks[] = $colname;
if (strtolower($d['extra']) != 'auto_increment') {
$columns[] = $colname;
}
} else {
$columns[] = $colname;
}
}
return array('pks' => $pks, 'columns' => $columns, 'types' => $types);
}
/* ----------------------------------------------------
* HELPER METHODS
*/
public function fixTypes($table_id, $data, $where_prefixes = array())
{
$types = $this->typesAttributes($table_id);
if ($types === false || !is_array($data)) return $data;
$data_keys = array_keys($data);
if (is_array($types)) foreach ($types as $column => $type) {
if (!in_array($column, $data_keys)) continue;
if (is_null($data[$column])) continue;
// kontrola special
$val = $data[$column];
if (is_array($val)) continue;
$prefix = $this->extractPrefix($val, $where_prefixes);
$n_val = str_replace($prefix, '', $val);
if (substr($n_val, 0, 1) == '`' && substr($n_val, -1) == '`') continue;
// kontrola formatu
$max_length = null;
$min_number = null;
$max_number = null;
$allowed_values = null;
switch (strtolower($type['format'])) {
case 'varchar':
$max_length = intval($type['length']);
break;
case 'tinyblob':
case 'tinytext':
$max_length = 255;
break;
case 'text':
case 'blob':
$max_length = 65535;
break;
case 'mediumblob':
case 'mediumtext':
$max_length = 16777216;
break;
case 'longblob':
case 'longtext':
$max_length = 4294967296;
break;
case 'tinyint':
$min_number = -128;
$max_number = 128;
break; // 1B
case 'smallint':
$min_number = -32768;
$max_number = 32768;
break; // 2B
case 'mediumint':
$min_number = -8388608;
$max_number = 8388608;
break; // 3B
case 'integer':
case 'int':
$min_number = -2147483648;
$max_number = 2147483648;
break; // 4B
case 'bigint':
$min_number = -1 * pow(2, 63);
$max_number = pow(2, 63);
break; // 8B
case 'decimal':
if (is_array($data[$column])) {
foreach ($data[$column] as $key => $val) {
$prefix = $this->extractPrefix($val, $where_prefixes);
$data[$column] = $prefix . $this->fixDecimalMysql($val, $type['length']);
}
} else {
$prefix = $this->extractPrefix($data[$column], $where_prefixes);
$data[$column] = $prefix . $this->fixDecimalMysql($data[$column], $type['length']);
}
break;
case 'float':
if (is_array($data[$column])) {
foreach ($data[$column] as $key => $val) {
$prefix = $this->extractPrefix($val, $where_prefixes);
$data[$column][$key] = $prefix . $this->fixDecimalPoint(floatval($this->fixDecimalPoint($val)));
}
} else {
$prefix = $this->extractPrefix($data[$column], $where_prefixes);
$data[$column] = $prefix . $this->fixDecimalPoint(floatval($this->fixDecimalPoint($data[$column])));
}
break;
case 'enum':
$allowed_values = $type['allowed_values'];
break;
case 'date':
if (is_array($data[$column])) {
foreach ($data[$column] as $key => $val) {
$prefix = $this->extractPrefix($val, $where_prefixes);
$n_val = str_replace($prefix, '', $val);
$data[$column] = $prefix . date('Y-m-d', strtotime($n_val));
}
} else {
$prefix = $this->extractPrefix($data[$column], $where_prefixes);
$n_val = str_replace($prefix, '', $data[$column]);
$data[$column] = $prefix . date('Y-m-d', strtotime($n_val));
}
break;
case 'datetime':
if (is_array($data[$column])) {
foreach ($data[$column] as $key => $val) {
$prefix = $this->extractPrefix($val, $where_prefixes);
$n_val = str_replace($prefix, '', $val);
$data[$column] = $prefix . date('Y-m-d H:i:s', strtotime($n_val));
}
} else {
$prefix = $this->extractPrefix($data[$column], $where_prefixes);
$n_val = str_replace($prefix, '', $data[$column]);
$data[$column] = $prefix . date('Y-m-d H:i:s', strtotime($n_val));
}
break;
}
if (!is_null($max_length)) {
if (strlen($data[$column]) > $max_length) {
if (is_array($data[$column])) {
foreach ($data[$column] as $key => $val) {
$data[$column][$key] = mb_substr($val, 0, $max_length);
}
} else {
$data[$column] = mb_substr($data[$column], 0, $max_length);
}
}
}
if (!is_null($min_number)) {
if (is_array($data[$column])) {
foreach ($data[$column] as $key => $val) {
$prefix = $this->extractPrefix($val, $where_prefixes);
$n_val = str_replace($prefix, '', $val);
$data[$column][$key] = $prefix . max(intval($n_val), $min_number);
}
} else {
$prefix = $this->extractPrefix($data[$column], $where_prefixes);
$n_val = str_replace($prefix, '', $data[$column]);
$data[$column] = $prefix . max(intval($n_val), $min_number);
}
}
if (!is_null($max_number)) {
if (is_array($data[$column])) {
foreach ($data[$column] as $key => $val) {
$prefix = $this->extractPrefix($val, $where_prefixes);
$n_val = str_replace($prefix, '', $val);
$data[$column][$key] = $prefix . min(intval($n_val), $max_number);
}
} else {
$prefix = $this->extractPrefix($data[$column], $where_prefixes);
$n_val = str_replace($prefix, '', $data[$column]);
$data[$column] = $prefix . min(intval($n_val), $max_number);
}
}
if (!is_null($allowed_values)) {
if (is_array($data[$column])) {
foreach ($data[$column] as $key => $val) {
if (!in_array($val, $allowed_values)) $data[$column][$key] = null;
}
} else {
if (!in_array($data[$column], $allowed_values)) $data[$column] = null;
}
}
}
return $data;
}
public function fixDecimalMysql($num, $length)
{
$lengths = explode(',', $length);
$lengths_0 = intval($lengths[0]);
$lengths_1 = intval($lengths[1]);
$num = $this->fixDecimalPoint($num);
$num_parts = explode('.', $num);
if (!is_array($num_parts)) return 0;
if (count($num_parts) == 0) return intval($num);
if (count($num_parts) == 1) return intval($num_parts[0]);
$num = intval(substr($num_parts[0], 0, $lengths_0 - $lengths_1)) . '.' . $this->allowedChars(substr($num_parts[1] . str_repeat('0', $lengths_1), 0, $lengths_1), '0123456789');
return $num;
}
public function fixDecimalPoint($num)
{
return str_replace(',', '.', $num);
}
public function fixDecimalPoints($table_id, $data)
{
if (!$this->tableInfoExist($table_id, 'fix_decimal_points')) return $data;
$for_fix = $this->tableInfo($table_id, 'fix_decimal_points');
if (is_array($data)) foreach ($data as $key => $val) {
if (in_array($key, $for_fix)) {
$data[$key] = $this->fixDecimalPoint($val);
}
}
return $data;
}
public function fixBoolean($val)
{
return in_array(strtolower($val), array('on', 'true', '1', 'yes', 'ano', 'checked', 'selected')) ? 1 : 0;
}
public function fixBooleans($table_id, $data)
{
if (!$this->tableInfoExist($table_id, 'fix_booleans')) return $data;
$for_fix = $this->tableInfo($table_id, 'fix_booleans');
if (is_array($data)) foreach ($data as $key => $val) {
if (in_array($key, $for_fix)) {
$data[$key] = $this->fixBoolean($val);
}
}
return $data;
}
public function forceBooleans($table_id, &$data)
{
if (!$this->tableInfoExist($table_id, 'fix_booleans')) return $data;
$for_fix = $this->tableInfo($table_id, 'fix_booleans');
if (is_array($for_fix)) foreach ($for_fix as $key) {
$data[$key] = $this->fixBoolean($data[$key]);
}
return $data;
}
public function fixJSON($val)
{
return json_encode($val);
}
public function fixJSONs($table_id, $data)
{
if (!$this->tableInfoExist($table_id, 'fix_json')) return $data;
$for_fix = $this->tableInfo($table_id, 'fix_json');
if (is_array($data)) foreach ($data as $key => $val) {
if (in_array($key, $for_fix)) {
$data[$key] = $this->fixJSON($val);
}
}
return $data;
}
public function unfixJSON($val)
{
if (!is_string($val)) return $val;
try {
$decoded = json_decode($val, true);
return $decoded;
} catch (\Exception $e) {
return $val;
}
}
public function unfixJSONs($table_id, $data)
{
if (!$this->tableInfoExist($table_id, 'fix_json')) return $data;
$for_fix = $this->tableInfo($table_id, 'fix_json');
if (is_array($data)) foreach ($data as $key => $val) {
if (in_array($key, $for_fix)) {
$data[$key] = $this->unfixJSON($val);
}
}
return $data;
}
public function preventOneError()
{
$this->oldErrorHandler = set_error_handler(array($this, 'silentErrorHandler'));
}
public function allowNextError()
{
if (is_null($this->oldErrorHandler)) {
return false;
}
set_error_handler($this->oldErrorHandler);
$this->oldErrorHandler = null;
return true;
}
public function silentErrorHandler($errno, $errstr, $errfile, $errline)
{
return $this->allowNextError();
}
public function import($objModel)
{
if (is_null($objModel)) return false;
if (
!isset($objModel->tables)
|| !is_array($objModel->tables)
)
return false;
$this->tables = array_merge($this->tables, $objModel->tables);
return is_array($this->tables);
}
public function textSearch($find, $table_id = null)
{
if (is_null($table_id)) {
$ret = array();
if (is_array($this->tables)) foreach ($this->tables as $t_id => $table_settings) {
$ret = array_merge($ret, $this->textSearch($find, $t_id));
}
return $ret;
}
$allow_attributes = $this->tableInfo($table_id, 'allow_attributes');
if (!is_array($allow_attributes) || count($allow_attributes) <= 0) return array();
$keys = array_keys($allow_attributes);
if (strlen($keys[0]) <= 1) { // stary format bez typov
$table_columns = $this->getTableColumns($this->tableName($table_id));
$attributes = $table_columns['types'];
} else { // novy format s typmi
$attributes = $allow_attributes;
}
if (!is_array($attributes) || count($attributes) <= 0) return array();
$forsearch = array();
foreach ($attributes as $name => $type) {
if ($this->checkPrefix(array('varchar', 'tinyblob', 'tinytext', 'text', 'blob', 'mediumblob', 'mediumtext', 'longblob', 'longtext'), $type, true)) {
$forsearch[] = $name;
}
}
if (count($forsearch) <= 0) return array();
foreach ($forsearch as $colname) $conds[] = sprintf('`%s` LIKE %s', $colname, $this->quote('%' . $find . '%'));
$query = sprintf('SELECT * FROM %s WHERE %s', $this->tableName($table_id), implode(' OR ', $conds));
$all = $this->getAll($query);
$ret = array();
$pk_name = $this->primaryKeyName($table_id);
if (is_array($all)) foreach ($all as $row) {
if (is_array($pk_name)) {
$pk = array();
foreach ($pk_name as $pkn) $pk[$pkn] = $row[$pkn];
} else {
$pk = $row[$pk_name];
}
$ret[] = array(
'table_id' => $table_id,
'record_id' => $pk,
'name' => $row['name'],
'context' => $this->context($find, implode(' ', array_values($row)))
);
}
return $ret;
}
public function extractPrefix($str, $prefixes)
{
if (is_null($prefixes)) return '';
if (!is_array($prefixes)) $prefixes = array($prefixes);
usort($prefixes, function ($a, $b) {
return strlen($b) - strlen($a);
});
foreach ($prefixes as $prefix) {
if ($this->checkPrefix($prefix, $str)) return $prefix;
}
return '';
}
public function checkPrefix($prefix, $str, $case_insensitive = false)
{
if (is_array($prefix)) {
foreach ($prefix as $one) {
if ($this->checkPrefix($one, $str, $case_insensitive)) return true;
}
return false;
}
if ($case_insensitive) {
$prefix = strtolower($prefix);
$str = strtolower($str);
}
return $prefix == substr($str, 0, strlen($prefix));
}
public function allowedChars($str, $allowed = 'abcdefghijklmnopqrstuvwxyz0123456789')
{
$ret = '';
for ($i = 0; $i < strlen($str); $i++) {
$char = substr($str, $i, 1);
if (strpos($allowed, $char) !== false) {
$ret .= $char;
}
}
return $ret;
}
public function context($findme, $string, $around_words = 2)
{
$findme = strtolower($findme);
$parts = explode(' ', $string);
$is = array();
for ($i = 0; $i < count($parts); $i++) {
if (strtolower($parts[$i]) == $findme) {
for ($j = max(0, $i - $around_words); $j < min(count($parts) - 1, $i + $around_words); $j++) {
if (!in_array($j, $is)) $is[] = $j;
}
}
}
$out = '';
$last_i = 0;
foreach ($is as $i) {
if ($last_i + 1 < $i) $out .= ' ...';
$out .= ' ' . $parts[$i];
$last_i = $i;
}
return $out . ' ...';
}
public function arrayKeysToLowerCase(array $array, int $case = CASE_LOWER): array
{
$newArray = [];
foreach ($array as $key => $value) {
$newKey = is_string($key)
? ($case === CASE_LOWER ? strtolower($key) : strtoupper($key))
: $key;
if (is_array($value)) {
$value = $this->arrayKeysToLowerCase($value, $case);
}
$newArray[$newKey] = $value;
}
return $newArray;
}
}
class QueryBuilder
{
private DBmodel $model;
private $action = null;
private $tab_hash = 'tab';
private $tab_key = null;
private $table_id = null;
private $tables = array();
private $columns = array();
private $vk_keys = array();
private $vk_values = array();
private $duplicates = array();
private $sets = array();
private $conditions = array();
private $group_by = array();
private $order_by = array();
private $limit = null;
private $alias = null;
private $offset = null;
public function __construct($_model)
{
$this->model = $_model;
}
private function incTabKey()
{
$this->tab_key = $this->tab_hash . (count($this->tables) + 1);
}
private function allAttributes($table_id)
{
$primary_key = $this->model->primaryKeyName($table_id);
$primary_key = (is_array($primary_key)) ? $primary_key : array($primary_key);
return array_merge($this->model->allowAttributes($table_id), $primary_key);
}
public function beginAction($action, $table_id, $tab_hash = 'tab')
{
$this->action = $action;
$this->table_id = $table_id;
$this->tab_hash = $tab_hash;
if (
$action == 'SELECT'
|| $action == 'UPDATE'
) {
$this->incTabKey();
$this->tables[$this->tab_key] = sprintf('%s AS %s', $this->model->tableName($table_id), $this->tab_key);
}
if (
$action == 'DELETE'
|| $action == 'INSERT'
) {
$this->tables[$this->tab_key] = sprintf('%s', $this->model->tableName($table_id));
}
return $this;
}
private function _join($join_type, $table_id, $left_attr, $right_attr, $joined_tab_key = null)
{
$this->table_id = $table_id;
$last_tab_key = is_null($joined_tab_key) ? $this->tab_key : $joined_tab_key;
$this->incTabKey();
$left_attr = is_array($left_attr) ? $left_attr : array($left_attr);
$right_attr = is_array($right_attr) ? $right_attr : array($right_attr);
$min_count = min(count($left_attr), count($right_attr));
if ($min_count <= 0) {
return $this;
}
$on = array();
for ($index = 0; $index < $min_count; $index++) {
$on[] = sprintf(
'%s%s = %s%s',
(!in_array(substr($left_attr[$index], 0, 1), array('"', ' '))) ? $last_tab_key . '.' : '',
$left_attr[$index],
(!in_array(substr($right_attr[$index], 0, 1), array('"', ' '))) ? $this->tab_key . '.' : '',
$right_attr[$index]
);
}
$this->tables[$this->tab_key] = sprintf(
'%s %s AS %s ON %s',
$join_type,
$this->model->tableName($table_id),
$this->tab_key,
implode(' AND ', $on)
);
return $this;
}
public function join($table_id, $left_attr, $right_attr, $joined_tab_key = null)
{
return $this->_join('JOIN', $table_id, $left_attr, $right_attr, $joined_tab_key);
}
public function leftjoin($table_id, $left_attr, $right_attr, $joined_tab_key = null)
{
return $this->_join('LEFT JOIN', $table_id, $left_attr, $right_attr, $joined_tab_key);
}
public function rightjoin($table_id, $left_attr, $right_attr, $joined_tab_key = null)
{
return $this->_join('RIGHT JOIN', $table_id, $left_attr, $right_attr, $joined_tab_key);
}
public function column($colname, $as = null)
{
if (is_array($colname)) {
foreach ($colname as $col) {
$this->column($col, $as);
}
} else {
$allow_attributes = $this->allAttributes($this->table_id);
if (in_array($colname, $allow_attributes)) {
$column = sprintf('%s.`%s`', $this->tab_key, $colname);
} else if ($colname == '*') {
$column = sprintf('%s.*', $this->tab_key);
} else if (
is_object($colname)
&& get_class($colname) == 'QueryBuilder'
) {
$column = sprintf('(%s) AS %s', $colname->completeQuery(), $colname->alias);
} else {
$column = sprintf('%s', $colname);
}
if (!is_null($as)) {
$column .= ' AS ' . $as;
}
$this->columns[] = $column;
}
return $this;
}
public function set($set = array())
{
$allow_attributes = $this->model->allowAttributes($this->table_id);
$set = $this->model->fixDecimalPoints($this->table_id, $set);
$set = $this->model->fixBooleans($this->table_id, $set);
$set = $this->model->fixJSONs($this->table_id, $set);
$set = $this->model->fixTypes($this->table_id, $set);
if (is_array($set)) foreach ($set as $key => $val) {
if (in_array($key, $allow_attributes)) {
if (is_null($val)) {
$this->sets[] = sprintf('%s.%s = NULL', $this->tab_key, $key);
} else if (
is_object($val)
&& get_class($val) == 'QueryBuilder'
) {
$this->sets[] = sprintf('%s.%s = (%s)', $this->tab_key, $key, $val->completeQuery());
} else {
$q_val = (substr($val, 0, 1) == '_' && substr($val, -1) == '_') ? substr($val, 1, -1) : $this->model->quote($val);
$this->sets[] = sprintf('%s.%s = %s', $this->tab_key, $key, $q_val);
}
}
}
return $this;
}
public function value($values = array())
{
$allow_attributes = $this->model->allowAttributes($this->table_id);
if (is_array($values)) foreach ($values as $key => $val) {
if (in_array($key, $allow_attributes)) {
$this->vk_keys[] = sprintf('`%s`', $key);
$this->vk_values[] = is_null($val) ? 'NULL' : sprintf('%s', $this->model->quote($val));
}
}
return $this;
}
public function duplicate($dupls = array())
{
$allow_attributes = $this->model->allowAttributes($this->table_id);
$dupls = $this->model->fixTypes($this->table_id, $dupls);
if (is_array($dupls)) foreach ($dupls as $key => $val) {
if (in_array($key, $allow_attributes)) {
if (is_null($val)) {
$this->duplicates[] = sprintf('%s = NULL', $key);
} else {
$this->duplicates[] = sprintf('%s = %s', $key, $this->model->quote($val));
}
}
}
return $this;
}
private $where_prefixes = array('!', '%', '<', '<=', '>', '>=');
public function where($search = array(), $concat_or = false)
{
$allow_attributes = $this->allAttributes($this->table_id);
$search = $this->model->fixTypes($this->table_id, $search, $this->where_prefixes);
$_tab_key = (strlen($this->tab_key) > 0) ? $this->tab_key . '.' : '';
$conds = array();
if (is_array($search)) foreach ($search as $key => $val) {
if (is_string($key)) {
$as_no_equal = (substr($key, 0, 1) == '!');
}
if (is_string($val)) {
if (strlen($val) <= 0) continue;
$as_like = (stristr($val, '%') !== false);
$as_lesser_equal = (substr($val, 0, 2) == '<=');
$as_lesser = (!$as_lesser_equal && substr($val, 0, 1) == '<');
$as_greater_equal = (substr($val, 0, 2) == '>=');
$as_greater = (!$as_greater_equal && substr($val, 0, 1) == '>');
$as_no_equal = $as_no_equal || (substr($val, 0, 1) == '!');
}
if (
is_array($val)
&& count($val) <= 0
) {
continue;
}
$key = trim(str_replace(array('%', '!'), '', $key));
if (
in_array($key, $allow_attributes)
|| (substr($key, 0, 1) == '_' && substr($key, -1) == '_')
|| (substr($key, 0, 1) == '*' && substr($key, -1) == '*')
) {
$__tab_key = $_tab_key;
if (
substr($key, 0, 1) == '_'
&& substr($key, -1) == '_'
) {
$key = $this->model->quote(substr($key, 1, -1));
$__tab_key = '';
}
if (
substr($key, 0, 1) == '*'
&& substr($key, -1) == '*'
) {
$key = substr($key, 1, -1);
$__tab_key = '';
}
if (is_null($val)) {
$conds[] = sprintf('%s%s IS NULL', $__tab_key, $key);
} else if (
is_object($val)
&& get_class($val) == 'QueryBuilder'
) {
$conds[] = sprintf('%s%s %s IN (%s)', $__tab_key, $key, $as_no_equal ? 'NOT' : '', $val->completeQuery());
} else if (is_array($val)) {
$values = array();
foreach ($val as $v) {
if (is_null($v)) continue;
$values[] = $this->model->quote($v);
}
$add_or = (in_array(null, $val, true)) ? sprintf('OR %s%s IS NULL', $__tab_key, $key) : '';
$conds[] = sprintf('(%s%s %s IN (%s) %s)', $__tab_key, $key, $as_no_equal ? 'NOT' : '', implode(', ', $values), $add_or);
} else if (in_array(strtolower($val), array('not null', 'is not null', '! null', '!null', '!= null', '!=null'))) {
$conds[] = sprintf('%s%s IS NOT NULL', $__tab_key, $key);
} else {
if ($as_like) {
$sign = 'LIKE';
} else if ($as_lesser) {
$sign = '<';
$val = substr($val, 1);
} else if ($as_lesser_equal) {
$sign = '<=';
$val = substr($val, 2);
} else if ($as_greater) {
$sign = '>';
$val = substr($val, 1);
} else if ($as_greater_equal) {
$sign = '>=';
$val = substr($val, 2);
} else if ($as_no_equal) {
$sign = '!=';
$val = substr($val, 1);
} else {
$sign = '=';
}
$q_val = (substr($val, 0, 1) == '`' && substr($val, -1) == '`') ? $val : $this->model->quote($val);
$q_val = (substr($val, 0, 1) == '_' && substr($val, -1) == '_') ? substr($val, 1, -1) : $q_val;
$conds[] = sprintf('%s%s %s %s', $__tab_key, $key, $sign, $q_val);
}
}
}
if ($concat_or) {
if (count($conds) > 0) {
$this->conditions[] = '(' . implode(' OR ', $conds) . ')';
}
} else {
$this->conditions = array_merge($this->conditions, $conds);
}
return $this;
}
public function whereOR($search = array())
{
return $this->where($search, true);
}
public function group($group)
{
if (is_array($group)) {
foreach ($group as $g) {
$this->group($g);
}
} else {
$allow_attributes = $this->allAttributes($this->table_id);
if (in_array($group, $allow_attributes)) {
$this->group_by[] = sprintf('%s.%s', $this->tab_key, $group);
} else if (substr($group, 0, 1) == '`' && substr($group, -1) == '`') {
$this->group_by[] = sprintf('%s', $group);
}
}
return $this;
}
public function order($order = array())
{
$allow_attributes = $this->allAttributes($this->table_id);
if (is_array($order)) foreach ($order as $key => $direction) {
$direction_sign = in_array(strtolower($direction), array('desc', 'descending', 'down')) ? 'DESC' : 'ASC';
if (in_array($key, $allow_attributes)) {
$this->order_by[] = sprintf('%s.%s %s', $this->tab_key, $key, $direction_sign);
} else if (substr($key, 0, 1) == '`' && substr($key, -1) == '`') {
$this->order_by[] = sprintf('%s %s', $key, $direction_sign);
}
}
return $this;
}
public function limit($count_or_from, $count = null)
{
if (is_null($count_or_from)) return $this;
if (is_array($count_or_from)) {
$count = $count_or_from[1];
$count_or_from = $count_or_from[0];
}
if (is_null($count)) {
$this->limit = sprintf('LIMIT %d', intval($count_or_from));
} else {
$this->limit = sprintf('LIMIT %d,%d', intval($count_or_from), intval($count));
}
return $this;
}
public function offset($offset)
{
if (is_null($offset)) return $this;
$this->offset = sprintf('OFFSET %d', intval($offset));
return $this;
}
private function completeQuery()
{
if ($this->action == 'SELECT') {
$query = sprintf(
'SELECT %s'
. ' FROM %s'
. ' %s' // WHERE
. ' %s' // GROUP
. ' %s' // ORDER
. ' %s' // LIMIT
. ' %s', // OFFSET
(count($this->columns) > 0) ? implode(', ', $this->columns) : '*',
implode(' ', $this->tables),
(count($this->conditions) > 0) ? 'WHERE ' . implode(' AND ', $this->conditions) : '',
(count($this->group_by) > 0) ? 'GROUP BY ' . implode(', ', $this->group_by) : '',
(count($this->order_by) > 0) ? 'ORDER BY ' . implode(', ', $this->order_by) : '',
is_null($this->limit) ? '' : $this->limit,
is_null($this->offset) ? '' : $this->offset
);
} else if ($this->action == 'INSERT') {
$query = sprintf(
'INSERT INTO %s'
. ' (%s)' // KEYS
. ' VALUES'
. ' (%s)' // VALUES
. ' %s', // ON DUPLICATE
implode(' ', $this->tables),
implode(', ', $this->vk_keys),
implode(', ', $this->vk_values),
((count($this->duplicates) > 0) ? 'ON DUPLICATE KEY UPDATE ' . implode(', ', $this->duplicates) : '')
);
} else if ($this->action == 'UPDATE') {
$query = sprintf(
'UPDATE %s'
. ' %s' // SET
. ' %s' // WHERE
. ' %s', // LIMIT
implode(' ', $this->tables),
(count($this->sets) > 0) ? 'SET ' . implode(', ', $this->sets) : '',
(count($this->conditions) > 0) ? 'WHERE ' . implode(' AND ', $this->conditions) : '',
is_null($this->limit) ? '' : $this->limit
);
} else if ($this->action == 'DELETE') {
$query = sprintf(
'DELETE FROM %s'
. ' %s' // WHERE
. ' %s', // LIMIT
implode(' ', $this->tables),
(count($this->conditions) > 0) ? 'WHERE ' . implode(' AND ', $this->conditions) : '',
is_null($this->limit) ? '' : $this->limit
);
} else {
throw new \Exception('Unknown action "' . $this->action . '"');
}
$this->model->_debug('[Model][completeQuery]: ' . $query);
return $query;
}
public function alias($alias)
{
$this->alias = $alias;
return $this;
}
public function toArray()
{
$query = $this->completeQuery();
$all = $this->model->getAll($query);
if (is_array($all)) foreach ($all as $index => $row) {
$all[$index] = $this->model->unfixJSONs($this->table_id, $row);
}
return $all;
}
public function getCol()
{
$query = $this->completeQuery();
return $this->model->getCol($query);
}
public function getOne()
{
$query = $this->completeQuery();
return $this->model->getOne($query);
}
public function toArrayFirst()
{
$query = $this->completeQuery();
$row = $this->model->getRow($query);
$row = $this->model->unfixJSONs($this->table_id, $row);
return $row;
}
public function toCombo($name_key, $name_value, $add_empty = false)
{
$all = $this->toArray();
$ret = array();
if ($add_empty) $ret[''] = '';
if (is_array($all)) foreach ($all as $row) {
$ret[$row[$name_key]] = $row[$name_value];
}
return $ret;
}
public function getRow()
{
return $this->toArrayFirst();
}
public function execute()
{
$query = $this->completeQuery();
if (!$this->model->query($query)) return false;
return $this->model->affectedRows();
}
}