xpdoquery.class.php 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931
  1. <?php
  2. /*
  3. * Copyright 2010-2015 by MODX, LLC.
  4. *
  5. * This file is part of xPDO.
  6. *
  7. * xPDO is free software; you can redistribute it and/or modify it under the
  8. * terms of the GNU General Public License as published by the Free Software
  9. * Foundation; either version 2 of the License, or (at your option) any later
  10. * version.
  11. *
  12. * xPDO is distributed in the hope that it will be useful, but WITHOUT ANY
  13. * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  14. * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License along with
  17. * xPDO; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
  18. * Suite 330, Boston, MA 02111-1307 USA
  19. */
  20. /**
  21. * A class for constructing complex SQL statements using a model-aware API.
  22. *
  23. * @package xpdo
  24. * @subpackage om
  25. */
  26. /**
  27. * An xPDOCriteria derivative with methods for constructing complex statements.
  28. *
  29. * @abstract
  30. * @package xpdo
  31. * @subpackage om
  32. */
  33. abstract class xPDOQuery extends xPDOCriteria {
  34. const SQL_AND = 'AND';
  35. const SQL_OR = 'OR';
  36. const SQL_JOIN_CROSS = 'JOIN';
  37. const SQL_JOIN_LEFT = 'LEFT JOIN';
  38. const SQL_JOIN_RIGHT = 'RIGHT JOIN';
  39. const SQL_JOIN_NATURAL_LEFT = 'NATURAL LEFT JOIN';
  40. const SQL_JOIN_NATURAL_RIGHT = 'NATURAL RIGHT JOIN';
  41. const SQL_JOIN_STRAIGHT = 'STRAIGHT_JOIN';
  42. /**
  43. * An array of symbols and keywords indicative of SQL operators.
  44. *
  45. * @var array
  46. * @todo Refactor this to separate xPDOQuery operators from db-specific conditional statement identifiers.
  47. */
  48. protected $_operators= array (
  49. '=',
  50. '!=',
  51. '<',
  52. '<=',
  53. '>',
  54. '>=',
  55. '<=>',
  56. ' LIKE ',
  57. ' IS NULL',
  58. ' IS NOT NULL',
  59. ' BETWEEN ',
  60. ' IN ',
  61. ' IN(',
  62. ' NOT(',
  63. ' NOT (',
  64. ' NOT IN ',
  65. ' NOT IN(',
  66. ' EXISTS (',
  67. ' EXISTS(',
  68. ' NOT EXISTS (',
  69. ' NOT EXISTS(',
  70. ' COALESCE(',
  71. ' GREATEST(',
  72. ' INTERVAL(',
  73. ' LEAST(',
  74. 'MATCH(',
  75. 'MATCH (',
  76. 'MAX(',
  77. 'MIN(',
  78. 'AVG('
  79. );
  80. protected $_quotable= array ('string', 'password', 'date', 'datetime', 'timestamp', 'time', 'json', 'array');
  81. protected $_class= null;
  82. protected $_alias= null;
  83. protected $_tableClass = null;
  84. public $graph= array ();
  85. public $query= array (
  86. 'command' => 'SELECT',
  87. 'distinct' => '',
  88. 'columns' => '',
  89. 'from' => array (
  90. 'tables' => array (),
  91. 'joins' => array (),
  92. ),
  93. 'set' => array (),
  94. 'where' => array (),
  95. 'groupby' => array (),
  96. 'having' => array (),
  97. 'orderby' => array (),
  98. 'offset' => '',
  99. 'limit' => '',
  100. );
  101. /**
  102. * Make sure a clause is valid and does not contain SQL injection attempts.
  103. *
  104. * @param string $clause The string clause to validate.
  105. *
  106. * @return bool True if the clause is valid.
  107. */
  108. public static function isValidClause($clause) {
  109. $output = rtrim($clause, ' ;');
  110. $output = preg_replace("/\\\\'.*?\\\\'/", '{mask}', $output);
  111. $output = preg_replace('/\\".*?\\"/', '{mask}', $output);
  112. $output = preg_replace("/'.*?'/", '{mask}', $output);
  113. $output = preg_replace('/".*?"/', '{mask}', $output);
  114. return strpos($output, ';') === false && strpos(strtolower($output), 'union') === false;
  115. }
  116. public function __construct(& $xpdo, $class, $criteria= null) {
  117. parent :: __construct($xpdo);
  118. if ($class= $this->xpdo->loadClass($class)) {
  119. $this->_class= $class;
  120. $this->_alias= $class;
  121. $this->_tableClass = $this->xpdo->getTableClass($this->_class);
  122. $this->query['from']['tables'][0]= array (
  123. 'table' => $this->xpdo->getTableName($this->_class),
  124. 'alias' => & $this->_alias
  125. );
  126. if ($criteria !== null) {
  127. if (is_object($criteria)) {
  128. $this->wrap($criteria);
  129. }
  130. else {
  131. $this->where($criteria);
  132. }
  133. }
  134. }
  135. }
  136. public function getClass() {
  137. return $this->_class;
  138. }
  139. public function getAlias() {
  140. return $this->_alias;
  141. }
  142. public function getTableClass() {
  143. return $this->_tableClass;
  144. }
  145. /**
  146. * Set the type of SQL command you want to build.
  147. *
  148. * The default is SELECT, though it also supports DELETE and UPDATE.
  149. *
  150. * @param string $command The type of SQL statement represented by this object. Default is 'SELECT'.
  151. * @return xPDOQuery Returns the current object for convenience.
  152. */
  153. public function command($command= 'SELECT') {
  154. $command= strtoupper(trim($command));
  155. if (preg_match('/(SELECT|UPDATE|DELETE)/', $command)) {
  156. $this->query['command']= $command;
  157. if (in_array($command, array('DELETE','UPDATE'))) $this->_alias= $this->xpdo->getTableName($this->_class);
  158. }
  159. return $this;
  160. }
  161. /**
  162. * Set the DISTINCT attribute of the query.
  163. *
  164. * @param null|boolean $on Defines how to set the distinct attribute:
  165. * - null (default) indicates the distinct attribute should be toggled
  166. * - any other value is treated as a boolean, i.e. true to set DISTINCT, false to unset
  167. * @return xPDOQuery Returns the current object for convenience.
  168. */
  169. public function distinct($on = null) {
  170. if ($on === null) {
  171. if (empty($this->query['distinct']) || $this->query['distinct'] !== 'DISTINCT') {
  172. $this->query['distinct']= 'DISTINCT';
  173. } else {
  174. $this->query['distinct']= '';
  175. }
  176. } else {
  177. $this->query['distinct']= $on == true ? 'DISTINCT' : '';
  178. }
  179. return $this;
  180. }
  181. /**
  182. * Sets a SQL alias for the table represented by the main class.
  183. *
  184. * @param string $alias An alias for the main table for the SQL statement.
  185. * @return xPDOQuery Returns the current object for convenience.
  186. */
  187. public function setClassAlias($alias= '') {
  188. $this->_alias= $alias;
  189. return $this;
  190. }
  191. /**
  192. * Specify columns to return from the SQL query.
  193. *
  194. * @param string $columns Columns to return from the query.
  195. * @return xPDOQuery Returns the current object for convenience.
  196. */
  197. public function select($columns= '*') {
  198. if (!is_array($columns)) {
  199. $columns= trim($columns);
  200. if ($columns == '*' || $columns === $this->_alias . '.*' || $columns === $this->xpdo->escape($this->_alias) . '.*') {
  201. $columns= $this->xpdo->getSelectColumns($this->_class, $this->_alias, $this->_alias . '_');
  202. }
  203. $columns= explode(',', $columns);
  204. foreach ($columns as $colKey => $column) $columns[$colKey] = trim($column);
  205. }
  206. if (is_array ($columns)) {
  207. if (!is_array($this->query['columns'])) {
  208. $this->query['columns']= $columns;
  209. } else {
  210. $this->query['columns']= array_merge($this->query['columns'], $columns);
  211. }
  212. }
  213. return $this;
  214. }
  215. /**
  216. * Specify the SET clause(s) for a SQL UPDATE query.
  217. *
  218. * @param array $values An associative array of fields and the values to set them to.
  219. * @return xPDOQuery Returns a reference to the current instance for convenience.
  220. */
  221. public function set(array $values) {
  222. $fieldMeta= $this->xpdo->getFieldMeta($this->_class);
  223. $fieldAliases= $this->xpdo->getFieldAliases($this->_class);
  224. foreach ($values as $key => $value) {
  225. $type= null;
  226. if (!array_key_exists($key, $fieldMeta)) {
  227. if (array_key_exists($key, $fieldAliases)) {
  228. $key = $fieldAliases[$key];
  229. } else {
  230. continue;
  231. }
  232. }
  233. if (array_key_exists($key, $fieldMeta)) {
  234. if ($value === null) {
  235. $type= PDO::PARAM_NULL;
  236. }
  237. elseif (!in_array($fieldMeta[$key]['phptype'], $this->_quotable)) {
  238. $type= PDO::PARAM_INT;
  239. }
  240. elseif (strpos($value, '(') === false && !$this->isConditionalClause($value)) {
  241. $type= PDO::PARAM_STR;
  242. }
  243. $this->query['set'][$key]= array('value' => $value, 'type' => $type);
  244. }
  245. }
  246. return $this;
  247. }
  248. /**
  249. * Join a table represented by the specified class.
  250. *
  251. * @param string $class The classname (or relation alias for aggregates and
  252. * composites) of representing the table to be joined.
  253. * @param string $alias An optional alias to represent the joined table in
  254. * the constructed query.
  255. * @param string $type The type of join to perform. See the xPDOQuery::SQL_JOIN
  256. * constants.
  257. * @param mixed $conditions Conditions of the join specified in any xPDO
  258. * compatible criteria object or expression.
  259. * @param string $conjunction A conjunction to be applied to the condition
  260. * or conditions supplied.
  261. * @param array $binding Optional bindings to accompany the conditions.
  262. * @param int $condGroup An optional identifier for adding the conditions
  263. * to a specific set of conjoined expressions.
  264. * @return xPDOQuery Returns the current object for convenience.
  265. */
  266. public function join($class, $alias= '', $type= xPDOQuery::SQL_JOIN_CROSS, $conditions= array (), $conjunction= xPDOQuery::SQL_AND, $binding= null, $condGroup= 0) {
  267. if ($this->xpdo->loadClass($class)) {
  268. $alias= $alias ? $alias : $class;
  269. $target= & $this->query['from']['joins'];
  270. $targetIdx= count($target);
  271. $target[$targetIdx]= array (
  272. 'table' => $this->xpdo->getTableName($class),
  273. 'class' => $class,
  274. 'alias' => $alias,
  275. 'type' => $type,
  276. 'conditions' => array ()
  277. );
  278. if (empty ($conditions)) {
  279. $fkMeta= $this->xpdo->getFKDefinition($this->_class, $alias);
  280. if ($fkMeta) {
  281. $parentAlias= isset ($this->_alias) ? $this->_alias : $this->_class;
  282. $local= $fkMeta['local'];
  283. $foreign= $fkMeta['foreign'];
  284. $conditions= $this->xpdo->escape($parentAlias) . '.' . $this->xpdo->escape($local) . ' = ' . $this->xpdo->escape($alias) . '.' . $this->xpdo->escape($foreign);
  285. if (isset($fkMeta['criteria']['local'])) {
  286. $localCriteria = array();
  287. if (is_array($fkMeta['criteria']['local'])) {
  288. foreach ($fkMeta['criteria']['local'] as $critKey => $critVal) {
  289. if (is_numeric($critKey)) {
  290. $localCriteria[] = $critVal;
  291. } else {
  292. $localCriteria["{$this->_class}.{$critKey}"] = $critVal;
  293. }
  294. }
  295. }
  296. if (!empty($localCriteria)) {
  297. $conditions = array($localCriteria, $conditions);
  298. }
  299. $foreignCriteria = array();
  300. if (is_array($fkMeta['criteria']['foreign'])) {
  301. foreach ($fkMeta['criteria']['foreign'] as $critKey => $critVal) {
  302. if (is_numeric($critKey)) {
  303. $foreignCriteria[] = $critVal;
  304. } else {
  305. $foreignCriteria["{$parentAlias}.{$critKey}"] = $critVal;
  306. }
  307. }
  308. }
  309. if (!empty($foreignCriteria)) {
  310. $conditions = array($foreignCriteria, $conditions);
  311. }
  312. }
  313. }
  314. }
  315. $this->condition($target[$targetIdx]['conditions'], $conditions, $conjunction, $binding, $condGroup);
  316. }
  317. return $this;
  318. }
  319. public function innerJoin($class, $alias= '', $conditions= array (), $conjunction= xPDOQuery::SQL_AND, $binding= null, $condGroup= 0) {
  320. return $this->join($class, $alias, xPDOQuery::SQL_JOIN_CROSS, $conditions, $conjunction, $binding, $condGroup);
  321. }
  322. public function leftJoin($class, $alias= '', $conditions= array (), $conjunction= xPDOQuery::SQL_AND, $binding= null, $condGroup= 0) {
  323. return $this->join($class, $alias, xPDOQuery::SQL_JOIN_LEFT, $conditions, $conjunction, $binding, $condGroup);
  324. }
  325. public function rightJoin($class, $alias= '', $conditions= array (), $conjunction= xPDOQuery::SQL_AND, $binding= null, $condGroup= 0) {
  326. return $this->join($class, $alias, xPDOQuery::SQL_JOIN_RIGHT, $conditions, $conjunction, $binding, $condGroup);
  327. }
  328. /**
  329. * Add a FROM clause to the query.
  330. *
  331. * @param string $class The class representing the table to add.
  332. * @param string $alias An optional alias for the class.
  333. * @return xPDOQuery Returns the instance.
  334. */
  335. public function from($class, $alias= '') {
  336. if ($class= $this->xpdo->loadClass($class)) {
  337. $alias= $alias ? $alias : $class;
  338. $this->query['from']['tables'][]= array (
  339. 'table' => $this->xpdo->getTableName($class),
  340. 'alias' => $alias
  341. );
  342. }
  343. return $this;
  344. }
  345. /**
  346. * Add a condition to the query.
  347. *
  348. * @param string $target The target clause for the condition.
  349. * @param mixed $conditions A valid xPDO criteria expression.
  350. * @param string $conjunction The conjunction to use when appending this condition, i.e., AND or OR.
  351. * @param mixed $binding A value or PDO binding representation of a value for the condition.
  352. * @param integer $condGroup A numeric identifier for associating conditions into groups.
  353. * @return xPDOQuery Returns the instance.
  354. */
  355. public function condition(& $target, $conditions= '1', $conjunction= xPDOQuery::SQL_AND, $binding= null, $condGroup= 0) {
  356. $condGroup= intval($condGroup);
  357. if (!isset ($target[$condGroup])) $target[$condGroup]= array ();
  358. try {
  359. $target[$condGroup][] = $this->parseConditions($conditions, $conjunction);
  360. } catch (xPDOException $e) {
  361. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, $e->getMessage());
  362. $this->where("2=1");
  363. }
  364. return $this;
  365. }
  366. /**
  367. * Add a WHERE condition to the query.
  368. *
  369. * @param mixed $conditions A valid xPDO criteria expression.
  370. * @param string $conjunction The conjunction to use when appending this condition, i.e., AND or OR.
  371. * @param mixed $binding A value or PDO binding representation of a value for the condition.
  372. * @param integer $condGroup A numeric identifier for associating conditions into groups.
  373. * @return xPDOQuery Returns the instance.
  374. */
  375. public function where($conditions= '', $conjunction= xPDOQuery::SQL_AND, $binding= null, $condGroup= 0) {
  376. $this->condition($this->query['where'], $conditions, $conjunction, $binding, $condGroup);
  377. return $this;
  378. }
  379. public function andCondition($conditions, $binding= null, $group= 0) {
  380. $this->where($conditions, xPDOQuery::SQL_AND, $binding, $group);
  381. return $this;
  382. }
  383. public function orCondition($conditions, $binding= null, $group= 0) {
  384. $this->where($conditions, xPDOQuery::SQL_OR, $binding, $group);
  385. return $this;
  386. }
  387. /**
  388. * Add an ORDER BY clause to the query.
  389. *
  390. * @param string $column Column identifier to sort by.
  391. * @param string $direction The direction to sort by, ASC or DESC.
  392. * @return xPDOQuery Returns the instance.
  393. */
  394. public function sortby($column, $direction= 'ASC') {
  395. /* The direction can only be ASC or DESC; anything else is bogus */
  396. if (!in_array(strtoupper($direction), array('ASC', 'DESC', 'ASCENDING', 'DESCENDING'), true)) {
  397. $direction = '';
  398. }
  399. if (!static::isValidClause($column)) {
  400. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'SQL injection attempt detected in sortby column; clause rejected');
  401. } elseif (!empty($column)) {
  402. $this->query['sortby'][] = array('column' => $column, 'direction' => $direction);
  403. }
  404. return $this;
  405. }
  406. /**
  407. * Add an GROUP BY clause to the query.
  408. *
  409. * @param string $column Column identifier to group by.
  410. * @param string $direction The direction to sort by, ASC or DESC.
  411. * @return xPDOQuery Returns the instance.
  412. */
  413. public function groupby($column, $direction= '') {
  414. $this->query['groupby'][]= array ('column' => $column, 'direction' => $direction);
  415. return $this;
  416. }
  417. public function having($conditions) {
  418. try {
  419. $this->query['having'][] = $this->parseConditions((array)$conditions);
  420. } catch (xPDOException $e) {
  421. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, $e->getMessage());
  422. $this->where("2=1");
  423. }
  424. return $this;
  425. }
  426. /**
  427. * Add a LIMIT/OFFSET clause to the query.
  428. *
  429. * @param integer $limit The number of records to return.
  430. * @param integer $offset The location in the result set to start from.
  431. * @return xPDOQuery Returns the instance.
  432. */
  433. public function limit($limit, $offset= 0) {
  434. $this->query['limit']= (int)$limit;
  435. $this->query['offset']= (int)$offset;
  436. return $this;
  437. }
  438. /**
  439. * Bind an object graph to the query.
  440. *
  441. * @param mixed $graph An array or JSON graph of related objects.
  442. * @return xPDOQuery Returns the instance.
  443. */
  444. public function bindGraph($graph) {
  445. if (is_string($graph)) {
  446. $graph= $this->xpdo->fromJSON($graph);
  447. }
  448. if (is_array ($graph)) {
  449. if ($this->graph !== $graph) {
  450. $this->graph= $graph;
  451. $this->select($this->xpdo->getSelectColumns($this->_class, $this->_alias, $this->_alias . '_'));
  452. foreach ($this->graph as $relationAlias => $subRelations) {
  453. $this->bindGraphNode($this->_class, $this->_alias, $relationAlias, $subRelations);
  454. }
  455. if ($pk= $this->xpdo->getPK($this->_class)) {
  456. if (is_array ($pk)) {
  457. foreach ($pk as $key) {
  458. $this->sortby($this->xpdo->escape($this->_alias) . '.' . $this->xpdo->escape($key), 'ASC');
  459. }
  460. } else {
  461. $this->sortby($this->xpdo->escape($this->_alias) . '.' . $this->xpdo->escape($pk), 'ASC');
  462. }
  463. }
  464. }
  465. }
  466. return $this;
  467. }
  468. /**
  469. * Bind the node of an object graph to the query.
  470. *
  471. * @param string $parentClass The class representing the relation parent.
  472. * @param string $parentAlias The alias the class is assuming.
  473. * @param string $classAlias The class representing the related graph node.
  474. * @param array $relations Child relations of the current graph node.
  475. */
  476. public function bindGraphNode($parentClass, $parentAlias, $classAlias, $relations) {
  477. if ($fkMeta= $this->xpdo->getFKDefinition($parentClass, $classAlias)) {
  478. $class= $fkMeta['class'];
  479. $local= $fkMeta['local'];
  480. $foreign= $fkMeta['foreign'];
  481. $this->select($this->xpdo->getSelectColumns($class, $classAlias, $classAlias . '_'));
  482. $expression= $this->xpdo->escape($parentAlias) . '.' . $this->xpdo->escape($local) . ' = ' . $this->xpdo->escape($classAlias) . '.' . $this->xpdo->escape($foreign);
  483. if (isset($fkMeta['criteria']['local'])) {
  484. $localCriteria = array();
  485. if (is_array($fkMeta['criteria']['local'])) {
  486. foreach ($fkMeta['criteria']['local'] as $critKey => $critVal) {
  487. if (is_numeric($critKey)) {
  488. $localCriteria[] = $critVal;
  489. } else {
  490. $localCriteria["{$classAlias}.{$critKey}"] = $critVal;
  491. }
  492. }
  493. }
  494. if (!empty($localCriteria)) {
  495. $expression = array($localCriteria, $expression);
  496. }
  497. $foreignCriteria = array();
  498. if (is_array($fkMeta['criteria']['foreign'])) {
  499. foreach ($fkMeta['criteria']['foreign'] as $critKey => $critVal) {
  500. if (is_numeric($critKey)) {
  501. $foreignCriteria[] = $critVal;
  502. } else {
  503. $foreignCriteria["{$parentAlias}.{$critKey}"] = $critVal;
  504. }
  505. }
  506. }
  507. if (!empty($foreignCriteria)) {
  508. $expression = array($foreignCriteria, $expression);
  509. }
  510. }
  511. $this->leftJoin($class, $classAlias, $expression);
  512. if (!empty ($relations)) {
  513. foreach ($relations as $relationAlias => $subRelations) {
  514. $this->bindGraphNode($class, $classAlias, $relationAlias, $subRelations);
  515. }
  516. }
  517. }
  518. }
  519. /**
  520. * Hydrates a graph of related objects from a single result set.
  521. *
  522. * @param array|PDOStatement $rows A collection of result set rows or an
  523. * executed PDOStatement to fetch rows from to hydrating the graph.
  524. * @param bool $cacheFlag Indicates if the objects should be cached and
  525. * optionally, by specifying an integer value, for how many seconds.
  526. * @return array A collection of objects with all related objects from the
  527. * graph pre-populated.
  528. */
  529. public function hydrateGraph($rows, $cacheFlag = true) {
  530. $instances= array ();
  531. $collectionCaching = $this->xpdo->getOption(xPDO::OPT_CACHE_DB_COLLECTIONS, array(), 1);
  532. if (is_object($rows)) {
  533. if ($cacheFlag && $this->xpdo->_cacheEnabled && $collectionCaching > 0) {
  534. $cacheRows = array();
  535. }
  536. while ($row = $rows->fetch(PDO::FETCH_ASSOC)) {
  537. $this->hydrateGraphParent($instances, $row);
  538. if ($cacheFlag && $this->xpdo->_cacheEnabled && $collectionCaching > 0) {
  539. $cacheRows[]= $row;
  540. }
  541. }
  542. if ($cacheFlag && $this->xpdo->_cacheEnabled && $collectionCaching > 0) {
  543. $this->xpdo->toCache($this, $cacheRows, $cacheFlag);
  544. }
  545. } elseif (is_array($rows)) {
  546. foreach ($rows as $row) {
  547. $this->hydrateGraphParent($instances, $row);
  548. }
  549. }
  550. return $instances;
  551. }
  552. public function hydrateGraphParent(& $instances, $row) {
  553. $hydrated = false;
  554. $instance = $this->xpdo->call($this->getClass(), '_loadInstance', array(& $this->xpdo, $this->getClass(), $this->getAlias(), $row));
  555. if (is_object($instance)) {
  556. $pk= $instance->getPrimaryKey();
  557. if (is_array($pk)) $pk= implode('-', $pk);
  558. if (isset ($instances[$pk])) {
  559. $instance= & $instances[$pk];
  560. }
  561. foreach ($this->graph as $relationAlias => $subRelations) {
  562. $this->hydrateGraphNode($row, $instance, $relationAlias, $subRelations);
  563. }
  564. $instances[$pk]= $instance;
  565. $hydrated = true;
  566. }
  567. return $hydrated;
  568. }
  569. /**
  570. * Hydrates a node of the object graph.
  571. *
  572. * @param array $row The result set representing the current node.
  573. * @param xPDOObject $instance The xPDOObject instance to be hydrated from the node.
  574. * @param string $alias The alias identifying the object in the parent relationship.
  575. * @param array $relations Child relations of the current node.
  576. */
  577. public function hydrateGraphNode(& $row, & $instance, $alias, $relations) {
  578. $relObj= null;
  579. if ($relationMeta= $instance->getFKDefinition($alias)) {
  580. if ($row[$alias.'_'.$relationMeta['foreign']] != null) {
  581. $relObj = $this->xpdo->call($relationMeta['class'], '_loadInstance', array(& $this->xpdo, $relationMeta['class'], $alias, $row));
  582. if ($relObj) {
  583. if (strtolower($relationMeta['cardinality']) == 'many') {
  584. $instance->addMany($relObj, $alias);
  585. } else {
  586. $instance->addOne($relObj, $alias);
  587. }
  588. }
  589. }
  590. }
  591. if (!empty($relations) && is_object($relObj)) {
  592. foreach ($relations as $relationAlias => $subRelations) {
  593. if (is_array($subRelations) && !empty($subRelations)) {
  594. foreach ($subRelations as $subRelation) {
  595. $this->hydrateGraphNode($row, $relObj, $relationAlias, $subRelation);
  596. }
  597. } else {
  598. $this->hydrateGraphNode($row, $relObj, $relationAlias, null);
  599. }
  600. }
  601. }
  602. }
  603. /**
  604. * Constructs the SQL query from the xPDOQuery definition.
  605. *
  606. * @return boolean Returns true if a SQL statement was successfully constructed.
  607. */
  608. abstract public function construct();
  609. /**
  610. * Prepares the xPDOQuery for execution.
  611. *
  612. * @return PDOStatement The PDOStatement representing the prepared query.
  613. */
  614. public function prepare($bindings= array (), $byValue= true, $cacheFlag= null) {
  615. $this->stmt= null;
  616. if ($this->construct() && $this->stmt= $this->xpdo->prepare($this->sql)) {
  617. $this->bind($bindings, $byValue, $cacheFlag);
  618. } else {
  619. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Could not construct or prepare query because it is invalid or could not connect: ' . $this->sql);
  620. }
  621. return $this->stmt;
  622. }
  623. /**
  624. * Parses an xPDO condition expression into one or more xPDOQueryConditions.
  625. *
  626. * @param mixed $conditions A valid xPDO condition expression.
  627. * @param string $conjunction The optional conjunction for the condition( s ).
  628. * @return array||xPDOQueryCondition An xPDOQueryCondition or array of xPDOQueryConditions.
  629. */
  630. public function parseConditions($conditions, $conjunction = xPDOQuery::SQL_AND) {
  631. $result= array ();
  632. $pk= $this->xpdo->getPK($this->_class);
  633. $pktype= $this->xpdo->getPKType($this->_class);
  634. $fieldMeta= $this->xpdo->getFieldMeta($this->_class, true);
  635. $fieldAliases= $this->xpdo->getFieldAliases($this->_class);
  636. $command= strtoupper($this->query['command']);
  637. $alias= $command == 'SELECT' ? $this->_class : $this->xpdo->getTableName($this->_class, false);
  638. $alias= trim($alias, $this->xpdo->_escapeCharOpen . $this->xpdo->_escapeCharClose);
  639. if (is_array($conditions)) {
  640. if (isset($conditions[0]) && is_scalar($conditions[0]) && !$this->isConditionalClause($conditions[0]) && is_array($pk) && count($conditions) == count($pk)) {
  641. $iteration= 0;
  642. foreach ($pk as $k) {
  643. if (!isset ($conditions[$iteration])) {
  644. $conditions[$iteration]= null;
  645. }
  646. $isString= in_array($fieldMeta[$k]['phptype'], $this->_quotable);
  647. $field= array();
  648. $field['sql']= $this->xpdo->escape($alias) . '.' . $this->xpdo->escape($k) . " = ?";
  649. $field['binding']= array (
  650. 'value' => $conditions[$iteration],
  651. 'type' => $isString ? PDO::PARAM_STR : PDO::PARAM_INT,
  652. 'length' => 0
  653. );
  654. $field['conjunction']= $conjunction;
  655. $result[$iteration]= new xPDOQueryCondition($field);
  656. $iteration++;
  657. }
  658. } else {
  659. foreach ($conditions as $key => $val) {
  660. if (is_int($key)) {
  661. if (is_array($val)) {
  662. $result[]= $this->parseConditions($val, $conjunction);
  663. continue;
  664. } elseif ($this->isConditionalClause($val)) {
  665. $result[]= new xPDOQueryCondition(array('sql' => $val, 'binding' => null, 'conjunction' => $conjunction));
  666. continue;
  667. } else {
  668. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error parsing condition with key {$key}: " . print_r($val, true));
  669. continue;
  670. }
  671. } elseif (is_scalar($val) || is_array($val) || $val === null) {
  672. $alias= $command == 'SELECT' ? $this->_class : trim($this->xpdo->getTableName($this->_class, false), $this->xpdo->_escapeCharOpen . $this->xpdo->_escapeCharClose);
  673. $operator= '=';
  674. $conj = $conjunction;
  675. $key_operator= explode(':', $key);
  676. if ($key_operator && count($key_operator) === 2) {
  677. $key= $key_operator[0];
  678. $operator= $key_operator[1];
  679. }
  680. elseif ($key_operator && count($key_operator) === 3) {
  681. $conj= $key_operator[0];
  682. $key= $key_operator[1];
  683. $operator= $key_operator[2];
  684. }
  685. if (strpos($key, '.') !== false) {
  686. $key_parts= explode('.', $key);
  687. $alias= trim($key_parts[0], " {$this->xpdo->_escapeCharOpen}{$this->xpdo->_escapeCharClose}");
  688. $key= $key_parts[1];
  689. }
  690. if (!array_key_exists($key, $fieldMeta)) {
  691. if (array_key_exists($key, $fieldAliases)) {
  692. $key= $fieldAliases[$key];
  693. } elseif ($this->isConditionalClause($key)) {
  694. continue;
  695. }
  696. }
  697. if (!empty($key)) {
  698. if ($val === null) {
  699. $type= PDO::PARAM_NULL;
  700. if (!in_array($operator, array('IS', 'IS NOT'))) {
  701. $operator= $operator === '!=' ? 'IS NOT' : 'IS';
  702. }
  703. }
  704. elseif (isset($fieldMeta[$key]) && !in_array($fieldMeta[$key]['phptype'], $this->_quotable)) {
  705. $type= PDO::PARAM_INT;
  706. }
  707. else {
  708. $type= PDO::PARAM_STR;
  709. }
  710. if (in_array(strtoupper($operator), array('IN', 'NOT IN')) && is_array($val)) {
  711. $vals = array();
  712. foreach ($val as $v) {
  713. if ($v === null) {
  714. $vals[] = null;
  715. } else {
  716. switch ($type) {
  717. case PDO::PARAM_INT:
  718. $vals[] = (integer) $v;
  719. break;
  720. case PDO::PARAM_STR:
  721. $vals[] = $this->xpdo->quote($v);
  722. break;
  723. default:
  724. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error parsing {$operator} condition with key {$key}: " . print_r($v, true));
  725. break;
  726. }
  727. }
  728. }
  729. if (empty($vals)) {
  730. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Encountered empty {$operator} condition with key {$key}");
  731. }
  732. $val = "(" . implode(',', $vals) . ")";
  733. $sql = "{$this->xpdo->escape($alias)}.{$this->xpdo->escape($key)} {$operator} {$val}";
  734. $result[]= new xPDOQueryCondition(array('sql' => $sql, 'binding' => null, 'conjunction' => $conj));
  735. continue;
  736. }
  737. $field= array ();
  738. $field['sql']= $this->xpdo->escape($alias) . '.' . $this->xpdo->escape($key) . ' ' . $operator . ' ?';
  739. $field['binding']= array (
  740. 'value' => $val,
  741. 'type' => $type,
  742. 'length' => 0
  743. );
  744. $field['conjunction']= $conj;
  745. $result[]= new xPDOQueryCondition($field);
  746. } else {
  747. throw new xPDOException("Invalid query expression");
  748. }
  749. }
  750. }
  751. }
  752. }
  753. elseif ($this->isConditionalClause($conditions)) {
  754. $result= new xPDOQueryCondition(array(
  755. 'sql' => $conditions
  756. ,'binding' => null
  757. ,'conjunction' => $conjunction
  758. ));
  759. }
  760. elseif (($pktype == 'integer' && is_numeric($conditions)) || ($pktype == 'string' && is_string($conditions) && static::isValidClause($conditions))) {
  761. if ($pktype == 'integer') {
  762. $param_type= PDO::PARAM_INT;
  763. } else {
  764. $param_type= PDO::PARAM_STR;
  765. }
  766. $field['sql']= $this->xpdo->escape($alias) . '.' . $this->xpdo->escape($pk) . ' = ?';
  767. $field['binding']= array ('value' => $conditions, 'type' => $param_type, 'length' => 0);
  768. $field['conjunction']= $conjunction;
  769. $result = new xPDOQueryCondition($field);
  770. }
  771. return $result;
  772. }
  773. /**
  774. * Determines if a string contains a conditional operator.
  775. *
  776. * @param string $string The string to evaluate.
  777. *
  778. * @return bool True if the string is a complete conditional SQL clause.
  779. * @throws xPDOException If a SQL injection attempt is detected.
  780. */
  781. public function isConditionalClause($string) {
  782. $matched= false;
  783. if (is_string($string)) {
  784. if (!static::isValidClause($string)) {
  785. throw new xPDOException("SQL injection attempt detected: {$string}");
  786. }
  787. foreach ($this->_operators as $operator) {
  788. if (strpos(strtoupper($string), $operator) !== false) {
  789. $matched= true;
  790. break;
  791. }
  792. }
  793. }
  794. return $matched;
  795. }
  796. /**
  797. * Builds conditional clauses from xPDO condition expressions.
  798. *
  799. * @param array|xPDOQueryCondition $conditions An array of conditions or an xPDOQueryCondition instance.
  800. * @param string $conjunction Either xPDOQuery:SQL_AND or xPDOQuery::SQL_OR
  801. * @param boolean $isFirst Indicates if this is the first condition in an array.
  802. * @return string The generated SQL clause.
  803. */
  804. public function buildConditionalClause($conditions, & $conjunction = xPDOQuery::SQL_AND, $isFirst = true) {
  805. $clause= '';
  806. if (is_array($conditions)) {
  807. $groups= count($conditions);
  808. $currentGroup= 1;
  809. $first = true;
  810. $origConjunction = $conjunction;
  811. $groupConjunction = $conjunction;
  812. foreach ($conditions as $groupKey => $group) {
  813. $groupClause = '';
  814. $groupClause.= $this->buildConditionalClause($group, $groupConjunction, $first);
  815. if ($first) {
  816. $conjunction = $groupConjunction;
  817. }
  818. if (!empty($groupClause)) $clause.= $groupClause;
  819. $currentGroup++;
  820. $first = false;
  821. }
  822. $conjunction = $origConjunction;
  823. if ($groups > 1 && !empty($clause)) {
  824. $clause = " ( {$clause} ) ";
  825. }
  826. if (!$isFirst && !empty($clause)) {
  827. $clause = ' ' . $groupConjunction . ' ' . $clause;
  828. }
  829. } elseif (is_object($conditions) && $conditions instanceof xPDOQueryCondition) {
  830. if ($isFirst) {
  831. $conjunction = $conditions->conjunction;
  832. } else {
  833. $clause.= ' ' . $conditions->conjunction . ' ';
  834. }
  835. $clause.= $conditions->sql;
  836. if (!empty ($conditions->binding)) {
  837. $this->bindings[]= $conditions->binding;
  838. }
  839. }
  840. if ($this->xpdo->getDebug() === true) {
  841. $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Returning clause:\n{$clause}\nfrom conditions:\n" . print_r($conditions, 1));
  842. }
  843. return $clause;
  844. }
  845. /**
  846. * Wrap an existing xPDOCriteria into this xPDOQuery instance.
  847. *
  848. * @param xPDOCriteria $criteria
  849. */
  850. public function wrap($criteria) {
  851. if ($criteria instanceof xPDOQuery) {
  852. $this->_class= $criteria->_class;
  853. $this->_alias= $criteria->_alias;
  854. $this->graph= $criteria->graph;
  855. $this->query= $criteria->query;
  856. }
  857. $this->sql= $criteria->sql;
  858. $this->stmt= $criteria->stmt;
  859. $this->bindings= $criteria->bindings;
  860. $this->cacheFlag= $criteria->cacheFlag;
  861. }
  862. }
  863. /**
  864. * Abstracts individual query conditions used in xPDOQuery instances.
  865. *
  866. * @package xpdo
  867. * @subpackage om
  868. */
  869. class xPDOQueryCondition {
  870. /**
  871. * @var string The SQL string for the condition.
  872. */
  873. public $sql = '';
  874. /**
  875. * @var array An array of value/parameter bindings for the condition.
  876. */
  877. public $binding = array();
  878. /**
  879. * @var string The conjunction identifying how the condition is related to the previous condition(s).
  880. */
  881. public $conjunction = xPDOQuery::SQL_AND;
  882. /**
  883. * The constructor for creating an xPDOQueryCondition instance.
  884. *
  885. * @param array $properties An array of properties representing the condition.
  886. */
  887. public function __construct(array $properties) {
  888. if (isset($properties['sql'])) $this->sql = $properties['sql'];
  889. if (isset($properties['binding'])) $this->binding = $properties['binding'];
  890. if (isset($properties['conjunction'])) $this->conjunction = $properties['conjunction'];
  891. }
  892. }