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