| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931 |
- <?php
- /*
- * Copyright 2010-2015 by MODX, LLC.
- *
- * This file is part of xPDO.
- *
- * xPDO is free software; you can redistribute it and/or modify it under the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 2 of the License, or (at your option) any later
- * version.
- *
- * xPDO is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * xPDO; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
- * Suite 330, Boston, MA 02111-1307 USA
- */
- /**
- * A class for constructing complex SQL statements using a model-aware API.
- *
- * @package xpdo
- * @subpackage om
- */
- /**
- * An xPDOCriteria derivative with methods for constructing complex statements.
- *
- * @abstract
- * @package xpdo
- * @subpackage om
- */
- abstract class xPDOQuery extends xPDOCriteria {
- const SQL_AND = 'AND';
- const SQL_OR = 'OR';
- const SQL_JOIN_CROSS = 'JOIN';
- const SQL_JOIN_LEFT = 'LEFT JOIN';
- const SQL_JOIN_RIGHT = 'RIGHT JOIN';
- const SQL_JOIN_NATURAL_LEFT = 'NATURAL LEFT JOIN';
- const SQL_JOIN_NATURAL_RIGHT = 'NATURAL RIGHT JOIN';
- const SQL_JOIN_STRAIGHT = 'STRAIGHT_JOIN';
- /**
- * An array of symbols and keywords indicative of SQL operators.
- *
- * @var array
- * @todo Refactor this to separate xPDOQuery operators from db-specific conditional statement identifiers.
- */
- protected $_operators= array (
- '=',
- '!=',
- '<',
- '<=',
- '>',
- '>=',
- '<=>',
- ' LIKE ',
- ' IS NULL',
- ' IS NOT NULL',
- ' BETWEEN ',
- ' IN ',
- ' IN(',
- ' NOT(',
- ' NOT (',
- ' NOT IN ',
- ' NOT IN(',
- ' EXISTS (',
- ' EXISTS(',
- ' NOT EXISTS (',
- ' NOT EXISTS(',
- ' COALESCE(',
- ' GREATEST(',
- ' INTERVAL(',
- ' LEAST(',
- 'MATCH(',
- 'MATCH (',
- 'MAX(',
- 'MIN(',
- 'AVG('
- );
- protected $_quotable= array ('string', 'password', 'date', 'datetime', 'timestamp', 'time', 'json', 'array');
- protected $_class= null;
- protected $_alias= null;
- protected $_tableClass = null;
- public $graph= array ();
- public $query= array (
- 'command' => 'SELECT',
- 'distinct' => '',
- 'columns' => '',
- 'from' => array (
- 'tables' => array (),
- 'joins' => array (),
- ),
- 'set' => array (),
- 'where' => array (),
- 'groupby' => array (),
- 'having' => array (),
- 'orderby' => array (),
- 'offset' => '',
- 'limit' => '',
- );
- /**
- * Make sure a clause is valid and does not contain SQL injection attempts.
- *
- * @param string $clause The string clause to validate.
- *
- * @return bool True if the clause is valid.
- */
- public static function isValidClause($clause) {
- $output = rtrim($clause, ' ;');
- $output = preg_replace("/\\\\'.*?\\\\'/", '{mask}', $output);
- $output = preg_replace('/\\".*?\\"/', '{mask}', $output);
- $output = preg_replace("/'.*?'/", '{mask}', $output);
- $output = preg_replace('/".*?"/', '{mask}', $output);
- return strpos($output, ';') === false && strpos(strtolower($output), 'union') === false;
- }
- public function __construct(& $xpdo, $class, $criteria= null) {
- parent :: __construct($xpdo);
- if ($class= $this->xpdo->loadClass($class)) {
- $this->_class= $class;
- $this->_alias= $class;
- $this->_tableClass = $this->xpdo->getTableClass($this->_class);
- $this->query['from']['tables'][0]= array (
- 'table' => $this->xpdo->getTableName($this->_class),
- 'alias' => & $this->_alias
- );
- if ($criteria !== null) {
- if (is_object($criteria)) {
- $this->wrap($criteria);
- }
- else {
- $this->where($criteria);
- }
- }
- }
- }
- public function getClass() {
- return $this->_class;
- }
- public function getAlias() {
- return $this->_alias;
- }
- public function getTableClass() {
- return $this->_tableClass;
- }
- /**
- * Set the type of SQL command you want to build.
- *
- * The default is SELECT, though it also supports DELETE and UPDATE.
- *
- * @param string $command The type of SQL statement represented by this object. Default is 'SELECT'.
- * @return xPDOQuery Returns the current object for convenience.
- */
- public function command($command= 'SELECT') {
- $command= strtoupper(trim($command));
- if (preg_match('/(SELECT|UPDATE|DELETE)/', $command)) {
- $this->query['command']= $command;
- if (in_array($command, array('DELETE','UPDATE'))) $this->_alias= $this->xpdo->getTableName($this->_class);
- }
- return $this;
- }
- /**
- * Set the DISTINCT attribute of the query.
- *
- * @param null|boolean $on Defines how to set the distinct attribute:
- * - null (default) indicates the distinct attribute should be toggled
- * - any other value is treated as a boolean, i.e. true to set DISTINCT, false to unset
- * @return xPDOQuery Returns the current object for convenience.
- */
- public function distinct($on = null) {
- if ($on === null) {
- if (empty($this->query['distinct']) || $this->query['distinct'] !== 'DISTINCT') {
- $this->query['distinct']= 'DISTINCT';
- } else {
- $this->query['distinct']= '';
- }
- } else {
- $this->query['distinct']= $on == true ? 'DISTINCT' : '';
- }
- return $this;
- }
- /**
- * Sets a SQL alias for the table represented by the main class.
- *
- * @param string $alias An alias for the main table for the SQL statement.
- * @return xPDOQuery Returns the current object for convenience.
- */
- public function setClassAlias($alias= '') {
- $this->_alias= $alias;
- return $this;
- }
- /**
- * Specify columns to return from the SQL query.
- *
- * @param string $columns Columns to return from the query.
- * @return xPDOQuery Returns the current object for convenience.
- */
- public function select($columns= '*') {
- if (!is_array($columns)) {
- $columns= trim($columns);
- if ($columns == '*' || $columns === $this->_alias . '.*' || $columns === $this->xpdo->escape($this->_alias) . '.*') {
- $columns= $this->xpdo->getSelectColumns($this->_class, $this->_alias, $this->_alias . '_');
- }
- $columns= explode(',', $columns);
- foreach ($columns as $colKey => $column) $columns[$colKey] = trim($column);
- }
- if (is_array ($columns)) {
- if (!is_array($this->query['columns'])) {
- $this->query['columns']= $columns;
- } else {
- $this->query['columns']= array_merge($this->query['columns'], $columns);
- }
- }
- return $this;
- }
- /**
- * Specify the SET clause(s) for a SQL UPDATE query.
- *
- * @param array $values An associative array of fields and the values to set them to.
- * @return xPDOQuery Returns a reference to the current instance for convenience.
- */
- public function set(array $values) {
- $fieldMeta= $this->xpdo->getFieldMeta($this->_class);
- $fieldAliases= $this->xpdo->getFieldAliases($this->_class);
- foreach ($values as $key => $value) {
- $type= null;
- if (!array_key_exists($key, $fieldMeta)) {
- if (array_key_exists($key, $fieldAliases)) {
- $key = $fieldAliases[$key];
- } else {
- continue;
- }
- }
- if (array_key_exists($key, $fieldMeta)) {
- if ($value === null) {
- $type= PDO::PARAM_NULL;
- }
- elseif (!in_array($fieldMeta[$key]['phptype'], $this->_quotable)) {
- $type= PDO::PARAM_INT;
- }
- elseif (strpos($value, '(') === false && !$this->isConditionalClause($value)) {
- $type= PDO::PARAM_STR;
- }
- $this->query['set'][$key]= array('value' => $value, 'type' => $type);
- }
- }
- return $this;
- }
- /**
- * Join a table represented by the specified class.
- *
- * @param string $class The classname (or relation alias for aggregates and
- * composites) of representing the table to be joined.
- * @param string $alias An optional alias to represent the joined table in
- * the constructed query.
- * @param string $type The type of join to perform. See the xPDOQuery::SQL_JOIN
- * constants.
- * @param mixed $conditions Conditions of the join specified in any xPDO
- * compatible criteria object or expression.
- * @param string $conjunction A conjunction to be applied to the condition
- * or conditions supplied.
- * @param array $binding Optional bindings to accompany the conditions.
- * @param int $condGroup An optional identifier for adding the conditions
- * to a specific set of conjoined expressions.
- * @return xPDOQuery Returns the current object for convenience.
- */
- public function join($class, $alias= '', $type= xPDOQuery::SQL_JOIN_CROSS, $conditions= array (), $conjunction= xPDOQuery::SQL_AND, $binding= null, $condGroup= 0) {
- if ($this->xpdo->loadClass($class)) {
- $alias= $alias ? $alias : $class;
- $target= & $this->query['from']['joins'];
- $targetIdx= count($target);
- $target[$targetIdx]= array (
- 'table' => $this->xpdo->getTableName($class),
- 'class' => $class,
- 'alias' => $alias,
- 'type' => $type,
- 'conditions' => array ()
- );
- if (empty ($conditions)) {
- $fkMeta= $this->xpdo->getFKDefinition($this->_class, $alias);
- if ($fkMeta) {
- $parentAlias= isset ($this->_alias) ? $this->_alias : $this->_class;
- $local= $fkMeta['local'];
- $foreign= $fkMeta['foreign'];
- $conditions= $this->xpdo->escape($parentAlias) . '.' . $this->xpdo->escape($local) . ' = ' . $this->xpdo->escape($alias) . '.' . $this->xpdo->escape($foreign);
- if (isset($fkMeta['criteria']['local'])) {
- $localCriteria = array();
- if (is_array($fkMeta['criteria']['local'])) {
- foreach ($fkMeta['criteria']['local'] as $critKey => $critVal) {
- if (is_numeric($critKey)) {
- $localCriteria[] = $critVal;
- } else {
- $localCriteria["{$this->_class}.{$critKey}"] = $critVal;
- }
- }
- }
- if (!empty($localCriteria)) {
- $conditions = array($localCriteria, $conditions);
- }
- $foreignCriteria = array();
- if (is_array($fkMeta['criteria']['foreign'])) {
- foreach ($fkMeta['criteria']['foreign'] as $critKey => $critVal) {
- if (is_numeric($critKey)) {
- $foreignCriteria[] = $critVal;
- } else {
- $foreignCriteria["{$parentAlias}.{$critKey}"] = $critVal;
- }
- }
- }
- if (!empty($foreignCriteria)) {
- $conditions = array($foreignCriteria, $conditions);
- }
- }
- }
- }
- $this->condition($target[$targetIdx]['conditions'], $conditions, $conjunction, $binding, $condGroup);
- }
- return $this;
- }
- public function innerJoin($class, $alias= '', $conditions= array (), $conjunction= xPDOQuery::SQL_AND, $binding= null, $condGroup= 0) {
- return $this->join($class, $alias, xPDOQuery::SQL_JOIN_CROSS, $conditions, $conjunction, $binding, $condGroup);
- }
- public function leftJoin($class, $alias= '', $conditions= array (), $conjunction= xPDOQuery::SQL_AND, $binding= null, $condGroup= 0) {
- return $this->join($class, $alias, xPDOQuery::SQL_JOIN_LEFT, $conditions, $conjunction, $binding, $condGroup);
- }
- public function rightJoin($class, $alias= '', $conditions= array (), $conjunction= xPDOQuery::SQL_AND, $binding= null, $condGroup= 0) {
- return $this->join($class, $alias, xPDOQuery::SQL_JOIN_RIGHT, $conditions, $conjunction, $binding, $condGroup);
- }
- /**
- * Add a FROM clause to the query.
- *
- * @param string $class The class representing the table to add.
- * @param string $alias An optional alias for the class.
- * @return xPDOQuery Returns the instance.
- */
- public function from($class, $alias= '') {
- if ($class= $this->xpdo->loadClass($class)) {
- $alias= $alias ? $alias : $class;
- $this->query['from']['tables'][]= array (
- 'table' => $this->xpdo->getTableName($class),
- 'alias' => $alias
- );
- }
- return $this;
- }
- /**
- * Add a condition to the query.
- *
- * @param string $target The target clause for the condition.
- * @param mixed $conditions A valid xPDO criteria expression.
- * @param string $conjunction The conjunction to use when appending this condition, i.e., AND or OR.
- * @param mixed $binding A value or PDO binding representation of a value for the condition.
- * @param integer $condGroup A numeric identifier for associating conditions into groups.
- * @return xPDOQuery Returns the instance.
- */
- public function condition(& $target, $conditions= '1', $conjunction= xPDOQuery::SQL_AND, $binding= null, $condGroup= 0) {
- $condGroup= intval($condGroup);
- if (!isset ($target[$condGroup])) $target[$condGroup]= array ();
- try {
- $target[$condGroup][] = $this->parseConditions($conditions, $conjunction);
- } catch (xPDOException $e) {
- $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, $e->getMessage());
- $this->where("2=1");
- }
- return $this;
- }
- /**
- * Add a WHERE condition to the query.
- *
- * @param mixed $conditions A valid xPDO criteria expression.
- * @param string $conjunction The conjunction to use when appending this condition, i.e., AND or OR.
- * @param mixed $binding A value or PDO binding representation of a value for the condition.
- * @param integer $condGroup A numeric identifier for associating conditions into groups.
- * @return xPDOQuery Returns the instance.
- */
- public function where($conditions= '', $conjunction= xPDOQuery::SQL_AND, $binding= null, $condGroup= 0) {
- $this->condition($this->query['where'], $conditions, $conjunction, $binding, $condGroup);
- return $this;
- }
- public function andCondition($conditions, $binding= null, $group= 0) {
- $this->where($conditions, xPDOQuery::SQL_AND, $binding, $group);
- return $this;
- }
- public function orCondition($conditions, $binding= null, $group= 0) {
- $this->where($conditions, xPDOQuery::SQL_OR, $binding, $group);
- return $this;
- }
- /**
- * Add an ORDER BY clause to the query.
- *
- * @param string $column Column identifier to sort by.
- * @param string $direction The direction to sort by, ASC or DESC.
- * @return xPDOQuery Returns the instance.
- */
- public function sortby($column, $direction= 'ASC') {
- /* The direction can only be ASC or DESC; anything else is bogus */
- if (!in_array(strtoupper($direction), array('ASC', 'DESC', 'ASCENDING', 'DESCENDING'), true)) {
- $direction = '';
- }
- if (!static::isValidClause($column)) {
- $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'SQL injection attempt detected in sortby column; clause rejected');
- } elseif (!empty($column)) {
- $this->query['sortby'][] = array('column' => $column, 'direction' => $direction);
- }
- return $this;
- }
- /**
- * Add an GROUP BY clause to the query.
- *
- * @param string $column Column identifier to group by.
- * @param string $direction The direction to sort by, ASC or DESC.
- * @return xPDOQuery Returns the instance.
- */
- public function groupby($column, $direction= '') {
- $this->query['groupby'][]= array ('column' => $column, 'direction' => $direction);
- return $this;
- }
- public function having($conditions) {
- try {
- $this->query['having'][] = $this->parseConditions((array)$conditions);
- } catch (xPDOException $e) {
- $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, $e->getMessage());
- $this->where("2=1");
- }
- return $this;
- }
- /**
- * Add a LIMIT/OFFSET clause to the query.
- *
- * @param integer $limit The number of records to return.
- * @param integer $offset The location in the result set to start from.
- * @return xPDOQuery Returns the instance.
- */
- public function limit($limit, $offset= 0) {
- $this->query['limit']= (int)$limit;
- $this->query['offset']= (int)$offset;
- return $this;
- }
- /**
- * Bind an object graph to the query.
- *
- * @param mixed $graph An array or JSON graph of related objects.
- * @return xPDOQuery Returns the instance.
- */
- public function bindGraph($graph) {
- if (is_string($graph)) {
- $graph= $this->xpdo->fromJSON($graph);
- }
- if (is_array ($graph)) {
- if ($this->graph !== $graph) {
- $this->graph= $graph;
- $this->select($this->xpdo->getSelectColumns($this->_class, $this->_alias, $this->_alias . '_'));
- foreach ($this->graph as $relationAlias => $subRelations) {
- $this->bindGraphNode($this->_class, $this->_alias, $relationAlias, $subRelations);
- }
- if ($pk= $this->xpdo->getPK($this->_class)) {
- if (is_array ($pk)) {
- foreach ($pk as $key) {
- $this->sortby($this->xpdo->escape($this->_alias) . '.' . $this->xpdo->escape($key), 'ASC');
- }
- } else {
- $this->sortby($this->xpdo->escape($this->_alias) . '.' . $this->xpdo->escape($pk), 'ASC');
- }
- }
- }
- }
- return $this;
- }
- /**
- * Bind the node of an object graph to the query.
- *
- * @param string $parentClass The class representing the relation parent.
- * @param string $parentAlias The alias the class is assuming.
- * @param string $classAlias The class representing the related graph node.
- * @param array $relations Child relations of the current graph node.
- */
- public function bindGraphNode($parentClass, $parentAlias, $classAlias, $relations) {
- if ($fkMeta= $this->xpdo->getFKDefinition($parentClass, $classAlias)) {
- $class= $fkMeta['class'];
- $local= $fkMeta['local'];
- $foreign= $fkMeta['foreign'];
- $this->select($this->xpdo->getSelectColumns($class, $classAlias, $classAlias . '_'));
- $expression= $this->xpdo->escape($parentAlias) . '.' . $this->xpdo->escape($local) . ' = ' . $this->xpdo->escape($classAlias) . '.' . $this->xpdo->escape($foreign);
- if (isset($fkMeta['criteria']['local'])) {
- $localCriteria = array();
- if (is_array($fkMeta['criteria']['local'])) {
- foreach ($fkMeta['criteria']['local'] as $critKey => $critVal) {
- if (is_numeric($critKey)) {
- $localCriteria[] = $critVal;
- } else {
- $localCriteria["{$classAlias}.{$critKey}"] = $critVal;
- }
- }
- }
- if (!empty($localCriteria)) {
- $expression = array($localCriteria, $expression);
- }
- $foreignCriteria = array();
- if (is_array($fkMeta['criteria']['foreign'])) {
- foreach ($fkMeta['criteria']['foreign'] as $critKey => $critVal) {
- if (is_numeric($critKey)) {
- $foreignCriteria[] = $critVal;
- } else {
- $foreignCriteria["{$parentAlias}.{$critKey}"] = $critVal;
- }
- }
- }
- if (!empty($foreignCriteria)) {
- $expression = array($foreignCriteria, $expression);
- }
- }
- $this->leftJoin($class, $classAlias, $expression);
- if (!empty ($relations)) {
- foreach ($relations as $relationAlias => $subRelations) {
- $this->bindGraphNode($class, $classAlias, $relationAlias, $subRelations);
- }
- }
- }
- }
- /**
- * Hydrates a graph of related objects from a single result set.
- *
- * @param array|PDOStatement $rows A collection of result set rows or an
- * executed PDOStatement to fetch rows from to hydrating the graph.
- * @param bool $cacheFlag Indicates if the objects should be cached and
- * optionally, by specifying an integer value, for how many seconds.
- * @return array A collection of objects with all related objects from the
- * graph pre-populated.
- */
- public function hydrateGraph($rows, $cacheFlag = true) {
- $instances= array ();
- $collectionCaching = $this->xpdo->getOption(xPDO::OPT_CACHE_DB_COLLECTIONS, array(), 1);
- if (is_object($rows)) {
- if ($cacheFlag && $this->xpdo->_cacheEnabled && $collectionCaching > 0) {
- $cacheRows = array();
- }
- while ($row = $rows->fetch(PDO::FETCH_ASSOC)) {
- $this->hydrateGraphParent($instances, $row);
- if ($cacheFlag && $this->xpdo->_cacheEnabled && $collectionCaching > 0) {
- $cacheRows[]= $row;
- }
- }
- if ($cacheFlag && $this->xpdo->_cacheEnabled && $collectionCaching > 0) {
- $this->xpdo->toCache($this, $cacheRows, $cacheFlag);
- }
- } elseif (is_array($rows)) {
- foreach ($rows as $row) {
- $this->hydrateGraphParent($instances, $row);
- }
- }
- return $instances;
- }
- public function hydrateGraphParent(& $instances, $row) {
- $hydrated = false;
- $instance = $this->xpdo->call($this->getClass(), '_loadInstance', array(& $this->xpdo, $this->getClass(), $this->getAlias(), $row));
- if (is_object($instance)) {
- $pk= $instance->getPrimaryKey();
- if (is_array($pk)) $pk= implode('-', $pk);
- if (isset ($instances[$pk])) {
- $instance= & $instances[$pk];
- }
- foreach ($this->graph as $relationAlias => $subRelations) {
- $this->hydrateGraphNode($row, $instance, $relationAlias, $subRelations);
- }
- $instances[$pk]= $instance;
- $hydrated = true;
- }
- return $hydrated;
- }
- /**
- * Hydrates a node of the object graph.
- *
- * @param array $row The result set representing the current node.
- * @param xPDOObject $instance The xPDOObject instance to be hydrated from the node.
- * @param string $alias The alias identifying the object in the parent relationship.
- * @param array $relations Child relations of the current node.
- */
- public function hydrateGraphNode(& $row, & $instance, $alias, $relations) {
- $relObj= null;
- if ($relationMeta= $instance->getFKDefinition($alias)) {
- if ($row[$alias.'_'.$relationMeta['foreign']] != null) {
- $relObj = $this->xpdo->call($relationMeta['class'], '_loadInstance', array(& $this->xpdo, $relationMeta['class'], $alias, $row));
- if ($relObj) {
- if (strtolower($relationMeta['cardinality']) == 'many') {
- $instance->addMany($relObj, $alias);
- } else {
- $instance->addOne($relObj, $alias);
- }
- }
- }
- }
- if (!empty($relations) && is_object($relObj)) {
- foreach ($relations as $relationAlias => $subRelations) {
- if (is_array($subRelations) && !empty($subRelations)) {
- foreach ($subRelations as $subRelation) {
- $this->hydrateGraphNode($row, $relObj, $relationAlias, $subRelation);
- }
- } else {
- $this->hydrateGraphNode($row, $relObj, $relationAlias, null);
- }
- }
- }
- }
- /**
- * Constructs the SQL query from the xPDOQuery definition.
- *
- * @return boolean Returns true if a SQL statement was successfully constructed.
- */
- abstract public function construct();
- /**
- * Prepares the xPDOQuery for execution.
- *
- * @return PDOStatement The PDOStatement representing the prepared query.
- */
- public function prepare($bindings= array (), $byValue= true, $cacheFlag= null) {
- $this->stmt= null;
- if ($this->construct() && $this->stmt= $this->xpdo->prepare($this->sql)) {
- $this->bind($bindings, $byValue, $cacheFlag);
- } else {
- $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Could not construct or prepare query because it is invalid or could not connect: ' . $this->sql);
- }
- return $this->stmt;
- }
- /**
- * Parses an xPDO condition expression into one or more xPDOQueryConditions.
- *
- * @param mixed $conditions A valid xPDO condition expression.
- * @param string $conjunction The optional conjunction for the condition( s ).
- * @return array||xPDOQueryCondition An xPDOQueryCondition or array of xPDOQueryConditions.
- */
- public function parseConditions($conditions, $conjunction = xPDOQuery::SQL_AND) {
- $result= array ();
- $pk= $this->xpdo->getPK($this->_class);
- $pktype= $this->xpdo->getPKType($this->_class);
- $fieldMeta= $this->xpdo->getFieldMeta($this->_class, true);
- $fieldAliases= $this->xpdo->getFieldAliases($this->_class);
- $command= strtoupper($this->query['command']);
- $alias= $command == 'SELECT' ? $this->_class : $this->xpdo->getTableName($this->_class, false);
- $alias= trim($alias, $this->xpdo->_escapeCharOpen . $this->xpdo->_escapeCharClose);
- if (is_array($conditions)) {
- if (isset($conditions[0]) && is_scalar($conditions[0]) && !$this->isConditionalClause($conditions[0]) && is_array($pk) && count($conditions) == count($pk)) {
- $iteration= 0;
- foreach ($pk as $k) {
- if (!isset ($conditions[$iteration])) {
- $conditions[$iteration]= null;
- }
- $isString= in_array($fieldMeta[$k]['phptype'], $this->_quotable);
- $field= array();
- $field['sql']= $this->xpdo->escape($alias) . '.' . $this->xpdo->escape($k) . " = ?";
- $field['binding']= array (
- 'value' => $conditions[$iteration],
- 'type' => $isString ? PDO::PARAM_STR : PDO::PARAM_INT,
- 'length' => 0
- );
- $field['conjunction']= $conjunction;
- $result[$iteration]= new xPDOQueryCondition($field);
- $iteration++;
- }
- } else {
- foreach ($conditions as $key => $val) {
- if (is_int($key)) {
- if (is_array($val)) {
- $result[]= $this->parseConditions($val, $conjunction);
- continue;
- } elseif ($this->isConditionalClause($val)) {
- $result[]= new xPDOQueryCondition(array('sql' => $val, 'binding' => null, 'conjunction' => $conjunction));
- continue;
- } else {
- $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error parsing condition with key {$key}: " . print_r($val, true));
- continue;
- }
- } elseif (is_scalar($val) || is_array($val) || $val === null) {
- $alias= $command == 'SELECT' ? $this->_class : trim($this->xpdo->getTableName($this->_class, false), $this->xpdo->_escapeCharOpen . $this->xpdo->_escapeCharClose);
- $operator= '=';
- $conj = $conjunction;
- $key_operator= explode(':', $key);
- if ($key_operator && count($key_operator) === 2) {
- $key= $key_operator[0];
- $operator= $key_operator[1];
- }
- elseif ($key_operator && count($key_operator) === 3) {
- $conj= $key_operator[0];
- $key= $key_operator[1];
- $operator= $key_operator[2];
- }
- if (strpos($key, '.') !== false) {
- $key_parts= explode('.', $key);
- $alias= trim($key_parts[0], " {$this->xpdo->_escapeCharOpen}{$this->xpdo->_escapeCharClose}");
- $key= $key_parts[1];
- }
- if (!array_key_exists($key, $fieldMeta)) {
- if (array_key_exists($key, $fieldAliases)) {
- $key= $fieldAliases[$key];
- } elseif ($this->isConditionalClause($key)) {
- continue;
- }
- }
- if (!empty($key)) {
- if ($val === null) {
- $type= PDO::PARAM_NULL;
- if (!in_array($operator, array('IS', 'IS NOT'))) {
- $operator= $operator === '!=' ? 'IS NOT' : 'IS';
- }
- }
- elseif (isset($fieldMeta[$key]) && !in_array($fieldMeta[$key]['phptype'], $this->_quotable)) {
- $type= PDO::PARAM_INT;
- }
- else {
- $type= PDO::PARAM_STR;
- }
- if (in_array(strtoupper($operator), array('IN', 'NOT IN')) && is_array($val)) {
- $vals = array();
- foreach ($val as $v) {
- if ($v === null) {
- $vals[] = null;
- } else {
- switch ($type) {
- case PDO::PARAM_INT:
- $vals[] = (integer) $v;
- break;
- case PDO::PARAM_STR:
- $vals[] = $this->xpdo->quote($v);
- break;
- default:
- $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error parsing {$operator} condition with key {$key}: " . print_r($v, true));
- break;
- }
- }
- }
- if (empty($vals)) {
- $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Encountered empty {$operator} condition with key {$key}");
- }
- $val = "(" . implode(',', $vals) . ")";
- $sql = "{$this->xpdo->escape($alias)}.{$this->xpdo->escape($key)} {$operator} {$val}";
- $result[]= new xPDOQueryCondition(array('sql' => $sql, 'binding' => null, 'conjunction' => $conj));
- continue;
- }
- $field= array ();
- $field['sql']= $this->xpdo->escape($alias) . '.' . $this->xpdo->escape($key) . ' ' . $operator . ' ?';
- $field['binding']= array (
- 'value' => $val,
- 'type' => $type,
- 'length' => 0
- );
- $field['conjunction']= $conj;
- $result[]= new xPDOQueryCondition($field);
- } else {
- throw new xPDOException("Invalid query expression");
- }
- }
- }
- }
- }
- elseif ($this->isConditionalClause($conditions)) {
- $result= new xPDOQueryCondition(array(
- 'sql' => $conditions
- ,'binding' => null
- ,'conjunction' => $conjunction
- ));
- }
- elseif (($pktype == 'integer' && is_numeric($conditions)) || ($pktype == 'string' && is_string($conditions) && static::isValidClause($conditions))) {
- if ($pktype == 'integer') {
- $param_type= PDO::PARAM_INT;
- } else {
- $param_type= PDO::PARAM_STR;
- }
- $field['sql']= $this->xpdo->escape($alias) . '.' . $this->xpdo->escape($pk) . ' = ?';
- $field['binding']= array ('value' => $conditions, 'type' => $param_type, 'length' => 0);
- $field['conjunction']= $conjunction;
- $result = new xPDOQueryCondition($field);
- }
- return $result;
- }
- /**
- * Determines if a string contains a conditional operator.
- *
- * @param string $string The string to evaluate.
- *
- * @return bool True if the string is a complete conditional SQL clause.
- * @throws xPDOException If a SQL injection attempt is detected.
- */
- public function isConditionalClause($string) {
- $matched= false;
- if (is_string($string)) {
- if (!static::isValidClause($string)) {
- throw new xPDOException("SQL injection attempt detected: {$string}");
- }
- foreach ($this->_operators as $operator) {
- if (strpos(strtoupper($string), $operator) !== false) {
- $matched= true;
- break;
- }
- }
- }
- return $matched;
- }
- /**
- * Builds conditional clauses from xPDO condition expressions.
- *
- * @param array|xPDOQueryCondition $conditions An array of conditions or an xPDOQueryCondition instance.
- * @param string $conjunction Either xPDOQuery:SQL_AND or xPDOQuery::SQL_OR
- * @param boolean $isFirst Indicates if this is the first condition in an array.
- * @return string The generated SQL clause.
- */
- public function buildConditionalClause($conditions, & $conjunction = xPDOQuery::SQL_AND, $isFirst = true) {
- $clause= '';
- if (is_array($conditions)) {
- $groups= count($conditions);
- $currentGroup= 1;
- $first = true;
- $origConjunction = $conjunction;
- $groupConjunction = $conjunction;
- foreach ($conditions as $groupKey => $group) {
- $groupClause = '';
- $groupClause.= $this->buildConditionalClause($group, $groupConjunction, $first);
- if ($first) {
- $conjunction = $groupConjunction;
- }
- if (!empty($groupClause)) $clause.= $groupClause;
- $currentGroup++;
- $first = false;
- }
- $conjunction = $origConjunction;
- if ($groups > 1 && !empty($clause)) {
- $clause = " ( {$clause} ) ";
- }
- if (!$isFirst && !empty($clause)) {
- $clause = ' ' . $groupConjunction . ' ' . $clause;
- }
- } elseif (is_object($conditions) && $conditions instanceof xPDOQueryCondition) {
- if ($isFirst) {
- $conjunction = $conditions->conjunction;
- } else {
- $clause.= ' ' . $conditions->conjunction . ' ';
- }
- $clause.= $conditions->sql;
- if (!empty ($conditions->binding)) {
- $this->bindings[]= $conditions->binding;
- }
- }
- if ($this->xpdo->getDebug() === true) {
- $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Returning clause:\n{$clause}\nfrom conditions:\n" . print_r($conditions, 1));
- }
- return $clause;
- }
- /**
- * Wrap an existing xPDOCriteria into this xPDOQuery instance.
- *
- * @param xPDOCriteria $criteria
- */
- public function wrap($criteria) {
- if ($criteria instanceof xPDOQuery) {
- $this->_class= $criteria->_class;
- $this->_alias= $criteria->_alias;
- $this->graph= $criteria->graph;
- $this->query= $criteria->query;
- }
- $this->sql= $criteria->sql;
- $this->stmt= $criteria->stmt;
- $this->bindings= $criteria->bindings;
- $this->cacheFlag= $criteria->cacheFlag;
- }
- }
- /**
- * Abstracts individual query conditions used in xPDOQuery instances.
- *
- * @package xpdo
- * @subpackage om
- */
- class xPDOQueryCondition {
- /**
- * @var string The SQL string for the condition.
- */
- public $sql = '';
- /**
- * @var array An array of value/parameter bindings for the condition.
- */
- public $binding = array();
- /**
- * @var string The conjunction identifying how the condition is related to the previous condition(s).
- */
- public $conjunction = xPDOQuery::SQL_AND;
- /**
- * The constructor for creating an xPDOQueryCondition instance.
- *
- * @param array $properties An array of properties representing the condition.
- */
- public function __construct(array $properties) {
- if (isset($properties['sql'])) $this->sql = $properties['sql'];
- if (isset($properties['binding'])) $this->binding = $properties['binding'];
- if (isset($properties['conjunction'])) $this->conjunction = $properties['conjunction'];
- }
- }
|