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: use CollectionTrait;
34:
35: 36: 37: 38: 39: 40:
41: protected $_query;
42:
43: 44: 45: 46: 47:
48: protected $_statement;
49:
50: 51: 52: 53: 54:
55: protected $_index = 0;
56:
57: 58: 59: 60: 61:
62: protected $_current;
63:
64: 65: 66: 67: 68:
69: protected $_defaultTable;
70:
71: 72: 73: 74: 75:
76: protected $_defaultAlias;
77:
78: 79: 80: 81: 82: 83:
84: protected $_matchingMap = [];
85:
86: 87: 88: 89: 90:
91: protected $_containMap = [];
92:
93: 94: 95: 96: 97: 98:
99: protected $_map = [];
100:
101: 102: 103: 104: 105: 106:
107: protected $_matchingMapColumns = [];
108:
109: 110: 111: 112: 113:
114: protected $_results = [];
115:
116: 117: 118: 119: 120:
121: protected $_hydrate = true;
122:
123: 124: 125: 126: 127:
128: protected $_autoFields;
129:
130: 131: 132: 133: 134:
135: protected $_entityClass;
136:
137: 138: 139: 140: 141:
142: protected $_useBuffering = true;
143:
144: 145: 146: 147: 148:
149: protected $_count;
150:
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:
448: protected function _getTypes($table, $fields)
449: {
450: $types = [];
451: $schema = $table->getSchema();
452: $map = array_keys((array)Type::getMap() + ['string' => 1, 'text' => 1, 'boolean' => 1]);
453: $typeMap = array_combine(
454: $map,
455: array_map(['Cake\Database\Type', 'build'], $map)
456: );
457:
458: foreach (['string', 'text'] as $t) {
459: if (get_class($typeMap[$t]) === 'Cake\Database\Type') {
460: unset($typeMap[$t]);
461: }
462: }
463:
464: foreach (array_intersect($fields, $schema->columns()) as $col) {
465: $typeName = $schema->getColumnType($col);
466: if (isset($typeMap[$typeName])) {
467: $types[$col] = $typeMap[$typeName];
468: }
469: }
470:
471: return $types;
472: }
473:
474: 475: 476: 477: 478: 479:
480: protected function _fetchResult()
481: {
482: if (!$this->_statement) {
483: return false;
484: }
485:
486: $row = $this->_statement->fetch('assoc');
487: if ($row === false) {
488: return $row;
489: }
490:
491: return $this->_groupResult($row);
492: }
493:
494: 495: 496: 497: 498: 499:
500: protected function _groupResult($row)
501: {
502: $defaultAlias = $this->_defaultAlias;
503: $results = $presentAliases = [];
504: $options = [
505: 'useSetters' => false,
506: 'markClean' => true,
507: 'markNew' => false,
508: 'guard' => false
509: ];
510:
511: foreach ($this->_matchingMapColumns as $alias => $keys) {
512: $matching = $this->_matchingMap[$alias];
513: $results['_matchingData'][$alias] = array_combine(
514: $keys,
515: array_intersect_key($row, $keys)
516: );
517: if ($this->_hydrate) {
518:
519: $table = $matching['instance'];
520: $options['source'] = $table->getRegistryAlias();
521:
522: $entity = new $matching['entityClass']($results['_matchingData'][$alias], $options);
523: $results['_matchingData'][$alias] = $entity;
524: }
525: }
526:
527: foreach ($this->_map as $table => $keys) {
528: $results[$table] = array_combine($keys, array_intersect_key($row, $keys));
529: $presentAliases[$table] = true;
530: }
531:
532:
533:
534:
535: if (!isset($results[$defaultAlias])) {
536: $results[$defaultAlias] = [];
537: }
538:
539: unset($presentAliases[$defaultAlias]);
540:
541: foreach ($this->_containMap as $assoc) {
542: $alias = $assoc['nestKey'];
543:
544: if ($assoc['canBeJoined'] && empty($this->_map[$alias])) {
545: continue;
546: }
547:
548:
549: $instance = $assoc['instance'];
550:
551: if (!$assoc['canBeJoined'] && !isset($row[$alias])) {
552: $results = $instance->defaultRowValue($results, $assoc['canBeJoined']);
553: continue;
554: }
555:
556: if (!$assoc['canBeJoined']) {
557: $results[$alias] = $row[$alias];
558: }
559:
560: $target = $instance->getTarget();
561: $options['source'] = $target->getRegistryAlias();
562: unset($presentAliases[$alias]);
563:
564: if ($assoc['canBeJoined'] && $this->_autoFields !== false) {
565: $hasData = false;
566: foreach ($results[$alias] as $v) {
567: if ($v !== null && $v !== []) {
568: $hasData = true;
569: break;
570: }
571: }
572:
573: if (!$hasData) {
574: $results[$alias] = null;
575: }
576: }
577:
578: if ($this->_hydrate && $results[$alias] !== null && $assoc['canBeJoined']) {
579: $entity = new $assoc['entityClass']($results[$alias], $options);
580: $results[$alias] = $entity;
581: }
582:
583: $results = $instance->transformRow($results, $alias, $assoc['canBeJoined'], $assoc['targetProperty']);
584: }
585:
586: foreach ($presentAliases as $alias => $present) {
587: if (!isset($results[$alias])) {
588: continue;
589: }
590: $results[$defaultAlias][$alias] = $results[$alias];
591: }
592:
593: if (isset($results['_matchingData'])) {
594: $results[$defaultAlias]['_matchingData'] = $results['_matchingData'];
595: }
596:
597: $options['source'] = $this->_defaultTable->getRegistryAlias();
598: if (isset($results[$defaultAlias])) {
599: $results = $results[$defaultAlias];
600: }
601: if ($this->_hydrate && !($results instanceof EntityInterface)) {
602: $results = new $this->_entityClass($results, $options);
603: }
604:
605: return $results;
606: }
607:
608: 609: 610: 611: 612: 613: 614: 615: 616:
617: protected function _castValues($alias, $values)
618: {
619: deprecationWarning('ResultSet::_castValues() is deprecated, and will be removed in 4.0.0.');
620:
621: return $values;
622: }
623:
624: 625: 626: 627: 628: 629:
630: public function __debugInfo()
631: {
632: return [
633: 'items' => $this->toArray(),
634: ];
635: }
636: }
637: