ResultSet.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.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. $repository = $query->repository();
  157. $this->_statement = $statement;
  158. $this->_driver = $query->connection()->driver();
  159. $this->_defaultTable = $query->repository();
  160. $this->_calculateAssociationMap($query);
  161. $this->_hydrate = $query->hydrate();
  162. $this->_entityClass = $repository->entityClass();
  163. $this->_useBuffering = $query->bufferResults();
  164. $this->_defaultAlias = $this->_defaultTable->alias();
  165. $this->_calculateColumnMap($query);
  166. $this->_calculateTypeMap();
  167. $this->_autoFields = $query->autoFields();
  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. while ($this->valid()) {
  280. $this->next();
  281. }
  282. return serialize($this->_results);
  283. }
  284. /**
  285. * Unserializes a resultset.
  286. *
  287. * Part of Serializable interface.
  288. *
  289. * @param string $serialized Serialized object
  290. * @return void
  291. */
  292. public function unserialize($serialized)
  293. {
  294. $this->_results = unserialize($serialized);
  295. $this->_useBuffering = true;
  296. $this->_count = count($this->_results);
  297. }
  298. /**
  299. * Gives the number of rows in the result set.
  300. *
  301. * Part of the Countable interface.
  302. *
  303. * @return int
  304. */
  305. public function count()
  306. {
  307. if ($this->_count !== null) {
  308. return $this->_count;
  309. }
  310. if ($this->_statement) {
  311. return $this->_count = $this->_statement->rowCount();
  312. }
  313. return $this->_count = count($this->_results);
  314. }
  315. /**
  316. * Calculates the list of associations that should get eager loaded
  317. * when fetching each record
  318. *
  319. * @param \Cake\ORM\Query $query The query from where to derive the associations
  320. * @return void
  321. */
  322. protected function _calculateAssociationMap($query)
  323. {
  324. $map = $query->eagerLoader()->associationsMap($this->_defaultTable);
  325. $this->_matchingMap = (new Collection($map))
  326. ->match(['matching' => true])
  327. ->indexBy('alias')
  328. ->toArray();
  329. $this->_containMap = (new Collection(array_reverse($map)))
  330. ->match(['matching' => false])
  331. ->indexBy('nestKey')
  332. ->toArray();
  333. }
  334. /**
  335. * Creates a map of row keys out of the query select clause that can be
  336. * used to hydrate nested result sets more quickly.
  337. *
  338. * @param \Cake\ORM\Query $query The query from where to derive the column map
  339. * @return void
  340. */
  341. protected function _calculateColumnMap($query)
  342. {
  343. $map = [];
  344. foreach ($query->clause('select') as $key => $field) {
  345. $key = trim($key, '"`[]');
  346. if (strpos($key, '__') <= 0) {
  347. $map[$this->_defaultAlias][$key] = $key;
  348. continue;
  349. }
  350. $parts = explode('__', $key, 2);
  351. $map[$parts[0]][$key] = $parts[1];
  352. }
  353. foreach ($this->_matchingMap as $alias => $assoc) {
  354. if (!isset($map[$alias])) {
  355. continue;
  356. }
  357. $this->_matchingMapColumns[$alias] = $map[$alias];
  358. unset($map[$alias]);
  359. }
  360. $this->_map = $map;
  361. }
  362. /**
  363. * Creates a map of Type converter classes for each of the columns that should
  364. * be fetched by this object.
  365. *
  366. * @deprecated 3.2.0 Not used anymore. Type casting is done at the statement level
  367. * @return void
  368. */
  369. protected function _calculateTypeMap()
  370. {
  371. }
  372. /**
  373. * Returns the Type classes for each of the passed fields belonging to the
  374. * table.
  375. *
  376. * @param \Cake\ORM\Table $table The table from which to get the schema
  377. * @param array $fields The fields whitelist to use for fields in the schema.
  378. * @return array
  379. */
  380. protected function _getTypes($table, $fields)
  381. {
  382. $types = [];
  383. $schema = $table->schema();
  384. $map = array_keys(Type::map() + ['string' => 1, 'text' => 1, 'boolean' => 1]);
  385. $typeMap = array_combine(
  386. $map,
  387. array_map(['Cake\Database\Type', 'build'], $map)
  388. );
  389. foreach (['string', 'text'] as $t) {
  390. if (get_class($typeMap[$t]) === 'Cake\Database\Type') {
  391. unset($typeMap[$t]);
  392. }
  393. }
  394. foreach (array_intersect($fields, $schema->columns()) as $col) {
  395. $typeName = $schema->columnType($col);
  396. if (isset($typeMap[$typeName])) {
  397. $types[$col] = $typeMap[$typeName];
  398. }
  399. }
  400. return $types;
  401. }
  402. /**
  403. * Helper function to fetch the next result from the statement or
  404. * seeded results.
  405. *
  406. * @return mixed
  407. */
  408. protected function _fetchResult()
  409. {
  410. if (!$this->_statement) {
  411. return false;
  412. }
  413. $row = $this->_statement->fetch('assoc');
  414. if ($row === false) {
  415. return $row;
  416. }
  417. return $this->_groupResult($row);
  418. }
  419. /**
  420. * Correctly nests results keys including those coming from associations
  421. *
  422. * @param mixed $row Array containing columns and values or false if there is no results
  423. * @return array Results
  424. */
  425. protected function _groupResult($row)
  426. {
  427. $defaultAlias = $this->_defaultAlias;
  428. $results = $presentAliases = [];
  429. $options = [
  430. 'useSetters' => false,
  431. 'markClean' => true,
  432. 'markNew' => false,
  433. 'guard' => false
  434. ];
  435. foreach ($this->_matchingMapColumns as $alias => $keys) {
  436. $matching = $this->_matchingMap[$alias];
  437. $results['_matchingData'][$alias] = array_combine(
  438. $keys,
  439. array_intersect_key($row, $keys)
  440. );
  441. if ($this->_hydrate) {
  442. $options['source'] = $matching['instance']->registryAlias();
  443. $entity = new $matching['entityClass']($results['_matchingData'][$alias], $options);
  444. $entity->clean();
  445. $results['_matchingData'][$alias] = $entity;
  446. }
  447. }
  448. foreach ($this->_map as $table => $keys) {
  449. $results[$table] = array_combine($keys, array_intersect_key($row, $keys));
  450. $presentAliases[$table] = true;
  451. }
  452. unset($presentAliases[$defaultAlias]);
  453. foreach ($this->_containMap as $assoc) {
  454. $alias = $assoc['nestKey'];
  455. if ($assoc['canBeJoined'] && empty($this->_map[$alias])) {
  456. continue;
  457. }
  458. $instance = $assoc['instance'];
  459. if (!$assoc['canBeJoined'] && !isset($row[$alias])) {
  460. $results = $instance->defaultRowValue($results, $assoc['canBeJoined']);
  461. continue;
  462. }
  463. if (!$assoc['canBeJoined']) {
  464. $results[$alias] = $row[$alias];
  465. }
  466. $target = $instance->target();
  467. $options['source'] = $target->registryAlias();
  468. unset($presentAliases[$alias]);
  469. if ($assoc['canBeJoined'] && $this->_autoFields !== false) {
  470. $hasData = false;
  471. foreach ($results[$alias] as $v) {
  472. if ($v !== null && $v !== []) {
  473. $hasData = true;
  474. break;
  475. }
  476. }
  477. if (!$hasData) {
  478. $results[$alias] = null;
  479. }
  480. }
  481. if ($this->_hydrate && $results[$alias] !== null && $assoc['canBeJoined']) {
  482. $entity = new $assoc['entityClass']($results[$alias], $options);
  483. $entity->clean();
  484. $results[$alias] = $entity;
  485. }
  486. $results = $instance->transformRow($results, $alias, $assoc['canBeJoined'], $assoc['targetProperty']);
  487. }
  488. foreach ($presentAliases as $alias => $present) {
  489. if (!isset($results[$alias])) {
  490. continue;
  491. }
  492. $results[$defaultAlias][$alias] = $results[$alias];
  493. }
  494. if (isset($results['_matchingData'])) {
  495. $results[$defaultAlias]['_matchingData'] = $results['_matchingData'];
  496. }
  497. $options['source'] = $this->_defaultTable->registryAlias();
  498. if (isset($results[$defaultAlias])) {
  499. $results = $results[$defaultAlias];
  500. }
  501. if ($this->_hydrate && !($results instanceof EntityInterface)) {
  502. $results = new $this->_entityClass($results, $options);
  503. }
  504. return $results;
  505. }
  506. /**
  507. * Casts all values from a row brought from a table to the correct
  508. * PHP type.
  509. *
  510. * @param string $alias The table object alias
  511. * @param array $values The values to cast
  512. * @deprecated 3.2.0 Not used anymore. Type casting is done at the statement level
  513. * @return array
  514. */
  515. protected function _castValues($alias, $values)
  516. {
  517. return $values;
  518. }
  519. /**
  520. * Returns an array that can be used to describe the internal state of this
  521. * object.
  522. *
  523. * @return array
  524. */
  525. public function __debugInfo()
  526. {
  527. return [
  528. 'items' => $this->toArray(),
  529. ];
  530. }
  531. }