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