ResultSet.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  11. * @link https://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\ORM;
  16. use Cake\Collection\Collection;
  17. use Cake\Collection\CollectionTrait;
  18. use Cake\Database\Exception;
  19. use Cake\Database\Type;
  20. use Cake\Datasource\EntityInterface;
  21. use Cake\Datasource\ResultSetInterface;
  22. use SplFixedArray;
  23. /**
  24. * Represents the results obtained after executing a query for a specific table
  25. * This object is responsible for correctly nesting result keys reported from
  26. * the query, casting each field to the correct type and executing the extra
  27. * queries required for eager loading external associations.
  28. */
  29. class ResultSet implements ResultSetInterface
  30. {
  31. use CollectionTrait;
  32. /**
  33. * Original query from where results were generated
  34. *
  35. * @var \Cake\ORM\Query
  36. * @deprecated 3.1.6 Due to a memory leak, this property cannot be used anymore
  37. */
  38. protected $_query;
  39. /**
  40. * Database statement holding the results
  41. *
  42. * @var \Cake\Database\StatementInterface
  43. */
  44. protected $_statement;
  45. /**
  46. * Points to the next record number that should be fetched
  47. *
  48. * @var int
  49. */
  50. protected $_index = 0;
  51. /**
  52. * Last record fetched from the statement
  53. *
  54. * @var array
  55. */
  56. protected $_current;
  57. /**
  58. * Default table instance
  59. *
  60. * @var \Cake\ORM\Table
  61. */
  62. protected $_defaultTable;
  63. /**
  64. * The default table alias
  65. *
  66. * @var string
  67. */
  68. protected $_defaultAlias;
  69. /**
  70. * List of associations that should be placed under the `_matchingData`
  71. * result key.
  72. *
  73. * @var array
  74. */
  75. protected $_matchingMap = [];
  76. /**
  77. * List of associations that should be eager loaded.
  78. *
  79. * @var array
  80. */
  81. protected $_containMap = [];
  82. /**
  83. * Map of fields that are fetched from the statement with
  84. * their type and the table they belong to
  85. *
  86. * @var array
  87. */
  88. protected $_map = [];
  89. /**
  90. * List of matching associations and the column keys to expect
  91. * from each of them.
  92. *
  93. * @var array
  94. */
  95. protected $_matchingMapColumns = [];
  96. /**
  97. * Results that have been fetched or hydrated into the results.
  98. *
  99. * @var array|\ArrayAccess
  100. */
  101. protected $_results = [];
  102. /**
  103. * Whether to hydrate results into objects or not
  104. *
  105. * @var bool
  106. */
  107. protected $_hydrate = true;
  108. /**
  109. * Tracks value of $_autoFields property of $query passed to constructor.
  110. *
  111. * @var bool
  112. */
  113. protected $_autoFields;
  114. /**
  115. * The fully namespaced name of the class to use for hydrating results
  116. *
  117. * @var string
  118. */
  119. protected $_entityClass;
  120. /**
  121. * Whether or not to buffer results fetched from the statement
  122. *
  123. * @var bool
  124. */
  125. protected $_useBuffering = true;
  126. /**
  127. * Holds the count of records in this result set
  128. *
  129. * @var int
  130. */
  131. protected $_count;
  132. /**
  133. * Type cache for type converters.
  134. *
  135. * Converters are indexed by alias and column name.
  136. *
  137. * @var array
  138. */
  139. protected $_types = [];
  140. /**
  141. * The Database driver object.
  142. *
  143. * Cached in a property to avoid multiple calls to the same function.
  144. *
  145. * @var \Cake\Database\Driver
  146. */
  147. protected $_driver;
  148. /**
  149. * Constructor
  150. *
  151. * @param \Cake\ORM\Query $query Query from where results come
  152. * @param \Cake\Database\StatementInterface $statement The statement to fetch from
  153. */
  154. public function __construct($query, $statement)
  155. {
  156. /** @var \Cake\ORM\Table $repository */
  157. $repository = $query->getRepository();
  158. $this->_statement = $statement;
  159. $this->_driver = $query->getConnection()->getDriver();
  160. $this->_defaultTable = $query->getRepository();
  161. $this->_calculateAssociationMap($query);
  162. $this->_hydrate = $query->isHydrationEnabled();
  163. $this->_entityClass = $repository->getEntityClass();
  164. $this->_useBuffering = $query->isBufferedResultsEnabled();
  165. $this->_defaultAlias = $this->_defaultTable->getAlias();
  166. $this->_calculateColumnMap($query);
  167. $this->_autoFields = $query->isAutoFieldsEnabled();
  168. if ($this->_useBuffering) {
  169. $count = $this->count();
  170. $this->_results = new SplFixedArray($count);
  171. }
  172. }
  173. /**
  174. * Returns the current record in the result iterator
  175. *
  176. * Part of Iterator interface.
  177. *
  178. * @return array|object
  179. */
  180. public function current()
  181. {
  182. return $this->_current;
  183. }
  184. /**
  185. * Returns the key of the current record in the iterator
  186. *
  187. * Part of Iterator interface.
  188. *
  189. * @return int
  190. */
  191. public function key()
  192. {
  193. return $this->_index;
  194. }
  195. /**
  196. * Advances the iterator pointer to the next record
  197. *
  198. * Part of Iterator interface.
  199. *
  200. * @return void
  201. */
  202. public function next()
  203. {
  204. $this->_index++;
  205. }
  206. /**
  207. * Rewinds a ResultSet.
  208. *
  209. * Part of Iterator interface.
  210. *
  211. * @throws \Cake\Database\Exception
  212. * @return void
  213. */
  214. public function rewind()
  215. {
  216. if ($this->_index == 0) {
  217. return;
  218. }
  219. if (!$this->_useBuffering) {
  220. $msg = 'You cannot rewind an un-buffered ResultSet. Use Query::bufferResults() to get a buffered ResultSet.';
  221. throw new Exception($msg);
  222. }
  223. $this->_index = 0;
  224. }
  225. /**
  226. * Whether there are more results to be fetched from the iterator
  227. *
  228. * Part of Iterator interface.
  229. *
  230. * @return bool
  231. */
  232. public function valid()
  233. {
  234. if ($this->_useBuffering) {
  235. $valid = $this->_index < $this->_count;
  236. if ($valid && $this->_results[$this->_index] !== null) {
  237. $this->_current = $this->_results[$this->_index];
  238. return true;
  239. }
  240. if (!$valid) {
  241. return $valid;
  242. }
  243. }
  244. $this->_current = $this->_fetchResult();
  245. $valid = $this->_current !== false;
  246. if ($valid && $this->_useBuffering) {
  247. $this->_results[$this->_index] = $this->_current;
  248. }
  249. if (!$valid && $this->_statement !== null) {
  250. $this->_statement->closeCursor();
  251. }
  252. return $valid;
  253. }
  254. /**
  255. * Get the first record from a result set.
  256. *
  257. * This method will also close the underlying statement cursor.
  258. *
  259. * @return array|object
  260. */
  261. public function first()
  262. {
  263. foreach ($this as $result) {
  264. if ($this->_statement && !$this->_useBuffering) {
  265. $this->_statement->closeCursor();
  266. }
  267. return $result;
  268. }
  269. }
  270. /**
  271. * Serializes a resultset.
  272. *
  273. * Part of Serializable interface.
  274. *
  275. * @return string Serialized object
  276. */
  277. public function serialize()
  278. {
  279. if (!$this->_useBuffering) {
  280. $msg = 'You cannot serialize an un-buffered ResultSet. Use Query::bufferResults() to get a buffered ResultSet.';
  281. throw new Exception($msg);
  282. }
  283. while ($this->valid()) {
  284. $this->next();
  285. }
  286. if ($this->_results instanceof SplFixedArray) {
  287. return serialize($this->_results->toArray());
  288. }
  289. return serialize($this->_results);
  290. }
  291. /**
  292. * Unserializes a resultset.
  293. *
  294. * Part of Serializable interface.
  295. *
  296. * @param string $serialized Serialized object
  297. * @return void
  298. */
  299. public function unserialize($serialized)
  300. {
  301. $results = (array)(unserialize($serialized) ?: []);
  302. $this->_results = SplFixedArray::fromArray($results);
  303. $this->_useBuffering = true;
  304. $this->_count = $this->_results->count();
  305. }
  306. /**
  307. * Gives the number of rows in the result set.
  308. *
  309. * Part of the Countable interface.
  310. *
  311. * @return int
  312. */
  313. public function count()
  314. {
  315. if ($this->_count !== null) {
  316. return $this->_count;
  317. }
  318. if ($this->_statement) {
  319. return $this->_count = $this->_statement->rowCount();
  320. }
  321. if ($this->_results instanceof SplFixedArray) {
  322. $this->_count = $this->_results->count();
  323. } else {
  324. $this->_count = count($this->_results);
  325. }
  326. return $this->_count;
  327. }
  328. /**
  329. * Calculates the list of associations that should get eager loaded
  330. * when fetching each record
  331. *
  332. * @param \Cake\ORM\Query $query The query from where to derive the associations
  333. * @return void
  334. */
  335. protected function _calculateAssociationMap($query)
  336. {
  337. $map = $query->getEagerLoader()->associationsMap($this->_defaultTable);
  338. $this->_matchingMap = (new Collection($map))
  339. ->match(['matching' => true])
  340. ->indexBy('alias')
  341. ->toArray();
  342. $this->_containMap = (new Collection(array_reverse($map)))
  343. ->match(['matching' => false])
  344. ->indexBy('nestKey')
  345. ->toArray();
  346. }
  347. /**
  348. * Creates a map of row keys out of the query select clause that can be
  349. * used to hydrate nested result sets more quickly.
  350. *
  351. * @param \Cake\ORM\Query $query The query from where to derive the column map
  352. * @return void
  353. */
  354. protected function _calculateColumnMap($query)
  355. {
  356. $map = [];
  357. foreach ($query->clause('select') as $key => $field) {
  358. $key = trim($key, '"`[]');
  359. if (strpos($key, '__') <= 0) {
  360. $map[$this->_defaultAlias][$key] = $key;
  361. continue;
  362. }
  363. $parts = explode('__', $key, 2);
  364. $map[$parts[0]][$key] = $parts[1];
  365. }
  366. foreach ($this->_matchingMap as $alias => $assoc) {
  367. if (!isset($map[$alias])) {
  368. continue;
  369. }
  370. $this->_matchingMapColumns[$alias] = $map[$alias];
  371. unset($map[$alias]);
  372. }
  373. $this->_map = $map;
  374. }
  375. /**
  376. * Creates a map of Type converter classes for each of the columns that should
  377. * be fetched by this object.
  378. *
  379. * @deprecated 3.2.0 Not used anymore. Type casting is done at the statement level
  380. * @return void
  381. */
  382. protected function _calculateTypeMap()
  383. {
  384. deprecationWarning('ResultSet::_calculateTypeMap() is deprecated, and will be removed in 4.0.0.');
  385. }
  386. /**
  387. * Returns the Type classes for each of the passed fields belonging to the
  388. * table.
  389. *
  390. * @param \Cake\ORM\Table $table The table from which to get the schema
  391. * @param array $fields The fields whitelist to use for fields in the schema.
  392. * @return array
  393. */
  394. protected function _getTypes($table, $fields)
  395. {
  396. $types = [];
  397. $schema = $table->getSchema();
  398. $map = array_keys((array)Type::getMap() + ['string' => 1, 'text' => 1, 'boolean' => 1]);
  399. $typeMap = array_combine(
  400. $map,
  401. array_map(['Cake\Database\Type', 'build'], $map)
  402. );
  403. foreach (['string', 'text'] as $t) {
  404. if (get_class($typeMap[$t]) === 'Cake\Database\Type') {
  405. unset($typeMap[$t]);
  406. }
  407. }
  408. foreach (array_intersect($fields, $schema->columns()) as $col) {
  409. $typeName = $schema->getColumnType($col);
  410. if (isset($typeMap[$typeName])) {
  411. $types[$col] = $typeMap[$typeName];
  412. }
  413. }
  414. return $types;
  415. }
  416. /**
  417. * Helper function to fetch the next result from the statement or
  418. * seeded results.
  419. *
  420. * @return mixed
  421. */
  422. protected function _fetchResult()
  423. {
  424. if (!$this->_statement) {
  425. return false;
  426. }
  427. $row = $this->_statement->fetch('assoc');
  428. if ($row === false) {
  429. return $row;
  430. }
  431. return $this->_groupResult($row);
  432. }
  433. /**
  434. * Correctly nests results keys including those coming from associations
  435. *
  436. * @param array $row Array containing columns and values or false if there is no results
  437. * @return array Results
  438. */
  439. protected function _groupResult($row)
  440. {
  441. $defaultAlias = $this->_defaultAlias;
  442. $results = $presentAliases = [];
  443. $options = [
  444. 'useSetters' => false,
  445. 'markClean' => true,
  446. 'markNew' => false,
  447. 'guard' => false
  448. ];
  449. foreach ($this->_matchingMapColumns as $alias => $keys) {
  450. $matching = $this->_matchingMap[$alias];
  451. $results['_matchingData'][$alias] = array_combine(
  452. $keys,
  453. array_intersect_key($row, $keys)
  454. );
  455. if ($this->_hydrate) {
  456. /* @var \Cake\ORM\Table $table */
  457. $table = $matching['instance'];
  458. $options['source'] = $table->getRegistryAlias();
  459. /* @var \Cake\Datasource\EntityInterface $entity */
  460. $entity = new $matching['entityClass']($results['_matchingData'][$alias], $options);
  461. $results['_matchingData'][$alias] = $entity;
  462. }
  463. }
  464. foreach ($this->_map as $table => $keys) {
  465. $results[$table] = array_combine($keys, array_intersect_key($row, $keys));
  466. $presentAliases[$table] = true;
  467. }
  468. unset($presentAliases[$defaultAlias]);
  469. foreach ($this->_containMap as $assoc) {
  470. $alias = $assoc['nestKey'];
  471. if ($assoc['canBeJoined'] && empty($this->_map[$alias])) {
  472. continue;
  473. }
  474. /* @var \Cake\ORM\Association $instance */
  475. $instance = $assoc['instance'];
  476. if (!$assoc['canBeJoined'] && !isset($row[$alias])) {
  477. $results = $instance->defaultRowValue($results, $assoc['canBeJoined']);
  478. continue;
  479. }
  480. if (!$assoc['canBeJoined']) {
  481. $results[$alias] = $row[$alias];
  482. }
  483. $target = $instance->getTarget();
  484. $options['source'] = $target->getRegistryAlias();
  485. unset($presentAliases[$alias]);
  486. if ($assoc['canBeJoined'] && $this->_autoFields !== false) {
  487. $hasData = false;
  488. foreach ($results[$alias] as $v) {
  489. if ($v !== null && $v !== []) {
  490. $hasData = true;
  491. break;
  492. }
  493. }
  494. if (!$hasData) {
  495. $results[$alias] = null;
  496. }
  497. }
  498. if ($this->_hydrate && $results[$alias] !== null && $assoc['canBeJoined']) {
  499. $entity = new $assoc['entityClass']($results[$alias], $options);
  500. $results[$alias] = $entity;
  501. }
  502. $results = $instance->transformRow($results, $alias, $assoc['canBeJoined'], $assoc['targetProperty']);
  503. }
  504. foreach ($presentAliases as $alias => $present) {
  505. if (!isset($results[$alias])) {
  506. continue;
  507. }
  508. $results[$defaultAlias][$alias] = $results[$alias];
  509. }
  510. if (isset($results['_matchingData'])) {
  511. $results[$defaultAlias]['_matchingData'] = $results['_matchingData'];
  512. }
  513. $options['source'] = $this->_defaultTable->getRegistryAlias();
  514. if (isset($results[$defaultAlias])) {
  515. $results = $results[$defaultAlias];
  516. }
  517. if ($this->_hydrate && !($results instanceof EntityInterface)) {
  518. $results = new $this->_entityClass($results, $options);
  519. }
  520. return $results;
  521. }
  522. /**
  523. * Casts all values from a row brought from a table to the correct
  524. * PHP type.
  525. *
  526. * @param string $alias The table object alias
  527. * @param array $values The values to cast
  528. * @deprecated 3.2.0 Not used anymore. Type casting is done at the statement level
  529. * @return array
  530. */
  531. protected function _castValues($alias, $values)
  532. {
  533. deprecationWarning('ResultSet::_castValues() is deprecated, and will be removed in 4.0.0.');
  534. return $values;
  535. }
  536. /**
  537. * Returns an array that can be used to describe the internal state of this
  538. * object.
  539. *
  540. * @return array
  541. */
  542. public function __debugInfo()
  543. {
  544. return [
  545. 'items' => $this->toArray(),
  546. ];
  547. }
  548. }