1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
15: namespace Cake\ORM;
16:
17: use Cake\Collection\Collection;
18: use Cake\Collection\CollectionTrait;
19: use Cake\Database\Exception;
20: use Cake\Database\Type;
21: use Cake\Datasource\EntityInterface;
22: use Cake\Datasource\ResultSetInterface;
23: use SplFixedArray;
24:
25: 26: 27: 28: 29: 30:
31: class ResultSet implements ResultSetInterface
32: {
33:
34: use CollectionTrait;
35:
36: 37: 38: 39: 40: 41:
42: protected $_query;
43:
44: 45: 46: 47: 48:
49: protected $_statement;
50:
51: 52: 53: 54: 55:
56: protected $_index = 0;
57:
58: 59: 60: 61: 62:
63: protected $_current;
64:
65: 66: 67: 68: 69:
70: protected $_defaultTable;
71:
72: 73: 74: 75: 76:
77: protected $_defaultAlias;
78:
79: 80: 81: 82: 83: 84:
85: protected $_matchingMap = [];
86:
87: 88: 89: 90: 91:
92: protected $_containMap = [];
93:
94: 95: 96: 97: 98: 99:
100: protected $_map = [];
101:
102: 103: 104: 105: 106: 107:
108: protected $_matchingMapColumns = [];
109:
110: 111: 112: 113: 114:
115: protected $_results = [];
116:
117: 118: 119: 120: 121:
122: protected $_hydrate = true;
123:
124: 125: 126: 127: 128:
129: protected $_autoFields;
130:
131: 132: 133: 134: 135:
136: protected $_entityClass;
137:
138: 139: 140: 141: 142:
143: protected $_useBuffering = true;
144:
145: 146: 147: 148: 149:
150: protected $_count;
151:
152: 153: 154: 155: 156: 157: 158:
159: protected $_types = [];
160:
161: 162: 163: 164: 165: 166: 167:
168: protected $_driver;
169:
170: 171: 172: 173: 174: 175:
176: public function __construct($query, $statement)
177: {
178:
179: $repository = $query->getRepository();
180: $this->_statement = $statement;
181: $this->_driver = $query->getConnection()->getDriver();
182: $this->_defaultTable = $query->getRepository();
183: $this->_calculateAssociationMap($query);
184: $this->_hydrate = $query->isHydrationEnabled();
185: $this->_entityClass = $repository->getEntityClass();
186: $this->_useBuffering = $query->isBufferedResultsEnabled();
187: $this->_defaultAlias = $this->_defaultTable->getAlias();
188: $this->_calculateColumnMap($query);
189: $this->_autoFields = $query->isAutoFieldsEnabled();
190:
191: if ($this->_useBuffering) {
192: $count = $this->count();
193: $this->_results = new SplFixedArray($count);
194: }
195: }
196:
197: 198: 199: 200: 201: 202: 203:
204: public function current()
205: {
206: return $this->_current;
207: }
208:
209: 210: 211: 212: 213: 214: 215:
216: public function key()
217: {
218: return $this->_index;
219: }
220:
221: 222: 223: 224: 225: 226: 227:
228: public function next()
229: {
230: $this->_index++;
231: }
232:
233: 234: 235: 236: 237: 238: 239: 240:
241: public function rewind()
242: {
243: if ($this->_index == 0) {
244: return;
245: }
246:
247: if (!$this->_useBuffering) {
248: $msg = 'You cannot rewind an un-buffered ResultSet. Use Query::bufferResults() to get a buffered ResultSet.';
249: throw new Exception($msg);
250: }
251:
252: $this->_index = 0;
253: }
254:
255: 256: 257: 258: 259: 260: 261:
262: public function valid()
263: {
264: if ($this->_useBuffering) {
265: $valid = $this->_index < $this->_count;
266: if ($valid && $this->_results[$this->_index] !== null) {
267: $this->_current = $this->_results[$this->_index];
268:
269: return true;
270: }
271: if (!$valid) {
272: return $valid;
273: }
274: }
275:
276: $this->_current = $this->_fetchResult();
277: $valid = $this->_current !== false;
278:
279: if ($valid && $this->_useBuffering) {
280: $this->_results[$this->_index] = $this->_current;
281: }
282: if (!$valid && $this->_statement !== null) {
283: $this->_statement->closeCursor();
284: }
285:
286: return $valid;
287: }
288:
289: 290: 291: 292: 293: 294: 295:
296: public function first()
297: {
298: foreach ($this as $result) {
299: if ($this->_statement && !$this->_useBuffering) {
300: $this->_statement->closeCursor();
301: }
302:
303: return $result;
304: }
305: }
306:
307: 308: 309: 310: 311: 312: 313:
314: public function serialize()
315: {
316: if (!$this->_useBuffering) {
317: $msg = 'You cannot serialize an un-buffered ResultSet. Use Query::bufferResults() to get a buffered ResultSet.';
318: throw new Exception($msg);
319: }
320:
321: while ($this->valid()) {
322: $this->next();
323: }
324:
325: if ($this->_results instanceof SplFixedArray) {
326: return serialize($this->_results->toArray());
327: }
328:
329: return serialize($this->_results);
330: }
331:
332: 333: 334: 335: 336: 337: 338: 339:
340: public function unserialize($serialized)
341: {
342: $results = (array)(unserialize($serialized) ?: []);
343: $this->_results = SplFixedArray::fromArray($results);
344: $this->_useBuffering = true;
345: $this->_count = $this->_results->count();
346: }
347:
348: 349: 350: 351: 352: 353: 354:
355: public function count()
356: {
357: if ($this->_count !== null) {
358: return $this->_count;
359: }
360: if ($this->_statement) {
361: return $this->_count = $this->_statement->rowCount();
362: }
363:
364: if ($this->_results instanceof SplFixedArray) {
365: $this->_count = $this->_results->count();
366: } else {
367: $this->_count = count($this->_results);
368: }
369:
370: return $this->_count;
371: }
372:
373: 374: 375: 376: 377: 378: 379:
380: protected function _calculateAssociationMap($query)
381: {
382: $map = $query->getEagerLoader()->associationsMap($this->_defaultTable);
383: $this->_matchingMap = (new Collection($map))
384: ->match(['matching' => true])
385: ->indexBy('alias')
386: ->toArray();
387:
388: $this->_containMap = (new Collection(array_reverse($map)))
389: ->match(['matching' => false])
390: ->indexBy('nestKey')
391: ->toArray();
392: }
393:
394: 395: 396: 397: 398: 399: 400:
401: protected function _calculateColumnMap($query)
402: {
403: $map = [];
404: foreach ($query->clause('select') as $key => $field) {
405: $key = trim($key, '"`[]');
406:
407: if (strpos($key, '__') <= 0) {
408: $map[$this->_defaultAlias][$key] = $key;
409: continue;
410: }
411:
412: $parts = explode('__', $key, 2);
413: $map[$parts[0]][$key] = $parts[1];
414: }
415:
416: foreach ($this->_matchingMap as $alias => $assoc) {
417: if (!isset($map[$alias])) {
418: continue;
419: }
420: $this->_matchingMapColumns[$alias] = $map[$alias];
421: unset($map[$alias]);
422: }
423:
424: $this->_map = $map;
425: }
426:
427: 428: 429: 430: 431: 432: 433:
434: protected function _calculateTypeMap()
435: {
436: deprecationWarning('ResultSet::_calculateTypeMap() is deprecated, and will be removed in 4.0.0.');
437: }
438:
439: 440: 441: 442: 443: 444: 445: 446:
447: protected function _getTypes($table, $fields)
448: {
449: $types = [];
450: $schema = $table->getSchema();
451: $map = array_keys((array)Type::getMap() + ['string' => 1, 'text' => 1, 'boolean' => 1]);
452: $typeMap = array_combine(
453: $map,
454: array_map(['Cake\Database\Type', 'build'], $map)
455: );
456:
457: foreach (['string', 'text'] as $t) {
458: if (get_class($typeMap[$t]) === 'Cake\Database\Type') {
459: unset($typeMap[$t]);
460: }
461: }
462:
463: foreach (array_intersect($fields, $schema->columns()) as $col) {
464: $typeName = $schema->getColumnType($col);
465: if (isset($typeMap[$typeName])) {
466: $types[$col] = $typeMap[$typeName];
467: }
468: }
469:
470: return $types;
471: }
472:
473: 474: 475: 476: 477: 478:
479: protected function _fetchResult()
480: {
481: if (!$this->_statement) {
482: return false;
483: }
484:
485: $row = $this->_statement->fetch('assoc');
486: if ($row === false) {
487: return $row;
488: }
489:
490: return $this->_groupResult($row);
491: }
492:
493: 494: 495: 496: 497: 498:
499: protected function _groupResult($row)
500: {
501: $defaultAlias = $this->_defaultAlias;
502: $results = $presentAliases = [];
503: $options = [
504: 'useSetters' => false,
505: 'markClean' => true,
506: 'markNew' => false,
507: 'guard' => false
508: ];
509:
510: foreach ($this->_matchingMapColumns as $alias => $keys) {
511: $matching = $this->_matchingMap[$alias];
512: $results['_matchingData'][$alias] = array_combine(
513: $keys,
514: array_intersect_key($row, $keys)
515: );
516: if ($this->_hydrate) {
517:
518: $table = $matching['instance'];
519: $options['source'] = $table->getRegistryAlias();
520:
521: $entity = new $matching['entityClass']($results['_matchingData'][$alias], $options);
522: $results['_matchingData'][$alias] = $entity;
523: }
524: }
525:
526: foreach ($this->_map as $table => $keys) {
527: $results[$table] = array_combine($keys, array_intersect_key($row, $keys));
528: $presentAliases[$table] = true;
529: }
530:
531:
532:
533:
534: if (!isset($results[$defaultAlias])) {
535: $results[$defaultAlias] = [];
536: }
537:
538: unset($presentAliases[$defaultAlias]);
539:
540: foreach ($this->_containMap as $assoc) {
541: $alias = $assoc['nestKey'];
542:
543: if ($assoc['canBeJoined'] && empty($this->_map[$alias])) {
544: continue;
545: }
546:
547:
548: $instance = $assoc['instance'];
549:
550: if (!$assoc['canBeJoined'] && !isset($row[$alias])) {
551: $results = $instance->defaultRowValue($results, $assoc['canBeJoined']);
552: continue;
553: }
554:
555: if (!$assoc['canBeJoined']) {
556: $results[$alias] = $row[$alias];
557: }
558:
559: $target = $instance->getTarget();
560: $options['source'] = $target->getRegistryAlias();
561: unset($presentAliases[$alias]);
562:
563: if ($assoc['canBeJoined'] && $this->_autoFields !== false) {
564: $hasData = false;
565: foreach ($results[$alias] as $v) {
566: if ($v !== null && $v !== []) {
567: $hasData = true;
568: break;
569: }
570: }
571:
572: if (!$hasData) {
573: $results[$alias] = null;
574: }
575: }
576:
577: if ($this->_hydrate && $results[$alias] !== null && $assoc['canBeJoined']) {
578: $entity = new $assoc['entityClass']($results[$alias], $options);
579: $results[$alias] = $entity;
580: }
581:
582: $results = $instance->transformRow($results, $alias, $assoc['canBeJoined'], $assoc['targetProperty']);
583: }
584:
585: foreach ($presentAliases as $alias => $present) {
586: if (!isset($results[$alias])) {
587: continue;
588: }
589: $results[$defaultAlias][$alias] = $results[$alias];
590: }
591:
592: if (isset($results['_matchingData'])) {
593: $results[$defaultAlias]['_matchingData'] = $results['_matchingData'];
594: }
595:
596: $options['source'] = $this->_defaultTable->getRegistryAlias();
597: if (isset($results[$defaultAlias])) {
598: $results = $results[$defaultAlias];
599: }
600: if ($this->_hydrate && !($results instanceof EntityInterface)) {
601: $results = new $this->_entityClass($results, $options);
602: }
603:
604: return $results;
605: }
606:
607: 608: 609: 610: 611: 612: 613: 614: 615:
616: protected function _castValues($alias, $values)
617: {
618: deprecationWarning('ResultSet::_castValues() is deprecated, and will be removed in 4.0.0.');
619:
620: return $values;
621: }
622:
623: 624: 625: 626: 627: 628:
629: public function __debugInfo()
630: {
631: return [
632: 'items' => $this->toArray(),
633: ];
634: }
635: }
636: