You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

611 lines
18 KiB

<?php
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
//
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id$
use Tiki\TikiDb\SanitizeEncoding;
class TikiDb_Table
{
/** @var TikiDb_Pdo|TikiDb_Adodb $db */
protected $db;
protected $tableName;
protected $autoIncrement;
protected $errorMode = TikiDb::ERR_DIRECT;
protected static $utf8FieldsCache = [];
public function __construct($db, $tableName, $autoIncrement = true)
{
$this->db = $db;
$this->tableName = $tableName;
$this->autoIncrement = $autoIncrement;
}
public function useExceptions()
{
$this->errorMode = TikiDb::ERR_EXCEPTION;
}
/**
* Inserts a row in the table by building the SQL query from an array of values.
* The target table is defined by the instance. Argument names are not validated
* against the schema. This is only a helper method to improve code readability.
*
* @param $values array Key-value pairs to insert.
* @param $ignore boolean Insert as ignore statement
* @return array|bool|mixed
*/
public function insert(array $values, $ignore = false)
{
$bindvars = [];
$query = $this->buildInsert($values, $ignore, $bindvars);
$result = $this->db->queryException($query, $bindvars);
if ($this->autoIncrement) {
if ($insertedId = $this->db->lastInsertId()) {
return $insertedId;
} else {
return false;
}
} else {
return $result;
}
}
/**
* @param array $data
* @param array $keys
* @return array|bool|mixed
*/
public function insertOrUpdate(array $data, array $keys)
{
$insertData = array_merge($data, $keys);
$bindvars = [];
$query = $this->buildInsert($insertData, false, $bindvars);
$query .= ' ON DUPLICATE KEY UPDATE ';
$query .= $this->buildUpdateList($data, $bindvars);
$result = $this->db->queryException($query, $bindvars);
if ($this->autoIncrement) {
if ($insertedId = $this->db->lastInsertId()) {
return $insertedId;
//Multiple actions in a query (e.g., INSERT + UPDATE) returns result class instead of the id number
} elseif (is_object($result)) {
return $result;
} else {
return false;
}
} else {
return $result;
}
}
/**
* Deletes a single record from the table matching the provided conditions.
* Conditions use exact matching. Multiple conditions will result in AND matching.
* @param array $conditions
* @return TikiDb_Pdo_Result|TikiDb_Adodb_Result
*/
public function delete(array $conditions)
{
$bindvars = [];
$query = $this->buildDelete($conditions, $bindvars) . ' LIMIT 1';
return $this->db->queryException($query, $bindvars);
}
/**
* Builds and performs and SQL update query on the table defined by the instance.
* This query will update a single record.
* @param array $values
* @param array $conditions
* @return TikiDb_Pdo_Result|TikiDb_Adodb_Result
*/
public function update(array $values, array $conditions)
{
return $this->updateMultiple($values, $conditions, 1);
}
/**
* @param array $values
* @param array $conditions
* @param null $limit
* @return TikiDb_Pdo_Result|TikiDb_Adodb_Result
*/
public function updateMultiple(array $values, array $conditions, $limit = null)
{
$bindvars = [];
$query = $this->buildUpdate($values, $conditions, $bindvars);
if (! is_null($limit)) {
$query .= ' LIMIT ' . (int)$limit;
}
return $this->db->queryException($query, $bindvars);
}
/**
* Deletes a multiple records from the table matching the provided conditions.
* Conditions use exact matching. Multiple conditions will result in AND matching.
*
* The method works just like delete, except that it does not have the one record
* limitation.
* @param array $conditions
* @return TikiDb_Pdo_Result|TikiDb_Adodb_Result
*/
public function deleteMultiple(array $conditions)
{
$bindvars = [];
$query = $this->buildDelete($conditions, $bindvars);
return $this->db->queryException($query, $bindvars);
}
public function fetchOne($field, array $conditions, $orderClause = null)
{
if ($result = $this->fetchRow([$field], $conditions, $orderClause)) {
return reset($result);
}
return false;
}
/**
* Provides the result count only
* @param array $conditions
*
* @return bool|mixed
*/
public function fetchCount(array $conditions)
{
return $this->fetchOne($this->count(), $conditions);
}
/**
* Retrieve all fields from a single row
* @param array $conditions
* @param null $orderClause
*
* @return mixed
*/
public function fetchFullRow(array $conditions, $orderClause = null)
{
return $this->fetchRow($this->all(), $conditions, $orderClause);
}
/**
* Retrieve the selected fields from a single row
* @param array $fields
* @param array $conditions
* @param null $orderClause
*
* @return mixed
*/
public function fetchRow(array $fields, array $conditions, $orderClause = null)
{
$result = $this->fetchAll($fields, $conditions, 1, 0, $orderClause);
return reset($result);
}
/**
* Provides all the matched values from a single column
* @param $field
* @param array $conditions
* @param int $numrows
* @param int $offset
* @param null $order
*
* @return array
*/
public function fetchColumn($field, array $conditions, $numrows = -1, $offset = -1, $order = null)
{
if (is_string($order)) {
$order = [$field => $order];
}
$result = $this->fetchAll([$field], $conditions, $numrows, $offset, $order);
$output = [];
foreach ($result as $row) {
$output[] = reset($row);
}
return $output;
}
/**
* Retrieves the two values from the table and generates a map from the key and the value
* @param $keyField
* @param $valueField
* @param array $conditions
* @param int $numrows
* @param int $offset
* @param null $order
*
* @return array
*/
public function fetchMap($keyField, $valueField, array $conditions, $numrows = -1, $offset = -1, $order = null)
{
$result = $this->fetchAll([$keyField, $valueField], $conditions, $numrows, $offset, $order);
$map = [];
foreach ($result as $row) {
$key = $row[$keyField];
$value = $row[$valueField];
$map[ $key ] = $value;
}
return $map;
}
/**
* Test if a condition exists in the database.
*
* @param array $conditions List of conditions that will be tested
*
* @return bool True if the condition exists, false otherwise
*/
public function fetchBool(array $conditions = []): bool
{
$query = 'SELECT 1 FROM ' . $this->escapeIdentifier($this->tableName);
$query .= $this->buildConditions($conditions, $bindvars);
$result = $this->db->fetchAll($query, $bindvars, 1, -1, $this->errorMode);
return ! empty($result[0][1]);
}
/**
* Fully-customizable fetch providing an array of associative arrays.
* @param array $fields
* @param array $conditions
* @param int $numrows
* @param int $offset
* @param null $orderClause
*
* @return array|bool
*/
public function fetchAll(array $fields = [], array $conditions = [], $numrows = -1, $offset = -1, $orderClause = null)
{
$bindvars = [];
$fieldDescription = '';
foreach ($fields as $k => $f) {
if ($f instanceof TikiDB_Expr) {
$fieldDescription .= $f->getQueryPart(null);
$bindvars = array_merge($bindvars, $f->getValues());
} else {
$fieldDescription .= $this->escapeIdentifier($f);
}
if (is_string($k)) {
$fieldDescription .= ' AS ' . $this->escapeIdentifier($k);
}
$fieldDescription .= ', ';
}
$query = 'SELECT ';
$query .= (! empty($fieldDescription)) ? rtrim($fieldDescription, ', ') : '*';
$query .= ' FROM ' . $this->escapeIdentifier($this->tableName);
$query .= $this->buildConditions($conditions, $bindvars);
$query .= $this->buildOrderClause($orderClause);
return $this->db->fetchAll($query, $bindvars, $numrows, $offset, $this->errorMode);
}
/**
* Most generic usage, allows to insert SQL in many places.
* In update for the data, they are used for the values.
* In conditions, they represent the whole condition.
* In a select query, they represent a single field.
* An expression can be used instead of the sort array to replace the entire order by argument.
* Within the fragment, $$ will be replaced by the field for conditions.
* All other expressions are just shorthands for this one.
* @param $string
* @param array $arguments
*
* @return TikiDb_Expr
*/
public function expr($string, $arguments = [])
{
return new TikiDb_Expr($string, $arguments);
}
/**
* For all fields, not a specific field, returns an array of expressions
* @return array
*/
public function all()
{
return [$this->expr('*')];
}
public function count()
{
return $this->expr('COUNT(*)');
}
public function sum($field)
{
return $this->expr("SUM(`$field`)");
}
public function max($field)
{
return $this->expr("MAX(`$field`)");
}
public function min($field)
{
return $this->expr("MIN(`$field`)");
}
public function increment($count)
{
return $this->expr('$$ + ?', [$count]);
}
public function decrement($count)
{
return $this->expr('$$ - ?', [$count]);
}
public function greaterThan($value)
{
return $this->expr('$$ > ?', [$value]);
}
public function lesserThan($value)
{
return $this->expr('$$ < ?', [$value]);
}
/**
* Retrieve values within a range. The vales given will be included.
*
* @param $values array Must be an array containing 2 strings
*
* @return TikiDb_Expr
*/
public function between($values)
{
return $this->expr('$$ BETWEEN ? AND ?', $values);
}
public function not($value)
{
if (empty($value)) {
return $this->expr('($$ <> ? AND $$ IS NOT NULL)', [$value]);
} else {
return $this->expr('$$ <> ?', [$value]);
}
}
/**
* String comparison using a formula.
* @param $value string A pattern where % represents zero, one, or multiple characters and _ represents a single character.
* eg. ['a%'] matches anything that starts with an 'a'.
*
* @return TikiDb_Expr
*/
public function like($value)
{
return $this->expr('$$ LIKE ?', [$value]);
}
/**
* Negative string comparision. See like()
* @param $value string
*
* @return TikiDb_Expr
*/
public function unlike($value)
{
return $this->expr('$$ NOT LIKE ?', [$value]);
}
/**
* Search for a substring. (a common LIKE statement)
* @param $value string Containing a string to search for.
*
* @return TikiDb_Expr
*/
public function contains($value)
{
$value = '%' . $value . '%';
return $this->expr('$$ LIKE ?', [$value]);
}
/**
* binary safe compare
* @param $value string
*
* @return TikiDb_Expr
*/
public function exactly($value)
{
return $this->expr('BINARY $$ = ?', [$value]);
}
public function in(array $values, $caseSensitive = false)
{
if (empty($values)) {
return $this->expr('1=0', []);
} else {
return $this->expr(($caseSensitive ? 'BINARY ' : '') . '$$ IN(' . rtrim(str_repeat('?, ', count($values)), ', ') . ')', $values);
}
}
public function notIn(array $values, $caseSensitive = false)
{
if (empty($values)) {
return $this->expr('1=0', []);
} else {
return $this->expr(($caseSensitive ? 'BINARY ' : '') . '$$ NOT IN(' . rtrim(str_repeat('?, ', count($values)), ', ') . ')', $values);
}
}
public function findIn($value, array $fields)
{
$expr = $this->like("%$value%");
return $this->any(array_fill_keys($fields, $expr));
}
public function concatFields(array $fields)
{
$fields = array_map([$this, 'escapeIdentifier'], $fields);
$fields = implode(', ', $fields);
$expr = '';
if ($fields) {
$expr = "CONCAT($fields)";
}
return $this->expr($expr);
}
public function any(array $conditions)
{
$binds = [];
$parts = [];
foreach ($conditions as $field => $expr) {
$parts[] = $expr->getQueryPart($this->escapeIdentifier($field));
$binds = array_merge($binds, $expr->getValues());
}
return $this->expr('(' . implode(' OR ', $parts) . ')', $binds);
}
public function sortMode($sortMode)
{
return $this->expr($this->db->convertSortMode($sortMode));
}
private function buildDelete(array $conditions, &$bindvars)
{
$query = "DELETE FROM {$this->escapeIdentifier($this->tableName)}";
$query .= $this->buildConditions($conditions, $bindvars);
return $query;
}
private function buildConditions(array $conditions, &$bindvars)
{
$query = " WHERE 1=1";
foreach ($conditions as $key => $value) {
$field = $this->escapeIdentifier($key);
if ($value instanceof TikiDb_Expr) {
$query .= " AND {$value->getQueryPart($field)}";
$bindvars = array_merge($bindvars, $value->getValues());
} elseif (empty($value)) {
$query .= " AND ($field = ? OR $field IS NULL)";
$bindvars[] = $value;
} else {
$query .= " AND $field = ?";
$bindvars[] = $value;
}
}
return $query;
}
private function buildOrderClause($orderClause)
{
if ($orderClause instanceof TikiDb_Expr) {
return ' ORDER BY ' . $orderClause->getQueryPart(null);
} elseif (is_array($orderClause) && ! empty($orderClause)) {
$part = ' ORDER BY ';
foreach ($orderClause as $key => $direction) {
$part .= "`$key` $direction, ";
}
return rtrim($part, ', ');
}
}
private function buildUpdate(array $values, array $conditions, &$bindvars)
{
$query = "UPDATE {$this->escapeIdentifier($this->tableName)} SET ";
$query .= $this->buildUpdateList($values, $bindvars);
$query .= $this->buildConditions($conditions, $bindvars);
return $query;
}
private function buildUpdateList($values, &$bindvars)
{
$query = '';
foreach ($values as $key => $value) {
$field = $this->escapeIdentifier($key);
if ($value instanceof TikiDb_Expr) {
$query .= "$field = {$value->getQueryPart($field)}, ";
$bindvars = array_merge($bindvars, SanitizeEncoding::filterMysqlUtf8($value->getValues(), $this->getUtf8Fields(), $key));
} else {
$query .= "$field = ?, ";
$bindvars[] = SanitizeEncoding::filterMysqlUtf8($value, $this->getUtf8Fields(), $key);
}
}
return rtrim($query, ' ,');
}
private function buildInsert($values, $ignore, &$bindvars)
{
$fieldDefinition = implode(', ', array_map([$this, 'escapeIdentifier'], array_keys($values)));
$fieldPlaceholders = rtrim(str_repeat('?, ', count($values)), ' ,');
if ($ignore) {
$ignore = ' IGNORE';
}
$bindvars = array_merge($bindvars, array_values(SanitizeEncoding::filterMysqlUtf8($values, $this->getUtf8Fields())));
return "INSERT$ignore INTO {$this->escapeIdentifier($this->tableName)} ($fieldDefinition) VALUES ($fieldPlaceholders)";
}
protected function escapeIdentifier($identifier)
{
return "`$identifier`";
}
/**
* return the list of fields that have charset utf8 (vs utf8mb4) in the current table
*
* @return mixed
*/
public function getUtf8Fields()
{
if (! isset(self::$utf8FieldsCache[$this->tableName])) {
$sql = "SELECT COLUMN_NAME AS col FROM information_schema.`COLUMNS` WHERE table_schema = DATABASE()"
. " AND TABLE_NAME = ? AND CHARACTER_SET_NAME = 'utf8'";
$result = $this->db->fetchAll($sql, [$this->tableName]);
$shortFormat = is_array($result) ? array_column($result, 'col') : [];
self::$utf8FieldsCache[$this->tableName] = array_combine($shortFormat, $shortFormat);
}
return self::$utf8FieldsCache[$this->tableName];
}
}