Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
831 lines (775 sloc) 24.7 KB
<?php
/** Filtered table representation
* @method NotORM_Result and(mixed $condition, mixed $parameters = array()) Add AND condition
* @method NotORM_Result or(mixed $condition, mixed $parameters = array()) Add OR condition
*/
class NotORM_Result extends NotORM_Abstract implements Iterator, ArrayAccess, Countable, JsonSerializable {
protected $single;
protected $select = array(), $conditions = array(), $where = array(), $parameters = array(), $order = array(), $limit = null, $offset = null, $group = "", $having = "", $lock = null;
protected $union = array(), $unionOrder = array(), $unionLimit = null, $unionOffset = null;
protected $data, $referencing = array(), $aggregation = array(), $accessed, $access, $keys = array();
/** Create table result
* @param string
* @param NotORM
* @param bool single row
* @access protected must be public because it is called from NotORM
*/
function __construct($table, NotORM $notORM, $single = false) {
$this->table = $table;
$this->notORM = $notORM;
$this->single = $single;
$this->primary = $notORM->structure->getPrimary($table);
}
/** Save data to cache and empty result
*/
function __destruct() {
if ($this->notORM->cache && !$this->select && isset($this->rows)) {
$access = $this->access;
if (is_array($access)) {
$access = array_filter($access);
}
$this->notORM->cache->save("$this->table;" . implode(",", $this->conditions), $access);
}
$this->rows = null;
unset($this->data);
}
protected function limitString($limit, $offset = null) {
$return = "";
if (isset($limit) && $this->notORM->driver != "oci" && $this->notORM->driver != "dblib" && $this->notORM->driver != "mssql" && $this->notORM->driver != "sqlsrv") {
$return .= " LIMIT $limit";
if ($offset) {
$return .= " OFFSET $offset";
}
}
return $return;
}
protected function removeExtraDots($expression) {
return preg_replace('~(?:\\b[a-z_][a-z0-9_.:]*[.:])?([a-z_][a-z0-9_]*)[.:]([a-z_*])~i', '\\1.\\2', $expression); // rewrite tab1.tab2.col
}
protected function whereString() {
$return = "";
if ($this->group) {
$return .= " GROUP BY $this->group";
}
if ($this->having) {
$return .= " HAVING $this->having";
}
if ($this->order) {
$return .= " ORDER BY " . implode(", ", $this->order);
}
$return = $this->removeExtraDots($return);
$where = $this->where;
if (isset($this->limit) && $this->notORM->driver == "oci") {
$where[] = ($where ? " AND " : "") . "(" . ($this->offset ? "rownum > $this->offset AND " : "") . "rownum <= " . ($this->limit + $this->offset) . ")"; //! rownum > doesn't work - requires subselect (see adminer/drivers/oracle.inc.php)
}
if ($where) {
$return = " WHERE " . implode($where) . $return;
}
$return .= $this->limitString($this->limit, $this->offset);
if (isset($this->lock)) {
$return .= ($this->lock ? " FOR UPDATE" : " LOCK IN SHARE MODE");
}
return $return;
}
protected function topString($limit, $offset = null) {
if (isset($limit) && ($this->notORM->driver == "dblib" || $this->notORM->driver == "mssql" || $this->notORM->driver == "sqlsrv")) {
return " TOP ($this->limit)"; //! offset is not supported
}
return "";
}
protected function createJoins($val) {
$return = array();
preg_match_all('~\\b([a-z_][a-z0-9_.:]*[.:])[a-z_*]~i', $val, $matches);
foreach ($matches[1] as $names) {
$parent = $this->table;
if ($names != "$parent.") { // case-sensitive
preg_match_all('~\\b([a-z_][a-z0-9_]*)([.:])~i', $names, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
list(, $name, $delimiter) = $match;
$table = $this->notORM->structure->getReferencedTable($name, $parent);
$column = ($delimiter == ':' ? $this->notORM->structure->getPrimary($parent) : $this->notORM->structure->getReferencedColumn($name, $parent));
$primary = ($delimiter == ':' ? $this->notORM->structure->getReferencedColumn($parent, $table) : $this->notORM->structure->getPrimary($table));
$return[$name] = " LEFT JOIN $table" . ($table != $name ? " AS $name" : "") . " ON $parent.$column = $name.$primary"; // should use alias if the table is used on more places
$parent = $name;
}
}
}
return $return;
}
/** Get SQL query
* @return string
*/
function __toString() {
$return = "SELECT" . $this->topString($this->limit, $this->offset) . " ";
$join = $this->createJoins(implode(",", $this->conditions) . "," . implode(",", $this->select) . ",$this->group,$this->having," . implode(",", $this->order));
if (!isset($this->rows) && $this->notORM->cache && !is_string($this->accessed)) {
$this->accessed = $this->notORM->cache->load("$this->table;" . implode(",", $this->conditions));
$this->access = $this->accessed;
}
if ($this->select) {
$return .= $this->removeExtraDots(implode(", ", $this->select));
} elseif ($this->accessed) {
$return .= ($join ? "$this->table." : "") . implode(", " . ($join ? "$this->table." : ""), array_keys($this->accessed));
} else {
$return .= ($join ? "$this->table." : "") . "*";
}
$return .= " FROM $this->table" . implode($join) . $this->whereString();
if ($this->union) {
$return = ($this->notORM->driver == "sqlite" || $this->notORM->driver == "oci" ? $return : "($return)") . implode($this->union);
if ($this->unionOrder) {
$return .= " ORDER BY " . implode(", ", $this->unionOrder);
}
$return .= $this->limitString($this->unionLimit, $this->unionOffset);
$top = $this->topString($this->unionLimit, $this->unionOffset);
if ($top) {
$return = "SELECT$top * FROM ($return) t";
}
}
return $return;
}
protected function query($query, $parameters) {
if ($this->notORM->debug) {
if (!is_callable($this->notORM->debug)) {
$debug = "$query;";
if ($parameters) {
$debug .= " -- " . implode(", ", array_map(array($this, 'quote'), $parameters));
}
$pattern = '(^' . preg_quote(dirname(__FILE__)) . '(\\.php$|[/\\\\]))'; // can be static
foreach (debug_backtrace() as $backtrace) {
if (isset($backtrace["file"]) && !preg_match($pattern, $backtrace["file"])) { // stop on first file outside NotORM source codes
break;
}
}
error_log("$backtrace[file]:$backtrace[line]:$debug\n", 0);
} elseif (call_user_func($this->notORM->debug, $query, $parameters) === false) {
return false;
}
}
$return = $this->notORM->connection->prepare($query);
if (!$return || !$return->execute(array_map(array($this, 'formatValue'), $parameters))) {
$return = false;
}
if ($this->notORM->debugTimer) {
call_user_func($this->notORM->debugTimer);
}
return $return;
}
protected function formatValue($val) {
if ($val instanceof DateTime) {
return $val->format("Y-m-d H:i:s"); //! may be driver specific
}
return $val;
}
protected function quote($val) {
if (!isset($val)) {
return "NULL";
}
if (is_array($val)) { // (a, b) IN ((1, 2), (3, 4))
return "(" . implode(", ", array_map(array($this, 'quote'), $val)) . ")";
}
$val = $this->formatValue($val);
if (is_float($val)) {
return sprintf("%F", $val); // otherwise depends on setlocale()
}
if ($val === false) {
return "0";
}
if (is_int($val) || $val instanceof NotORM_Literal) { // number or SQL code - for example "NOW()"
return (string) $val;
}
return $this->notORM->connection->quote($val);
}
/** Shortcut for call_user_func_array(array($this, 'insert'), $rows)
* @param array
* @return int number of affected rows or false in case of an error
*/
function insert_multi(array $rows) {
if ($this->notORM->freeze) {
return false;
}
if (!$rows) {
return 0;
}
$data = reset($rows);
$parameters = array();
if ($data instanceof NotORM_Result) {
$parameters = $data->parameters; //! other parameters
$data = (string) $data;
} elseif ($data instanceof Traversable) {
$data = iterator_to_array($data);
}
$insert = $data;
if (is_array($data)) {
$values = array();
foreach ($rows as $value) {
if ($value instanceof Traversable) {
$value = iterator_to_array($value);
}
$values[] = $this->quote($value);
foreach ($value as $val) {
if ($val instanceof NotORM_Literal && $val->parameters) {
$parameters = array_merge($parameters, $val->parameters);
}
}
}
//! driver specific extended insert
$insert = ($data || $this->notORM->driver == "mysql"
? "(" . implode(", ", array_keys($data)) . ") VALUES " . implode(", ", $values)
: "DEFAULT VALUES"
);
}
// requires empty $this->parameters
$return = $this->query("INSERT INTO $this->table $insert", $parameters);
if (!$return) {
return false;
}
$this->rows = null;
return $return->rowCount();
}
/** Insert row in a table
* @param mixed array($column => $value)|Traversable for single row insert or NotORM_Result|string for INSERT ... SELECT
* @param ... used for extended insert
* @return mixed inserted NotORM_Row or false in case of an error or number of affected rows for INSERT ... SELECT
*/
function insert($data) {
$rows = func_get_args();
$return = $this->insert_multi($rows);
if (!$return) {
return false;
}
if (!is_array($data)) {
return $return;
}
if (!isset($data[$this->primary]) && ($id = $this->notORM->connection->lastInsertId($this->notORM->structure->getSequence($this->table)))) {
$data[$this->primary] = $id;
}
return new $this->notORM->rowClass($data, $this);
}
/** Update all rows in result set
* @param array ($column => $value)
* @return int number of affected rows or false in case of an error
*/
function update(array $data) {
if ($this->notORM->freeze) {
return false;
}
if (!$data) {
return 0;
}
$values = array();
$parameters = array();
foreach ($data as $key => $val) {
// doesn't use binding because $this->parameters can be filled by ? or :name
$values[] = "$key = " . $this->quote($val);
if ($val instanceof NotORM_Literal && $val->parameters) {
$parameters = array_merge($parameters, $val->parameters);
}
}
if ($this->parameters) {
$parameters = array_merge($parameters, $this->parameters);
}
// joins in UPDATE are supported only in MySQL
$return = $this->query("UPDATE" . $this->topString($this->limit, $this->offset) . " $this->table SET " . implode(", ", $values) . $this->whereString(), $parameters);
if (!$return) {
return false;
}
return $return->rowCount();
}
/** Insert row or update if it already exists
* @param array ($column => $value)
* @param array ($column => $value)
* @param array ($column => $value), empty array means use $insert
* @return int number of affected rows or false in case of an error
*/
function insert_update(array $unique, array $insert, array $update = array()) {
if (!$update) {
$update = $insert;
}
$insert = $unique + $insert;
$values = "(" . implode(", ", array_keys($insert)) . ") VALUES " . $this->quote($insert);
//! parameters
if ($this->notORM->driver == "mysql") {
$set = array();
if (!$update) {
$update = $unique;
}
foreach ($update as $key => $val) {
$set[] = "$key = " . $this->quote($val);
//! parameters
}
return $this->insert("$values ON DUPLICATE KEY UPDATE " . implode(", ", $set));
} else {
$connection = $this->notORM->connection;
$errorMode = $connection->getAttribute(PDO::ATTR_ERRMODE);
$connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
try {
$return = $this->insert($values);
$connection->setAttribute(PDO::ATTR_ERRMODE, $errorMode);
return $return;
} catch (PDOException $e) {
$connection->setAttribute(PDO::ATTR_ERRMODE, $errorMode);
if ($e->getCode() == "23000" || $e->getCode() == "23505") { // "23000" - duplicate key, "23505" unique constraint pgsql
if (!$update) {
return 0;
}
$clone = clone $this;
$return = $clone->where($unique)->update($update);
return ($return ? $return + 1 : $return);
}
if ($errorMode == PDO::ERRMODE_EXCEPTION) {
throw $e;
} elseif ($errorMode == PDO::ERRMODE_WARNING) {
trigger_error("PDOStatement::execute(): " . $e->getMessage(), E_USER_WARNING); // E_WARNING is unusable
}
}
}
}
/** Get last insert ID
* @return string number
*/
function insert_id() {
return $this->notORM->connection->lastInsertId();
}
/** Delete all rows in result set
* @return int number of affected rows or false in case of an error
*/
function delete() {
if ($this->notORM->freeze) {
return false;
}
$return = $this->query("DELETE" . $this->topString($this->limit, $this->offset) . " FROM $this->table" . $this->whereString(), $this->parameters);
if (!$return) {
return false;
}
return $return->rowCount();
}
/** Add select clause, more calls appends to the end
* @param string for example "column, MD5(column) AS column_md5", empty string to reset previously set columns
* @param string ...
* @return NotORM_Result fluent interface
*/
function select($columns) {
$this->__destruct();
if ($columns != "") {
foreach (func_get_args() as $columns) {
$this->select[] = $columns;
}
} else {
$this->select = array();
}
return $this;
}
/** Add where condition, more calls appends with AND
* @param mixed string possibly containing ? or :name; or array($condition => $parameters, ...)
* @param mixed array accepted by PDOStatement::execute or a scalar value
* @param mixed ...
* @return NotORM_Result fluent interface
*/
function where($condition, $parameters = array()) {
$args = func_get_args();
return $this->whereOperator("AND", $args);
}
protected function whereOperator($operator, array $args) {
$condition = $args[0];
$parameters = (count($args) > 1 ? $args[1] : array());
if (is_array($condition)) { // where(array("column1" => 1, "column2 > ?" => 2))
foreach ($condition as $key => $val) {
$this->where($key, $val);
}
return $this;
}
$this->__destruct();
$this->conditions[] = "$operator $condition";
$condition = $this->removeExtraDots($condition);
if (count($args) != 2 || strpbrk($condition, "?:")) { // where("column < ? OR column > ?", array(1, 2))
if (count($args) != 2 || !is_array($parameters)) { // where("column < ? OR column > ?", 1, 2)
$parameters = array_slice($args, 1);
}
$this->parameters = array_merge($this->parameters, $parameters);
} elseif ($parameters === null) { // where("column", null)
$condition .= " IS NULL";
} elseif ($parameters instanceof NotORM_Result) { // where("column", $db->$table())
$clone = clone $parameters;
if (!$clone->select) {
$clone->select($this->notORM->structure->getPrimary($clone->table));
}
if ($this->notORM->driver != "mysql") {
if ($clone instanceof NotORM_MultiResult) {
array_shift($clone->select);
$clone->single();
}
$condition .= " IN ($clone)";
$this->parameters = array_merge($this->parameters, $clone->parameters);
} else {
$in = array();
foreach ($clone as $row) {
$row = array_values(iterator_to_array($row));
if ($clone instanceof NotORM_MultiResult && count($row) > 1) {
array_shift($row);
}
if (count($row) == 1) {
$in[] = $this->quote($row[0]);
} else {
$in[] = $this->quote($row);
}
}
if ($in) {
$condition .= " IN (" . implode(", ", $in) . ")";
} else {
$condition = "($condition) IS NOT NULL AND $condition IS NULL"; // $condition = "NOT id"
}
}
} elseif (!is_array($parameters)) { // where("column", "x")
$condition .= " = " . $this->quote($parameters);
} else { // where("column", array(1, 2))
$condition = $this->whereIn($condition, $parameters);
}
$this->where[] = (preg_match('~^\)+$~', $condition)
? $condition
: ($this->where ? " $operator " : "") . "($condition)"
);
return $this;
}
protected function whereIn($condition, $parameters) {
if (!$parameters) {
$condition = "($condition) IS NOT NULL AND $condition IS NULL";
} elseif ($this->notORM->driver != "oci") {
$column = $condition;
$condition .= " IN " . $this->quote($parameters);
$nulls = array_filter($parameters, 'is_null');
if ($nulls) {
$condition = "$condition OR $column IS NULL";
}
} else { // http://download.oracle.com/docs/cd/B19306_01/server.102/b14200/expressions014.htm
$or = array();
for ($i=0; $i < count($parameters); $i += 1000) {
$or[] = "$condition IN " . $this->quote(array_slice($parameters, $i, 1000));
}
$condition = implode(" OR ", $or);
}
return $condition;
}
function __call($name, array $args) {
$operator = strtoupper($name);
switch ($operator) {
case "AND":
case "OR":
return $this->whereOperator($operator, $args);
}
trigger_error("Call to undefined method NotORM_Result::$name()", E_USER_ERROR);
}
/** Shortcut for where()
* @param string
* @param mixed
* @param mixed ...
* @return NotORM_Result fluent interface
*/
function __invoke($where, $parameters = array()) {
$args = func_get_args();
return $this->whereOperator("AND", $args);
}
/** Add order clause, more calls appends to the end
* @param mixed "column1, column2 DESC" or array("column1", "column2 DESC"), empty string to reset previous order
* @param string ...
* @return NotORM_Result fluent interface
*/
function order($columns) {
$this->rows = null;
if ($columns != "") {
$columns = (is_array($columns) ? $columns : func_get_args());
foreach ($columns as $column) {
if ($this->union) {
$this->unionOrder[] = $column;
} else {
$this->order[] = $column;
}
}
} elseif ($this->union) {
$this->unionOrder = array();
} else {
$this->order = array();
}
return $this;
}
/** Set limit clause, more calls rewrite old values
* @param int
* @param int
* @return NotORM_Result fluent interface
*/
function limit($limit, $offset = null) {
$this->rows = null;
if ($this->union) {
$this->unionLimit = +$limit;
$this->unionOffset = +$offset;
} else {
$this->limit = +$limit;
$this->offset = +$offset;
}
return $this;
}
/** Set group clause, more calls rewrite old values
* @param string
* @param string
* @return NotORM_Result fluent interface
*/
function group($columns, $having = "") {
$this->__destruct();
$this->group = $columns;
$this->having = $having;
return $this;
}
/** Set select FOR UPDATE or LOCK IN SHARE MODE
* @param bool
* @return NotORM_Result fluent interface
*/
function lock($exclusive = true) {
$this->lock = $exclusive;
return $this;
}
/**
* @param NotORM_Result
* @param bool
* @return NotORM_Result fluent interface
*/
function union(NotORM_Result $result, $all = false) {
$this->union[] = " UNION " . ($all ? "ALL " : "") . ($this->notORM->driver == "sqlite" || $this->notORM->driver == "oci" ? $result : "($result)");
$this->parameters = array_merge($this->parameters, $result->parameters);
return $this;
}
/** Execute aggregation function
* @param string
* @return string
*/
function aggregation($function) {
$join = $this->createJoins(implode(",", $this->conditions) . ",$function");
$query = "SELECT $function FROM $this->table" . implode($join);
if ($this->where) {
$query .= " WHERE " . implode($this->where);
}
foreach ($this->query($query, $this->parameters)->fetch() as $return) {
return $return;
}
}
/** Count number of rows
* @param string
* @return int
*/
function count($column = "") {
if (!$column) {
$this->execute();
return count($this->data);
}
return $this->aggregation("COUNT($column)");
}
/** Return minimum value from a column
* @param string
* @return int
*/
function min($column) {
return $this->aggregation("MIN($column)");
}
/** Return maximum value from a column
* @param string
* @return int
*/
function max($column) {
return $this->aggregation("MAX($column)");
}
/** Return sum of values in a column
* @param string
* @return int
*/
function sum($column) {
return $this->aggregation("SUM($column)");
}
/** Execute the built query
* @return null
*/
protected function execute() {
if (!isset($this->rows)) {
$result = false;
$exception = null;
$parameters = array();
foreach (array_merge($this->select, array($this, $this->group, $this->having), $this->order, $this->unionOrder) as $val) {
if (($val instanceof NotORM_Literal || $val instanceof self) && $val->parameters) {
$parameters = array_merge($parameters, $val->parameters);
}
}
try {
$result = $this->query($this->__toString(), $parameters);
} catch (PDOException $exception) {
// handled later
}
if (!$result) {
if (!$this->select && $this->accessed) {
$this->accessed = '';
$this->access = array();
$result = $this->query($this->__toString(), $parameters);
} elseif ($exception) {
throw $exception;
}
}
$this->rows = array();
if ($result) {
$result->setFetchMode(PDO::FETCH_ASSOC);
foreach ($result as $key => $row) {
if (isset($row[$this->primary])) {
$key = $row[$this->primary];
if (!is_string($this->access)) {
$this->access[$this->primary] = true;
}
}
$this->rows[$key] = new $this->notORM->rowClass($row, $this);
}
}
$this->data = $this->rows;
}
}
/** Fetch next row of result
* @param string column name to return or an empty string for the whole row
* @return mixed string or null with $column, NotORM_Row without $column, false if there is no row
*/
function fetch($column = '') {
// no $this->select($column) because next calls can access different columns
$this->execute();
$return = current($this->data);
next($this->data);
if ($return && $column != '') {
return $return[$column];
}
return $return;
}
/** Fetch all rows as associative array
* @param string
* @param string column name used for an array value or an empty string for the whole row
* @return array
*/
function fetchPairs($key, $value = '') {
$return = array();
$clone = clone $this;
if ($value != "") {
$clone->select = array();
$clone->select("$key, $value"); // MultiResult adds its column
} elseif ($clone->select) {
array_unshift($clone->select, $key);
} else {
$clone->select = array("$key, $this->table.*");
}
foreach ($clone as $row) {
$values = array_values(iterator_to_array($row));
if ($value != "" && $clone instanceof NotORM_MultiResult) {
array_shift($values);
}
$return[(string) $values[0]] = ($value != "" ? $values[(array_key_exists(1, $values) ? 1 : 0)] : $row); // isset($values[1]) - fetchPairs("id", "id")
}
return $return;
}
protected function access($key, $delete = false) {
if ($delete) {
if (is_array($this->access)) {
$this->access[$key] = false;
}
return false;
}
if (!isset($key)) {
$this->access = '';
} elseif (!is_string($this->access)) {
$this->access[$key] = true;
}
if (!$this->select && $this->accessed && (!isset($key) || !isset($this->accessed[$key]))) {
$this->accessed = '';
$this->rows = null;
return true;
}
return false;
}
protected function single() {
}
// Iterator implementation (not IteratorAggregate because $this->data can be changed during iteration)
function rewind() {
$this->execute();
$this->keys = array_keys($this->data);
reset($this->keys);
}
/** @return NotORM_Row */
function current() {
return $this->data[current($this->keys)];
}
/** @return string row ID */
function key() {
return current($this->keys);
}
function next() {
next($this->keys);
}
function valid() {
return current($this->keys) !== false;
}
// ArrayAccess implementation
/** Test if row exists
* @param string row ID or array for where conditions
* @return bool
*/
function offsetExists($key) {
$row = $this->offsetGet($key);
return isset($row);
}
/** Get specified row
* @param string row ID or array for where conditions
* @return NotORM_Row or null if there is no such row
*/
function offsetGet($key) {
if ($this->single && !isset($this->data)) {
$clone = clone $this;
if (is_array($key)) {
$clone->where($key)->limit(1);
} else {
$clone->where($this->primary, $key);
}
$return = $clone->fetch();
if ($return) {
return $return;
}
} else {
$this->execute();
if (is_array($key)) {
foreach ($this->data as $row) {
foreach ($key as $k => $v) {
if ((isset($v) && $row[$k] !== null ? $row[$k] != $v : $row[$k] !== $v)) {
continue 2;
}
}
return $row;
}
} elseif (isset($this->data[$key])) {
return $this->data[$key];
}
}
}
/** Mimic row
* @param string row ID
* @param NotORM_Row
* @return null
*/
function offsetSet($key, $value) {
$this->execute();
$this->data[$key] = $value;
}
/** Remove row from result set
* @param string row ID
* @return null
*/
function offsetUnset($key) {
$this->execute();
unset($this->data[$key]);
}
// JsonSerializable implementation
function jsonSerialize() {
$this->execute();
if ($this->notORM->jsonAsArray) {
return array_values($this->data);
} else {
return $this->data;
}
}
}