1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Database\Table;
9:
10: use Nette;
11: use Nette\Database\ISupplementalDriver;
12:
13:
14: 15: 16: 17: 18: 19: 20: 21: 22:
23: class Selection extends Nette\Object implements \Iterator, IRowContainer, \ArrayAccess, \Countable
24: {
25:
26: protected $connection;
27:
28:
29: protected $reflection;
30:
31:
32: protected $cache;
33:
34:
35: protected $sqlBuilder;
36:
37:
38: protected $name;
39:
40:
41: protected $primary;
42:
43:
44: protected $primarySequence = FALSE;
45:
46:
47: protected $rows;
48:
49:
50: protected $data;
51:
52:
53: protected $dataRefreshed = FALSE;
54:
55:
56: protected $globalRefCache;
57:
58:
59: protected $refCache;
60:
61:
62: protected $generalCacheKey;
63:
64:
65: protected $specificCacheKey;
66:
67:
68: protected $aggregation = array();
69:
70:
71: protected $accessedColumns;
72:
73:
74: protected $previousAccessedColumns;
75:
76:
77: protected $observeCache = FALSE;
78:
79:
80: protected $keys = array();
81:
82:
83: 84: 85: 86: 87:
88: public function __construct(Nette\Database\Connection $connection, $table, Nette\Database\IReflection $reflection, Nette\Caching\IStorage $cacheStorage = NULL)
89: {
90: $this->name = $table;
91: $this->connection = $connection;
92: $this->reflection = $reflection;
93: $this->cache = $cacheStorage ? new Nette\Caching\Cache($cacheStorage, 'Nette.Database.' . md5($connection->getDsn())) : NULL;
94: $this->primary = $reflection->getPrimary($table);
95: $this->sqlBuilder = new SqlBuilder($table, $connection, $reflection);
96: $this->refCache = & $this->getRefTable($refPath)->globalRefCache[$refPath];
97: }
98:
99:
100: public function __destruct()
101: {
102: $this->saveCacheState();
103: }
104:
105:
106: public function __clone()
107: {
108: $this->sqlBuilder = clone $this->sqlBuilder;
109: }
110:
111:
112: 113: 114:
115: public function getConnection()
116: {
117: return $this->connection;
118: }
119:
120:
121: 122: 123:
124: public function getDatabaseReflection()
125: {
126: return $this->reflection;
127: }
128:
129:
130: 131: 132:
133: public function getName()
134: {
135: return $this->name;
136: }
137:
138:
139: 140: 141: 142:
143: public function getPrimary($need = TRUE)
144: {
145: if ($this->primary === NULL && $need) {
146: throw new \LogicException("Table '{$this->name}' does not have a primary key.");
147: }
148: return $this->primary;
149: }
150:
151:
152: 153: 154:
155: public function getPrimarySequence()
156: {
157: if ($this->primarySequence === FALSE) {
158: $this->primarySequence = NULL;
159: $driver = $this->connection->getSupplementalDriver();
160: if ($driver->isSupported(ISupplementalDriver::SUPPORT_SEQUENCE) && $this->primary !== NULL) {
161: foreach ($driver->getColumns($this->name) as $column) {
162: if ($column['name'] === $this->primary) {
163: $this->primarySequence = $column['vendor']['sequence'];
164: break;
165: }
166: }
167: }
168: }
169:
170: return $this->primarySequence;
171: }
172:
173:
174: 175: 176: 177:
178: public function setPrimarySequence($sequence)
179: {
180: $this->primarySequence = $sequence;
181: return $this;
182: }
183:
184:
185: 186: 187:
188: public function getSql()
189: {
190: return $this->sqlBuilder->buildSelectQuery($this->getPreviousAccessedColumns());
191: }
192:
193:
194: 195: 196: 197: 198:
199: public function getPreviousAccessedColumns()
200: {
201: if ($this->cache && $this->previousAccessedColumns === NULL) {
202: $this->accessedColumns = $this->previousAccessedColumns = $this->cache->load($this->getGeneralCacheKey());
203: if ($this->previousAccessedColumns === NULL) {
204: $this->previousAccessedColumns = array();
205: }
206: }
207:
208: return array_keys(array_filter((array) $this->previousAccessedColumns));
209: }
210:
211:
212: 213: 214: 215:
216: public function getSqlBuilder()
217: {
218: return $this->sqlBuilder;
219: }
220:
221:
222:
223:
224:
225: 226: 227: 228: 229:
230: public function get($key)
231: {
232: $clone = clone $this;
233: return $clone->wherePrimary($key)->fetch();
234: }
235:
236:
237: 238: 239:
240: public function fetch()
241: {
242: $this->execute();
243: $return = current($this->data);
244: next($this->data);
245: return $return;
246: }
247:
248:
249: 250: 251:
252: public function fetchPairs($key = NULL, $value = NULL)
253: {
254: return Nette\Database\Helpers::toPairs(iterator_to_array($this), $key, $value);
255: }
256:
257:
258: 259: 260:
261: public function fetchAll()
262: {
263: return iterator_to_array($this);
264: }
265:
266:
267:
268:
269:
270: 271: 272: 273: 274:
275: public function select($columns)
276: {
277: $this->emptyResultSet();
278: call_user_func_array(array($this->sqlBuilder, 'addSelect'), func_get_args());
279: return $this;
280: }
281:
282:
283: 284: 285:
286: public function find($key)
287: {
288: trigger_error(__METHOD__ . '() is deprecated; use $selection->wherePrimary() instead.', E_USER_DEPRECATED);
289: return $this->wherePrimary($key);
290: }
291:
292:
293: 294: 295: 296: 297:
298: public function wherePrimary($key)
299: {
300: if (is_array($this->primary) && Nette\Utils\Arrays::isList($key)) {
301: if (isset($key[0]) && is_array($key[0])) {
302: $this->where($this->primary, $key);
303: } else {
304: foreach ($this->primary as $i => $primary) {
305: $this->where($this->name . '.' . $primary, $key[$i]);
306: }
307: }
308: } elseif (is_array($key) && !Nette\Utils\Arrays::isList($key)) {
309: $this->where($key);
310: } else {
311: $this->where($this->name . '.' . $this->getPrimary(), $key);
312: }
313:
314: return $this;
315: }
316:
317:
318: 319: 320: 321: 322: 323: 324:
325: public function where($condition, $parameters = array())
326: {
327: if (is_array($condition) && $parameters === array()) {
328: foreach ($condition as $key => $val) {
329: if (is_int($key)) {
330: $this->where($val);
331: } else {
332: $this->where($key, $val);
333: }
334: }
335: return $this;
336: }
337:
338: $this->emptyResultSet();
339: call_user_func_array(array($this->sqlBuilder, 'addWhere'), func_get_args());
340: return $this;
341: }
342:
343:
344: 345: 346: 347: 348:
349: public function order($columns)
350: {
351: $this->emptyResultSet();
352: call_user_func_array(array($this->sqlBuilder, 'addOrder'), func_get_args());
353: return $this;
354: }
355:
356:
357: 358: 359: 360: 361: 362:
363: public function limit($limit, $offset = NULL)
364: {
365: $this->emptyResultSet();
366: $this->sqlBuilder->setLimit($limit, $offset);
367: return $this;
368: }
369:
370:
371: 372: 373: 374: 375: 376:
377: public function page($page, $itemsPerPage, & $numOfPages = NULL)
378: {
379: if (func_num_args() > 2) {
380: $numOfPages = (int) ceil($this->count('*') / $itemsPerPage);
381: }
382: return $this->limit($itemsPerPage, ($page - 1) * $itemsPerPage);
383: }
384:
385:
386: 387: 388: 389: 390:
391: public function group($columns)
392: {
393: $this->emptyResultSet();
394: if (func_num_args() === 2 && strpos($columns, '?') === FALSE) {
395: trigger_error('Calling ' . __METHOD__ . '() with second argument is deprecated; use $selection->having() instead.', E_USER_DEPRECATED);
396: $this->having(func_get_arg(1));
397: $this->sqlBuilder->setGroup($columns);
398: } else {
399: call_user_func_array(array($this->sqlBuilder, 'setGroup'), func_get_args());
400: }
401: return $this;
402: }
403:
404:
405: 406: 407: 408: 409:
410: public function having($having)
411: {
412: $this->emptyResultSet();
413: call_user_func_array(array($this->sqlBuilder, 'setHaving'), func_get_args());
414: return $this;
415: }
416:
417:
418:
419:
420:
421: 422: 423: 424: 425:
426: public function aggregation($function)
427: {
428: $selection = $this->createSelectionInstance();
429: $selection->getSqlBuilder()->importConditions($this->getSqlBuilder());
430: $selection->select($function);
431: foreach ($selection->fetch() as $val) {
432: return $val;
433: }
434: }
435:
436:
437: 438: 439: 440: 441:
442: public function count($column = NULL)
443: {
444: if (!$column) {
445: $this->execute();
446: return count($this->data);
447: }
448: return $this->aggregation("COUNT($column)");
449: }
450:
451:
452: 453: 454: 455: 456:
457: public function min($column)
458: {
459: return $this->aggregation("MIN($column)");
460: }
461:
462:
463: 464: 465: 466: 467:
468: public function max($column)
469: {
470: return $this->aggregation("MAX($column)");
471: }
472:
473:
474: 475: 476: 477: 478:
479: public function sum($column)
480: {
481: return $this->aggregation("SUM($column)");
482: }
483:
484:
485:
486:
487:
488: protected function execute()
489: {
490: if ($this->rows !== NULL) {
491: return;
492: }
493:
494: $this->observeCache = $this;
495:
496: if ($this->primary === NULL && $this->sqlBuilder->getSelect() === NULL) {
497: throw new Nette\InvalidStateException('Table with no primary key requires an explicit select clause.');
498: }
499:
500: try {
501: $result = $this->query($this->getSql());
502:
503: } catch (\PDOException $exception) {
504: if (!$this->sqlBuilder->getSelect() && $this->previousAccessedColumns) {
505: $this->previousAccessedColumns = FALSE;
506: $this->accessedColumns = array();
507: $result = $this->query($this->getSql());
508: } else {
509: throw $exception;
510: }
511: }
512:
513: $this->rows = array();
514: $usedPrimary = TRUE;
515: foreach ($result->getPdoStatement() as $key => $row) {
516: $row = $this->createRow($result->normalizeRow($row));
517: $primary = $row->getSignature(FALSE);
518: $usedPrimary = $usedPrimary && $primary;
519: $this->rows[$primary ?: $key] = $row;
520: }
521: $this->data = $this->rows;
522:
523: if ($usedPrimary && $this->accessedColumns !== FALSE) {
524: foreach ((array) $this->primary as $primary) {
525: $this->accessedColumns[$primary] = TRUE;
526: }
527: }
528: }
529:
530:
531: protected function createRow(array $row)
532: {
533: return new ActiveRow($row, $this);
534: }
535:
536:
537: public function createSelectionInstance($table = NULL)
538: {
539: return new Selection($this->connection, $table ?: $this->name, $this->reflection, $this->cache ? $this->cache->getStorage() : NULL);
540: }
541:
542:
543: protected function createGroupedSelectionInstance($table, $column)
544: {
545: return new GroupedSelection($this, $table, $column);
546: }
547:
548:
549: protected function query($query)
550: {
551: return $this->connection->queryArgs($query, $this->sqlBuilder->getParameters());
552: }
553:
554:
555: protected function emptyResultSet($saveCache = TRUE)
556: {
557: if ($this->rows !== NULL && $saveCache) {
558: $this->saveCacheState();
559: }
560:
561: $this->rows = NULL;
562: $this->specificCacheKey = NULL;
563: $this->generalCacheKey = NULL;
564: $this->refCache['referencingPrototype'] = array();
565: }
566:
567:
568: protected function saveCacheState()
569: {
570: if ($this->observeCache === $this && $this->cache && !$this->sqlBuilder->getSelect() && $this->accessedColumns !== $this->previousAccessedColumns) {
571: $previousAccessed = $this->cache->load($this->getGeneralCacheKey());
572: $accessed = $this->accessedColumns;
573: $needSave = is_array($accessed) && is_array($previousAccessed)
574: ? array_intersect_key($accessed, $previousAccessed) !== $accessed
575: : $accessed !== $previousAccessed;
576:
577: if ($needSave) {
578: $save = is_array($accessed) && is_array($previousAccessed) ? $previousAccessed + $accessed : $accessed;
579: $this->cache->save($this->getGeneralCacheKey(), $save);
580: $this->previousAccessedColumns = NULL;
581: }
582: }
583: }
584:
585:
586: 587: 588: 589:
590: protected function getRefTable(& $refPath)
591: {
592: return $this;
593: }
594:
595:
596: 597: 598:
599: protected function loadRefCache()
600: {
601: }
602:
603:
604: 605: 606: 607: 608:
609: protected function getGeneralCacheKey()
610: {
611: if ($this->generalCacheKey) {
612: return $this->generalCacheKey;
613: }
614:
615: return $this->generalCacheKey = md5(serialize(array(__CLASS__, $this->name, $this->sqlBuilder->getConditions())));
616: }
617:
618:
619: 620: 621: 622: 623:
624: protected function getSpecificCacheKey()
625: {
626: if ($this->specificCacheKey) {
627: return $this->specificCacheKey;
628: }
629:
630: return $this->specificCacheKey = md5($this->getSql() . json_encode($this->sqlBuilder->getParameters()));
631: }
632:
633:
634: 635: 636: 637: 638:
639: public function accessColumn($key, $selectColumn = TRUE)
640: {
641: if (!$this->cache) {
642: return;
643: }
644:
645: if ($key === NULL) {
646: $this->accessedColumns = FALSE;
647: $currentKey = key((array) $this->data);
648: } elseif ($this->accessedColumns !== FALSE) {
649: $this->accessedColumns[$key] = $selectColumn;
650: }
651:
652: if ($selectColumn && !$this->sqlBuilder->getSelect() && $this->previousAccessedColumns && ($key === NULL || !isset($this->previousAccessedColumns[$key]))) {
653: $this->previousAccessedColumns = array();
654:
655: if ($this->sqlBuilder->getLimit()) {
656: $generalCacheKey = $this->generalCacheKey;
657: $sqlBuilder = $this->sqlBuilder;
658:
659: $primaryValues = array();
660: foreach ((array) $this->rows as $row) {
661: $primary = $row->getPrimary();
662: $primaryValues[] = is_array($primary) ? array_values($primary) : $primary;
663: }
664:
665: $this->emptyResultSet(FALSE);
666: $this->sqlBuilder = clone $this->sqlBuilder;
667: $this->sqlBuilder->setLimit(NULL, NULL);
668: $this->wherePrimary($primaryValues);
669:
670: $this->generalCacheKey = $generalCacheKey;
671: $this->execute();
672: $this->sqlBuilder = $sqlBuilder;
673: } else {
674: $this->emptyResultSet(FALSE);
675: $this->execute();
676: }
677:
678: $this->dataRefreshed = TRUE;
679:
680:
681: if (isset($currentKey)) {
682: while (key($this->data) !== $currentKey) {
683: next($this->data);
684: }
685: }
686: }
687: }
688:
689:
690: 691: 692: 693:
694: public function removeAccessColumn($key)
695: {
696: if ($this->cache && is_array($this->accessedColumns)) {
697: $this->accessedColumns[$key] = FALSE;
698: }
699: }
700:
701:
702: 703: 704: 705:
706: public function getDataRefreshed()
707: {
708: return $this->dataRefreshed;
709: }
710:
711:
712:
713:
714:
715: 716: 717: 718: 719:
720: public function insert($data)
721: {
722: if ($data instanceof Selection) {
723: $data = new Nette\Database\SqlLiteral($data->getSql(), $data->getSqlBuilder()->getParameters());
724:
725: } elseif ($data instanceof \Traversable) {
726: $data = iterator_to_array($data);
727: }
728:
729: $return = $this->connection->query($this->sqlBuilder->buildInsertQuery(), $data);
730: $this->loadRefCache();
731:
732: if ($data instanceof Nette\Database\SqlLiteral || $this->primary === NULL) {
733: unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]);
734: return $return->getRowCount();
735: }
736:
737: $primaryKey = $this->connection->getInsertId($this->getPrimarySequence());
738: if ($primaryKey === FALSE) {
739: unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]);
740: return $return->getRowCount();
741: }
742:
743: if (is_array($this->getPrimary())) {
744: $primaryKey = array();
745:
746: foreach ((array) $this->getPrimary() as $key) {
747: if (!isset($data[$key])) {
748: return $data;
749: }
750:
751: $primaryKey[$key] = $data[$key];
752: }
753: if (count($primaryKey) === 1) {
754: $primaryKey = reset($primaryKey);
755: }
756: }
757:
758: $row = $this->createSelectionInstance()
759: ->select('*')
760: ->wherePrimary($primaryKey)
761: ->fetch();
762:
763: if ($this->rows !== NULL) {
764: if ($signature = $row->getSignature(FALSE)) {
765: $this->rows[$signature] = $row;
766: $this->data[$signature] = $row;
767: } else {
768: $this->rows[] = $row;
769: $this->data[] = $row;
770: }
771: }
772:
773: return $row;
774: }
775:
776:
777: 778: 779: 780: 781: 782:
783: public function update($data)
784: {
785: if ($data instanceof \Traversable) {
786: $data = iterator_to_array($data);
787:
788: } elseif (!is_array($data)) {
789: throw new Nette\InvalidArgumentException;
790: }
791:
792: if (!$data) {
793: return 0;
794: }
795:
796: return $this->connection->queryArgs(
797: $this->sqlBuilder->buildUpdateQuery(),
798: array_merge(array($data), $this->sqlBuilder->getParameters())
799: )->getRowCount();
800: }
801:
802:
803: 804: 805: 806:
807: public function delete()
808: {
809: return $this->query($this->sqlBuilder->buildDeleteQuery())->getRowCount();
810: }
811:
812:
813:
814:
815:
816: 817: 818: 819: 820: 821: 822:
823: public function getReferencedTable($table, $column, $checkPrimaryKey)
824: {
825: $referenced = & $this->refCache['referenced'][$this->getSpecificCacheKey()]["$table.$column"];
826: $selection = & $referenced['selection'];
827: $cacheKeys = & $referenced['cacheKeys'];
828: if ($selection === NULL || !isset($cacheKeys[$checkPrimaryKey])) {
829: $this->execute();
830: $cacheKeys = array();
831: foreach ($this->rows as $row) {
832: if ($row[$column] === NULL) {
833: continue;
834: }
835:
836: $key = $row[$column];
837: $cacheKeys[$key] = TRUE;
838: }
839:
840: if ($cacheKeys) {
841: $selection = $this->createSelectionInstance($table);
842: $selection->where($selection->getPrimary(), array_keys($cacheKeys));
843: } else {
844: $selection = array();
845: }
846: }
847:
848: return $selection;
849: }
850:
851:
852: 853: 854: 855: 856: 857: 858:
859: public function getReferencingTable($table, $column, $active = NULL)
860: {
861: $prototype = & $this->refCache['referencingPrototype']["$table.$column"];
862: if (!$prototype) {
863: $prototype = $this->createGroupedSelectionInstance($table, $column);
864: $prototype->where("$table.$column", array_keys((array) $this->rows));
865: }
866:
867: $clone = clone $prototype;
868: $clone->setActive($active);
869: return $clone;
870: }
871:
872:
873:
874:
875:
876: public function rewind()
877: {
878: $this->execute();
879: $this->keys = array_keys($this->data);
880: reset($this->keys);
881: }
882:
883:
884:
885: public function current()
886: {
887: if (($key = current($this->keys)) !== FALSE) {
888: return $this->data[$key];
889: } else {
890: return FALSE;
891: }
892: }
893:
894:
895: 896: 897:
898: public function key()
899: {
900: return current($this->keys);
901: }
902:
903:
904: public function next()
905: {
906: next($this->keys);
907: }
908:
909:
910: public function valid()
911: {
912: return current($this->keys) !== FALSE;
913: }
914:
915:
916:
917:
918:
919: 920: 921: 922: 923: 924:
925: public function offsetSet($key, $value)
926: {
927: $this->execute();
928: $this->rows[$key] = $value;
929: }
930:
931:
932: 933: 934: 935: 936:
937: public function offsetGet($key)
938: {
939: $this->execute();
940: return $this->rows[$key];
941: }
942:
943:
944: 945: 946: 947: 948:
949: public function offsetExists($key)
950: {
951: $this->execute();
952: return isset($this->rows[$key]);
953: }
954:
955:
956: 957: 958: 959: 960:
961: public function offsetUnset($key)
962: {
963: $this->execute();
964: unset($this->rows[$key], $this->data[$key]);
965: }
966:
967: }
968: