PageRenderTime 41ms CodeModel.GetById 12ms RepoModel.GetById 1ms app.codeStats 0ms

/libs/Nette/Database/Table/Selection.php

https://github.com/premiumcombination/nts
PHP | 804 lines | 431 code | 186 blank | 187 comment | 60 complexity | 6809e359971848372d0f68ab96215322 MD5 | raw file
  1. <?php
  2. /**
  3. * This file is part of the Nette Framework (http://nette.org)
  4. *
  5. * Copyright (c) 2004, 2011 David Grudl (http://davidgrudl.com)
  6. *
  7. * For the full copyright and license information, please view
  8. * the file license.txt that was distributed with this source code.
  9. */
  10. namespace Nette\Database\Table;
  11. use Nette,
  12. PDO;
  13. /**
  14. * Filtered table representation.
  15. * Selector is based on the great library NotORM http://www.notorm.com written by Jakub Vrana.
  16. *
  17. * @author Jakub Vrana
  18. */
  19. class Selection extends Nette\Object implements \Iterator, \ArrayAccess, \Countable
  20. {
  21. /** @var Nette\Database\Connection */
  22. public $connection;
  23. /** @var string table name */
  24. public $name;
  25. /** @var string primary key field name */
  26. public $primary;
  27. /** @var array of [primary key => TableRow] readed from database */
  28. protected $rows;
  29. /** @var array of [primary key => TableRow] modifiable */
  30. protected $data;
  31. /** @var array of column to select */
  32. protected $select = array();
  33. /** @var array of where conditions */
  34. protected $where = array();
  35. /** @var array of where conditions for caching */
  36. protected $conditions = array();
  37. /** @var array of parameters passed to where conditions */
  38. protected $parameters = array();
  39. /** @var array or columns to order by */
  40. protected $order = array();
  41. /** @var int number of rows to fetch */
  42. protected $limit = NULL;
  43. /** @var int first row to fetch */
  44. protected $offset = NULL;
  45. /** @var string columns to grouping */
  46. protected $group = '';
  47. /** @var string grouping condition */
  48. protected $having = '';
  49. /** @var array of referenced TableSelection */
  50. protected $referenced = array();
  51. /** @var array of [sql => [column => [key => TableRow]]] used by GroupedTableSelection */
  52. protected $referencing = array();
  53. /** @var array of [sql => [key => TableRow]] used by GroupedTableSelection */
  54. protected $aggregation = array();
  55. /** @var array of touched columns */
  56. protected $accessed;
  57. /** @var array of earlier touched columns */
  58. protected $prevAccessed;
  59. /** @var array of primary key values */
  60. protected $keys = array();
  61. /** @var string */
  62. protected $delimitedName;
  63. /** @var string */
  64. protected $delimitedPrimary;
  65. /**
  66. * @param string
  67. * @param
  68. */
  69. public function __construct($table, Nette\Database\Connection $connection)
  70. {
  71. $this->name = $table;
  72. $this->connection = $connection;
  73. $this->primary = $this->getPrimary($table);
  74. $this->delimitedName = $connection->getSupplementalDriver()->delimite($this->name);
  75. $this->delimitedPrimary = $connection->getSupplementalDriver()->delimite($this->primary);
  76. }
  77. /**
  78. * Saves data to cache and empty result.
  79. */
  80. public function __destruct()
  81. {
  82. $cache = $this->connection->getCache();
  83. if ($cache && !$this->select && $this->rows !== NULL) {
  84. $accessed = $this->accessed;
  85. if (is_array($accessed)) {
  86. $accessed = array_filter($accessed);
  87. }
  88. $cache->save(array(__CLASS__, $this->name, $this->conditions), $accessed);
  89. }
  90. $this->rows = NULL;
  91. $this->data = NULL;
  92. }
  93. /**
  94. * Returns row specified by primary key.
  95. * @param mixed
  96. * @return ActiveRow or NULL if there is no such row
  97. */
  98. public function get($key)
  99. {
  100. // can also use array_pop($this->where) instead of clone to save memory
  101. $clone = clone $this;
  102. $clone->where($this->delimitedPrimary, $key);
  103. return $clone->fetch();
  104. }
  105. /**
  106. * Adds select clause, more calls appends to the end.
  107. * @param string for example "column, MD5(column) AS column_md5"
  108. * @return Selection provides a fluent interface
  109. */
  110. public function select($columns)
  111. {
  112. $this->__destruct();
  113. $this->select[] = $this->tryDelimite($columns);
  114. return $this;
  115. }
  116. /**
  117. * Selects by primary key.
  118. * @param mixed
  119. * @return Selection provides a fluent interface
  120. */
  121. public function find($key)
  122. {
  123. return $this->where($this->delimitedPrimary, $key);
  124. }
  125. /**
  126. * Adds where condition, more calls appends with AND.
  127. * @param string condition possibly containing ?
  128. * @param mixed
  129. * @param mixed ...
  130. * @return Selection provides a fluent interface
  131. */
  132. public function where($condition, $parameters = array())
  133. {
  134. if (is_array($condition)) { // where(array('column1' => 1, 'column2 > ?' => 2))
  135. foreach ($condition as $key => $val) {
  136. $this->where($key, $val);
  137. }
  138. return $this;
  139. }
  140. $this->__destruct();
  141. $this->conditions[] = $condition;
  142. $condition = $this->tryDelimite($condition);
  143. $args = func_num_args();
  144. if ($args !== 2 || strpbrk($condition, '?:')) { // where('column < ? OR column > ?', array(1, 2))
  145. if ($args !== 2 || !is_array($parameters)) { // where('column < ? OR column > ?', 1, 2)
  146. $parameters = func_get_args();
  147. array_shift($parameters);
  148. }
  149. $this->parameters = array_merge($this->parameters, $parameters);
  150. } elseif ($parameters === NULL) { // where('column', NULL)
  151. $condition .= ' IS NULL';
  152. } elseif ($parameters instanceof Selection) { // where('column', $db->$table())
  153. $clone = clone $parameters;
  154. if (!$clone->select) {
  155. $clone->select = array($this->getPrimary($clone->name));
  156. }
  157. if ($this->connection->getAttribute(PDO::ATTR_DRIVER_NAME) !== 'mysql') {
  158. $condition .= " IN ($clone)";
  159. } else {
  160. $in = array();
  161. foreach ($clone as $row) {
  162. $this->parameters[] = array_values(iterator_to_array($row));
  163. $in[] = (count($row) === 1 ? '?' : '(?)');
  164. }
  165. $condition .= ' IN (' . ($in ? implode(', ', $in) : 'NULL') . ')';
  166. }
  167. } elseif (!is_array($parameters)) { // where('column', 'x')
  168. $condition .= ' = ?';
  169. $this->parameters[] = $parameters;
  170. } else { // where('column', array(1, 2))
  171. if ($parameters) {
  172. $condition .= " IN (?)";
  173. $this->parameters[] = $parameters;
  174. } else {
  175. $condition .= " IN (NULL)";
  176. }
  177. }
  178. $this->where[] = $condition;
  179. return $this;
  180. }
  181. /**
  182. * Adds order clause, more calls appends to the end.
  183. * @param string for example 'column1, column2 DESC'
  184. * @return Selection provides a fluent interface
  185. */
  186. public function order($columns)
  187. {
  188. $this->rows = NULL;
  189. $this->order[] = $this->tryDelimite($columns);
  190. return $this;
  191. }
  192. /**
  193. * Sets limit clause, more calls rewrite old values.
  194. * @param int
  195. * @param int
  196. * @return Selection provides a fluent interface
  197. */
  198. public function limit($limit, $offset = NULL)
  199. {
  200. $this->rows = NULL;
  201. $this->limit = $limit;
  202. $this->offset = $offset;
  203. return $this;
  204. }
  205. /**
  206. * Sets offset using page number, more calls rewrite old values.
  207. * @param int
  208. * @param int
  209. * @return Selection provides a fluent interface
  210. */
  211. public function page($page, $itemsPerPage)
  212. {
  213. $this->rows = NULL;
  214. $this->limit = $itemsPerPage;
  215. $this->offset = ($page - 1) * $itemsPerPage;
  216. return $this;
  217. }
  218. /**
  219. * Sets group clause, more calls rewrite old values.
  220. * @param string
  221. * @param string
  222. * @return Selection provides a fluent interface
  223. */
  224. public function group($columns, $having = '')
  225. {
  226. $this->__destruct();
  227. $this->group = $columns;
  228. $this->having = $having;
  229. return $this;
  230. }
  231. /**
  232. * Executes aggregation function.
  233. * @param string
  234. * @return string
  235. */
  236. public function aggregation($function)
  237. {
  238. $join = $this->createJoins(implode(',', $this->conditions), TRUE) + $this->createJoins($function);
  239. $query = "SELECT $function FROM $this->delimitedName" . implode($join);
  240. if ($this->where) {
  241. $query .= ' WHERE (' . implode(') AND (', $this->where) . ')';
  242. }
  243. foreach ($this->query($query)->fetch() as $val) {
  244. return $val;
  245. }
  246. }
  247. /**
  248. * Counts number of rows.
  249. * @param string
  250. * @return int
  251. */
  252. public function count($column = '')
  253. {
  254. if (!$column) {
  255. $this->execute();
  256. return count($this->data);
  257. }
  258. return $this->aggregation("COUNT({$this->tryDelimite($column)})");
  259. }
  260. /**
  261. * Returns minimum value from a column.
  262. * @param string
  263. * @return int
  264. */
  265. public function min($column)
  266. {
  267. return $this->aggregation("MIN({$this->tryDelimite($column)})");
  268. }
  269. /**
  270. * Returns maximum value from a column.
  271. * @param string
  272. * @return int
  273. */
  274. public function max($column)
  275. {
  276. return $this->aggregation("MAX({$this->tryDelimite($column)})");
  277. }
  278. /**
  279. * Returns sum of values in a column.
  280. * @param string
  281. * @return int
  282. */
  283. public function sum($column)
  284. {
  285. return $this->aggregation("SUM({$this->tryDelimite($column)})");
  286. }
  287. /**
  288. * Returns SQL query.
  289. * @return string
  290. */
  291. public function getSql()
  292. {
  293. $join = $this->createJoins(implode(',', $this->conditions), TRUE)
  294. + $this->createJoins(implode(',', $this->select) . ",$this->group,$this->having," . implode(',', $this->order));
  295. $cache = $this->connection->getCache();
  296. if ($this->rows === NULL && $cache && !is_string($this->prevAccessed)) {
  297. $this->accessed = $this->prevAccessed = $cache->load(array(__CLASS__, $this->name, $this->conditions));
  298. }
  299. $prefix = $join ? "$this->delimitedName." : '';
  300. if ($this->select) {
  301. $cols = implode(', ', $this->select);
  302. } elseif ($this->prevAccessed) {
  303. $cols = $prefix . implode(', ' . $prefix, array_map(array($this->connection->getSupplementalDriver(), 'delimite'), array_keys($this->prevAccessed)));
  304. } else {
  305. $cols = $prefix . '*';
  306. }
  307. return "SELECT{$this->topString()} $cols FROM $this->delimitedName" . implode($join) . $this->whereString();
  308. }
  309. protected function createJoins($val, $inner = FALSE)
  310. {
  311. $supplementalDriver = $this->connection->getSupplementalDriver();
  312. $joins = array();
  313. preg_match_all('~\\b([a-z][\\w.]*)\\.([a-z]\\w*)(\\s+IS\\b|\\s*<=>)?~i', $val, $matches, PREG_SET_ORDER);
  314. foreach ($matches as $match) {
  315. if ($match[1] !== $this->name) { // case-sensitive
  316. foreach (explode('.', $match[1]) as $name) {
  317. $table = $this->connection->databaseReflection->getReferencedTable($name, $this->name);
  318. $column = $this->connection->databaseReflection->getReferencedColumn($name, $this->name);
  319. $primary = $this->getPrimary($table);
  320. $joins[$name] = ' ' . (!isset($joins[$name]) && $inner && !isset($match[3]) ? 'INNER' : 'LEFT')
  321. . ' JOIN ' . $supplementalDriver->delimite($table)
  322. . ($table !== $name ? ' AS ' . $supplementalDriver->delimite($name) : '')
  323. . " ON $this->delimitedName." . $supplementalDriver->delimite($column)
  324. . ' = ' . $supplementalDriver->delimite($name) . '.' . $supplementalDriver->delimite($primary);
  325. }
  326. }
  327. }
  328. return $joins;
  329. }
  330. /**
  331. * Executes built query.
  332. * @return NULL
  333. */
  334. protected function execute()
  335. {
  336. if ($this->rows !== NULL) {
  337. return;
  338. }
  339. try {
  340. $result = $this->query($this->getSql());
  341. } catch (\PDOException $exception) {
  342. if (!$this->select && $this->prevAccessed) {
  343. $this->prevAccessed = '';
  344. $this->accessed = array();
  345. $result = $this->query($this->getSql());
  346. } else {
  347. throw $exception;
  348. }
  349. }
  350. $this->rows = array();
  351. $result->setFetchMode(PDO::FETCH_ASSOC);
  352. foreach ($result as $key => $row) {
  353. $row = $result->normalizeRow($row);
  354. $this->rows[isset($row[$this->primary]) ? $row[$this->primary] : $key] = new ActiveRow($row, $this);
  355. }
  356. $this->data = $this->rows;
  357. if (isset($row[$this->primary]) && !is_string($this->accessed)) {
  358. $this->accessed[$this->primary] = TRUE;
  359. }
  360. }
  361. protected function whereString()
  362. {
  363. $return = '';
  364. $driver = $this->connection->getAttribute(PDO::ATTR_DRIVER_NAME);
  365. $where = $this->where;
  366. if ($this->limit !== NULL && $driver === 'oci') {
  367. $where[] = ($this->offset ? "rownum > $this->offset AND " : '') . 'rownum <= ' . ($this->limit + $this->offset);
  368. }
  369. if ($where) {
  370. $return .= ' WHERE (' . implode(') AND (', $where) . ')';
  371. }
  372. if ($this->group) {
  373. $return .= ' GROUP BY '. $this->tryDelimite($this->group);
  374. }
  375. if ($this->having) {
  376. $return .= ' HAVING '. $this->tryDelimite($this->having);
  377. }
  378. if ($this->order) {
  379. $return .= ' ORDER BY ' . implode(', ', $this->order);
  380. }
  381. if ($this->limit !== NULL && $driver !== 'oci' && $driver !== 'dblib') {
  382. $return .= " LIMIT $this->limit";
  383. if ($this->offset !== NULL) {
  384. $return .= " OFFSET $this->offset";
  385. }
  386. }
  387. return $return;
  388. }
  389. protected function topString()
  390. {
  391. if ($this->limit !== NULL && $this->connection->getAttribute(PDO::ATTR_DRIVER_NAME) === 'dblib') {
  392. return " TOP ($this->limit)"; //! offset is not supported
  393. }
  394. return '';
  395. }
  396. protected function tryDelimite($s)
  397. {
  398. return preg_match('#^[a-z_][a-z0-9_.]*$#i', $s) // is identifier?
  399. ? implode('.', array_map(array($this->connection->getSupplementalDriver(), 'delimite'), explode('.', $s)))
  400. : $s;
  401. }
  402. protected function query($query)
  403. {
  404. return $this->connection->queryArgs($query, $this->parameters);
  405. }
  406. public function access($key, $delete = FALSE)
  407. {
  408. if ($delete) {
  409. if (is_array($this->accessed)) {
  410. $this->accessed[$key] = FALSE;
  411. }
  412. return FALSE;
  413. }
  414. if ($key === NULL) {
  415. $this->accessed = '';
  416. } elseif (!is_string($this->accessed)) {
  417. $this->accessed[$key] = TRUE;
  418. }
  419. if (!$this->select && $this->prevAccessed && ($key === NULL || !isset($this->prevAccessed[$key]))) {
  420. $this->prevAccessed = '';
  421. $this->rows = NULL;
  422. return TRUE;
  423. }
  424. return FALSE;
  425. }
  426. /********************* manipulation ****************d*g**/
  427. /**
  428. * Inserts row in a table.
  429. * @param mixed array($column => $value)|Traversable for single row insert or TableSelection|string for INSERT ... SELECT
  430. * @return ActiveRow or FALSE in case of an error or number of affected rows for INSERT ... SELECT
  431. */
  432. public function insert($data)
  433. {
  434. if ($data instanceof Selection) {
  435. $data = $data->getSql();
  436. } elseif ($data instanceof \Traversable) {
  437. $data = iterator_to_array($data);
  438. }
  439. $return = $this->connection->query("INSERT INTO $this->delimitedName", $data);
  440. $this->rows = NULL;
  441. if (!is_array($data)) {
  442. return $return->rowCount();
  443. }
  444. if (!isset($data[$this->primary]) && ($id = $this->connection->lastInsertId())) {
  445. $data[$this->primary] = $id;
  446. }
  447. return new ActiveRow($data, $this);
  448. }
  449. /**
  450. * Updates all rows in result set.
  451. * @param array ($column => $value)
  452. * @return int number of affected rows or FALSE in case of an error
  453. */
  454. public function update($data)
  455. {
  456. if ($data instanceof \Traversable) {
  457. $data = iterator_to_array($data);
  458. } elseif (!is_array($data)) {
  459. throw new Nette\InvalidArgumentException;
  460. }
  461. if (!$data) {
  462. return 0;
  463. }
  464. // joins in UPDATE are supported only in MySQL
  465. return $this->connection->queryArgs(
  466. 'UPDATE' . $this->topString() . " $this->delimitedName SET ?" . $this->whereString(),
  467. array_merge(array($data), $this->parameters)
  468. )->rowCount();
  469. }
  470. /**
  471. * Deletes all rows in result set.
  472. * @return int number of affected rows or FALSE in case of an error
  473. */
  474. public function delete()
  475. {
  476. return $this->query(
  477. 'DELETE' . $this->topString() . " FROM $this->delimitedName" . $this->whereString()
  478. )->rowCount();
  479. }
  480. /********************* references ****************d*g**/
  481. /**
  482. * Returns referenced row.
  483. * @param string
  484. * @return ActiveRow or NULL if the row does not exist
  485. */
  486. public function getReferencedTable($name, & $column = NULL)
  487. {
  488. $column = $this->connection->databaseReflection->getReferencedColumn($name, $this->name);
  489. $referenced = & $this->referenced[$name];
  490. if ($referenced === NULL) {
  491. $keys = array();
  492. foreach ($this->rows as $row) {
  493. if ($row[$column] !== NULL) {
  494. $keys[$row[$column]] = NULL;
  495. }
  496. }
  497. if ($keys) {
  498. $table = $this->connection->databaseReflection->getReferencedTable($name, $this->name);
  499. $referenced = new Selection($table, $this->connection);
  500. $referenced->where($table . '.' . $this->getPrimary($table), array_keys($keys));
  501. } else {
  502. $referenced = array();
  503. }
  504. }
  505. return $referenced;
  506. }
  507. /**
  508. * Returns referencing rows.
  509. * @param string table name
  510. * @return GroupedSelection
  511. */
  512. public function getReferencingTable($table)
  513. {
  514. $column = $this->connection->databaseReflection->getReferencingColumn($table, $this->name);
  515. $referencing = new GroupedSelection($table, $this, $column);
  516. $referencing->where("$table.$column", array_keys((array) $this->rows)); // (array) - is NULL after insert
  517. return $referencing;
  518. }
  519. private function getPrimary($table)
  520. {
  521. return $this->connection->databaseReflection->getPrimary($table);
  522. }
  523. /********************* interface Iterator ****************d*g**/
  524. public function rewind()
  525. {
  526. $this->execute();
  527. $this->keys = array_keys($this->data);
  528. reset($this->keys);
  529. }
  530. /** @return ActiveRow */
  531. public function current()
  532. {
  533. return $this->data[current($this->keys)];
  534. }
  535. /**
  536. * @return string row ID
  537. */
  538. public function key()
  539. {
  540. return current($this->keys);
  541. }
  542. public function next()
  543. {
  544. next($this->keys);
  545. }
  546. public function valid()
  547. {
  548. return current($this->keys) !== FALSE;
  549. }
  550. /********************* interface ArrayAccess ****************d*g**/
  551. /**
  552. * Mimic row.
  553. * @param string row ID
  554. * @param ActiveRow
  555. * @return NULL
  556. */
  557. public function offsetSet($key, $value)
  558. {
  559. $this->execute();
  560. $this->data[$key] = $value;
  561. }
  562. /**
  563. * Returns specified row.
  564. * @param string row ID
  565. * @return ActiveRow or NULL if there is no such row
  566. */
  567. public function offsetGet($key)
  568. {
  569. $this->execute();
  570. return $this->data[$key];
  571. }
  572. /**
  573. * Tests if row exists.
  574. * @param string row ID
  575. * @return bool
  576. */
  577. public function offsetExists($key)
  578. {
  579. $this->execute();
  580. return isset($this->data[$key]);
  581. }
  582. /**
  583. * Removes row from result set.
  584. * @param string row ID
  585. * @return NULL
  586. */
  587. public function offsetUnset($key)
  588. {
  589. $this->execute();
  590. unset($this->data[$key]);
  591. }
  592. /**
  593. * Returns next row of result.
  594. * @return ActiveRow or FALSE if there is no row
  595. */
  596. public function fetch()
  597. {
  598. $this->execute();
  599. $return = current($this->data);
  600. next($this->data);
  601. return $return;
  602. }
  603. /**
  604. * Returns all rows as associative array.
  605. * @param string
  606. * @param string column name used for an array value or an empty string for the whole row
  607. * @return array
  608. */
  609. public function fetchPairs($key, $value = '')
  610. {
  611. $return = array();
  612. // no $clone->select = array($key, $value) to allow efficient caching with repetitive calls with different parameters
  613. foreach ($this as $row) {
  614. $return[$row[$key]] = ($value !== '' ? $row[$value] : $row);
  615. }
  616. return $return;
  617. }
  618. }