MultiKeyRecordSet.php 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2013, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\data\collection;
  9. class MultiKeyRecordSet extends \lithium\data\collection\RecordSet {
  10. /**
  11. * An array containing each record's unique key. This allows, for example, lookups of records
  12. * with composite keys, i.e.:
  13. *
  14. * {{{
  15. * $payment = $records[array('client_id' => 42, 'invoice_id' => 21)];
  16. * }}}
  17. *
  18. * @var array
  19. */
  20. protected $_index = array();
  21. /**
  22. * A 2D array of column-mapping information, where the top-level key is the fully-namespaced
  23. * model name, and the sub-arrays are column names.
  24. *
  25. * @var array
  26. */
  27. protected $_columns = array();
  28. /**
  29. * Initializes the record set and uses the database connection to get the column list contained
  30. * in the query that created this object.
  31. *
  32. * @see lithium\data\collection\RecordSet::$_columns
  33. * @return void
  34. * @todo The part that uses _handle->schema() should be rewritten so that the column list
  35. * is coming from the query object.
  36. */
  37. protected function _init() {
  38. parent::_init();
  39. if ($this->_result) {
  40. $this->_columns = $this->_columnMap();
  41. }
  42. }
  43. /**
  44. * Checks to see if a record with the given index key is in the record set. If the record
  45. * cannot be found, and not all records have been loaded into the set, it will continue loading
  46. * records until either all available records have been loaded, or a matching key has been
  47. * found.
  48. *
  49. * @see lithium\data\collection\RecordSet::offsetGet()
  50. * @param mixed $offset The ID of the record to check for.
  51. * @return boolean Returns true if the record's ID is found in the set, otherwise false.
  52. */
  53. public function offsetExists($offset) {
  54. $offset = (!$offset || $offset === true) ? 0 : $offset;
  55. $this->offsetGet($offset);
  56. if (in_array($offset, $this->_index)) {
  57. return true;
  58. }
  59. return false;
  60. }
  61. /**
  62. * Gets a record from the record set using PHP's array syntax, i.e. `$records[5]`. Using loose
  63. * typing, integer keys can be accessed using strings and vice-versa. For record sets with
  64. * composite keys, records may be accessed using arrays as array keys. Note that the order of
  65. * the keys in the array does not matter.
  66. *
  67. * Because record data in `RecordSet` is lazy-loaded from the database, new records are fetched
  68. * until one with a matching key is found.
  69. *
  70. * @see lithium\data\collection\RecordSet::$_index
  71. * @param mixed $offset The offset, or ID (index) of the record you wish to load. If
  72. * `$offset` is `null`, all records are loaded into the record set, and
  73. * `offsetGet` returns `null`.
  74. * @return object Returns a `Record` object if a record is found with a key that matches the
  75. * value of `$offset`, otheriwse returns `null`.
  76. */
  77. public function offsetGet($offset) {
  78. $offset = (!$offset || $offset === true) ? 0 : $offset;
  79. if (in_array($offset, $this->_index)) {
  80. return $this->_data[array_search($offset, $this->_index)];
  81. }
  82. if ($this->closed()) {
  83. return null;
  84. }
  85. if ($model = $this->_model) {
  86. $offsetKey = $model::key($offset);
  87. while ($record = $this->_populate($offset)) {
  88. $curKey = $model::key($record);
  89. $keySet = $offsetKey == $curKey;
  90. if (!is_null($offset) && $keySet) {
  91. return $record;
  92. }
  93. }
  94. }
  95. $this->close();
  96. }
  97. /**
  98. * Assigns a value to the specified offset.
  99. *
  100. * @param integer $offset The offset to assign the value to.
  101. * @param mixed $data The value to set.
  102. * @return mixed The value which was set.
  103. */
  104. public function offsetUnset($offset) {
  105. $offset = (!$offset || $offset === true) ? 0 : $offset;
  106. $this->offsetGet($offset);
  107. unset($this->_index[$index = array_search($offset, $this->_index)]);
  108. prev($this->_data);
  109. if (key($this->_data) === null) {
  110. $this->rewind();
  111. }
  112. unset($this->_data[$index]);
  113. }
  114. /**
  115. * Returns the currently pointed to record's unique key.
  116. *
  117. * @param boolean $full If true, returns the complete key.
  118. * @return mixed
  119. */
  120. public function key($full = false) {
  121. if ($this->_started === false) {
  122. $this->current();
  123. }
  124. if ($this->_valid) {
  125. $key = $this->_index[key($this->_data)];
  126. return (is_array($key) && !$full) ? reset($key) : $key;
  127. }
  128. return null;
  129. }
  130. /**
  131. * Returns the item keys.
  132. *
  133. * @return array The keys of the items.
  134. */
  135. public function keys() {
  136. $this->offsetGet(null);
  137. return $this->_index;
  138. }
  139. /**
  140. * Converts the data in the record set to a different format, i.e. an array.
  141. *
  142. * @param string $format
  143. * @param array $options
  144. * @return mixed
  145. */
  146. public function to($format, array $options = array()) {
  147. $default = array('indexed' => true);
  148. $options += $default;
  149. $options['internal'] = !$options['indexed'];
  150. unset($options['indexed']);
  151. $this->offsetGet(null);
  152. if (!$options['internal'] && !is_scalar(current($this->_index))) {
  153. $options['internal'] = true;
  154. }
  155. return $result = parent::to($format, $options);
  156. }
  157. /**
  158. * Applies a callback to all data in the collection.
  159. *
  160. * Overriden to load any data that has not yet been loaded.
  161. *
  162. * @param callback $filter The filter to apply.
  163. * @return object This collection instance.
  164. */
  165. public function each($filter) {
  166. $this->offsetGet(null);
  167. return parent::each($filter);
  168. }
  169. /**
  170. * Filters a copy of the items in the collection.
  171. *
  172. * Overridden to load any data that has not yet been loaded.
  173. *
  174. * @param callback $filter Callback to use for filtering.
  175. * @param array $options The available options are:
  176. * - `'collect'`: If `true`, the results will be returned wrapped
  177. * in a new `Collection` object or subclass.
  178. * @return mixed The filtered items. Will be an array unless `'collect'` is defined in the
  179. * `$options` argument, then an instance of this class will be returned.
  180. */
  181. public function find($filter, array $options = array()) {
  182. $this->offsetGet(null);
  183. return parent::find($filter, $options);
  184. }
  185. /**
  186. * Applies a callback to a copy of all data in the collection
  187. * and returns the result.
  188. *
  189. * Overriden to load any data that has not yet been loaded.
  190. *
  191. * @param callback $filter The filter to apply.
  192. * @param array $options The available options are:
  193. * - `'collect'`: If `true`, the results will be returned wrapped
  194. * in a new `Collection` object or subclass.
  195. * @return object The filtered data.
  196. */
  197. public function map($filter, array $options = array()) {
  198. $this->offsetGet(null);
  199. return parent::map($filter, $options);
  200. }
  201. /**
  202. * Extract the next item from the result ressource and wraps it into a `Record` object.
  203. *
  204. * @return mixed Returns the next `Record` if exists. Returns `null` otherwise
  205. */
  206. protected function _populate() {
  207. if ($this->closed() || !$this->_result->valid()) {
  208. return;
  209. }
  210. $data = $this->_result->current();
  211. if ($this->_query) {
  212. $data = $this->_mapRecord($data);
  213. }
  214. $result = $this->_set($data, null, array('exists' => true));
  215. $this->_result->next();
  216. return $result;
  217. }
  218. protected function _set($data = null, $offset = null, $options = array()) {
  219. if ($model = $this->_model) {
  220. $data = !is_object($data) ? $model::connection()->item($model, $data, $options) : $data;
  221. $key = $model::key($data);
  222. } else {
  223. $key = $offset;
  224. }
  225. if ($key === array() || $key === null || is_bool($key)) {
  226. $key = count($this->_data);
  227. }
  228. if (is_array($key)) {
  229. $key = count($key) === 1 ? reset($key) : $key;
  230. }
  231. if (in_array($key, $this->_index)) {
  232. $index = array_search($key, $this->_index);
  233. $this->_data[$index] = $data;
  234. return $this->_data[$index];
  235. }
  236. $this->_data[] = $data;
  237. $this->_index[] = $key;
  238. return $data;
  239. }
  240. }
  241. ?>