Collection.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  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;
  9. /**
  10. * The `Collection` class extends the generic `lithium\util\Collection` class to provide
  11. * context-specific features for working with sets of data persisted by a backend data store. This
  12. * is a general abstraction that operates on arbitrary sets of data from either relational or
  13. * non-relational data stores.
  14. */
  15. abstract class Collection extends \lithium\util\Collection {
  16. /**
  17. * A reference to this object's parent `Document` object.
  18. *
  19. * @var object
  20. */
  21. protected $_parent = null;
  22. /**
  23. * If this `Collection` instance has a parent document (see `$_parent`), this value indicates
  24. * the key name of the parent document that contains it.
  25. *
  26. * @see lithium\data\Collection::$_parent
  27. * @var string
  28. */
  29. protected $_pathKey = null;
  30. /**
  31. * The fully-namespaced class name of the model object to which this entity set is bound. This
  32. * is usually the model that executed the query which created this object.
  33. *
  34. * @var string
  35. */
  36. protected $_model = null;
  37. /**
  38. * A reference to the query object that originated this entity set; usually an instance of
  39. * `lithium\data\model\Query`.
  40. *
  41. * @see lithium\data\model\Query
  42. * @var object
  43. */
  44. protected $_query = null;
  45. /**
  46. * A pointer or resource that is used to load entities from the backend data source that
  47. * originated this collection.
  48. *
  49. * @var resource
  50. */
  51. protected $_result = null;
  52. /**
  53. * Indicates whether the current position is valid or not. This overrides the default value of
  54. * the parent class.
  55. *
  56. * @var boolean
  57. * @see lithium\util\Collection::valid()
  58. */
  59. protected $_valid = true;
  60. /**
  61. * Contains an array of backend-specific statistics generated by the query that produced this
  62. * `Collection` object. These stats are accessible via the `stats()` method.
  63. *
  64. * @see lithium\data\Collection::stats()
  65. * @var array
  66. */
  67. protected $_stats = array();
  68. /**
  69. * Setted to `true` when the collection has begun iterating.
  70. * @var integer
  71. */
  72. protected $_started = false;
  73. /**
  74. * Indicates whether this array was part of a document loaded from a data source, or is part of
  75. * a new document, or is in newly-added field of an existing document.
  76. *
  77. * @var boolean
  78. */
  79. protected $_exists = false;
  80. /**
  81. * If the `Collection` has a schema object assigned (rather than loading one from a model), it
  82. * will be assigned here.
  83. *
  84. * @see lithium\data\Schema
  85. * @var lithium\data\Schema
  86. */
  87. protected $_schema = null;
  88. /**
  89. * Holds an array of values that should be processed on initialization.
  90. *
  91. * @var array
  92. */
  93. protected $_autoConfig = array(
  94. 'model', 'result', 'query', 'parent', 'stats', 'pathKey', 'exists', 'schema'
  95. );
  96. /**
  97. * Class constructor.
  98. *
  99. * @param array $config
  100. */
  101. public function __construct(array $config = array()) {
  102. $defaults = array('data' => array(), 'model' => null);
  103. parent::__construct($config + $defaults);
  104. }
  105. protected function _init() {
  106. $data = $this->_config['data'];
  107. parent::_init();
  108. $this->set($data);
  109. foreach (array('classes', 'model', 'result', 'query') as $key) {
  110. unset($this->_config[$key]);
  111. }
  112. }
  113. /**
  114. * Configures protected properties of a `Collection` so that it is parented to `$parent`.
  115. *
  116. * @param object $parent
  117. * @param array $config
  118. * @return void
  119. */
  120. public function assignTo($parent, array $config = array()) {
  121. foreach ($config as $key => $val) {
  122. $this->{'_' . $key} = $val;
  123. }
  124. $this->_parent =& $parent;
  125. }
  126. /**
  127. * Returns the model which this particular collection is based off of.
  128. *
  129. * @return string The fully qualified model class name.
  130. */
  131. public function model() {
  132. return $this->_model;
  133. }
  134. /**
  135. * Returns the object's parent `Document` object.
  136. *
  137. * @return object
  138. */
  139. public function parent() {
  140. return $this->_parent;
  141. }
  142. /**
  143. * A flag indicating whether or not the items of this collection exists.
  144. *
  145. * @return boolean `True` if exists, `false` otherwise.
  146. */
  147. public function exists() {
  148. return $this->_exists;
  149. }
  150. public function schema($field = null) {
  151. $schema = null;
  152. switch (true) {
  153. case ($this->_schema):
  154. $schema = $this->_schema;
  155. break;
  156. case ($model = $this->_model):
  157. $schema = $model::schema();
  158. break;
  159. }
  160. if ($schema) {
  161. return $field ? $schema->fields($field) : $schema;
  162. }
  163. }
  164. /**
  165. * Allows several properties to be assigned at once.
  166. *
  167. * For example:
  168. * {{{
  169. * $collection->set(array('title' => 'Lorem Ipsum', 'value' => 42));
  170. * }}}
  171. *
  172. * @param $values An associative array of fields and values to assign to the `Collection`.
  173. * @return void
  174. */
  175. public function set($values) {
  176. foreach ($values as $key => $val) {
  177. $this[$key] = $val;
  178. }
  179. }
  180. /**
  181. * Returns a boolean indicating whether an offset exists for the
  182. * current `Collection`.
  183. *
  184. * @param string $offset String or integer indicating the offset or
  185. * index of an entity in the set.
  186. * @return boolean Result.
  187. */
  188. public function offsetExists($offset) {
  189. $this->offsetGet($offset);
  190. return array_key_exists($offset, $this->_data);
  191. }
  192. /**
  193. * Gets an `Entity` object using PHP's array syntax, i.e. `$documents[3]` or `$records[5]`.
  194. *
  195. * @param mixed $offset The offset.
  196. * @return mixed Returns an `Entity` object if exists otherwise returns `null`.
  197. */
  198. public function offsetGet($offset) {
  199. while (!array_key_exists($offset, $this->_data) && $this->_populate()) {}
  200. if (array_key_exists($offset, $this->_data)) {
  201. return $this->_data[$offset];
  202. }
  203. return null;
  204. }
  205. /**
  206. * Adds the specified object to the `Collection` instance, and assigns associated metadata to
  207. * the added object.
  208. *
  209. * @param string $offset The offset to assign the value to.
  210. * @param mixed $data The entity object to add.
  211. * @return mixed Returns the set `Entity` object.
  212. */
  213. public function offsetSet($offset, $data) {
  214. $this->offsetGet($offset);
  215. return $this->_set($data, $offset);
  216. }
  217. /**
  218. * Unsets an offset.
  219. *
  220. * @param integer $offset The offset to unset.
  221. */
  222. public function offsetUnset($offset) {
  223. $this->offsetGet($offset);
  224. prev($this->_data);
  225. if (key($this->_data) === null) {
  226. $this->rewind();
  227. }
  228. unset($this->_data[$offset]);
  229. }
  230. /**
  231. * Rewinds the collection to the beginning.
  232. */
  233. public function rewind() {
  234. $this->_started = true;
  235. reset($this->_data);
  236. $this->_valid = !empty($this->_data) || !is_null($this->_populate());
  237. return current($this->_data);
  238. }
  239. /**
  240. * Returns the currently pointed to record's unique key.
  241. *
  242. * @param boolean $full If true, returns the complete key.
  243. * @return mixed
  244. */
  245. public function key($full = false) {
  246. if ($this->_started === false) {
  247. $this->current();
  248. }
  249. if ($this->_valid) {
  250. $key = key($this->_data);
  251. return (is_array($key) && !$full) ? reset($key) : $key;
  252. }
  253. return null;
  254. }
  255. /**
  256. * Returns the item keys.
  257. *
  258. * @return array The keys of the items.
  259. */
  260. public function keys() {
  261. $this->offsetGet(null);
  262. return parent::keys();
  263. }
  264. /**
  265. * Returns the currently pointed to record in the set.
  266. *
  267. * @return object `Record`
  268. */
  269. public function current() {
  270. if (!$this->_started) {
  271. $this->rewind();
  272. }
  273. if (!$this->_valid) {
  274. return false;
  275. }
  276. return current($this->_data);
  277. }
  278. /**
  279. * Returns the next document in the set, and advances the object's internal pointer. If the end
  280. * of the set is reached, a new document will be fetched from the data source connection handle
  281. * If no more documents can be fetched, returns `null`.
  282. *
  283. * @return mixed Returns the next document in the set, or `false`, if no more documents are
  284. * available.
  285. */
  286. public function next() {
  287. if (!$this->_started) {
  288. $this->rewind();
  289. }
  290. next($this->_data);
  291. $this->_valid = !(key($this->_data) === null);
  292. if (!$this->_valid) {
  293. $this->_valid = !is_null($this->_populate());
  294. }
  295. return current($this->_data);
  296. }
  297. /**
  298. * Checks if current position is valid.
  299. *
  300. * @return boolean `true` if valid, `false` otherwise.
  301. */
  302. public function valid() {
  303. if (!$this->_started) {
  304. $this->rewind();
  305. }
  306. return $this->_valid;
  307. }
  308. /**
  309. * Overrides parent `find()` implementation to enable key/value-based filtering of entity
  310. * objects contained in this collection.
  311. *
  312. * @param mixed $filter Callback to use for filtering, or array of key/value pairs which entity
  313. * properties will be matched against.
  314. * @param array $options Options to modify the behavior of this method. See the documentation
  315. * for the `$options` parameter of `lithium\util\Collection::find()`.
  316. * @return mixed The filtered items. Will be an array unless `'collect'` is defined in the
  317. * `$options` argument, then an instance of this class will be returned.
  318. */
  319. public function find($filter, array $options = array()) {
  320. $this->offsetGet(null);
  321. if (is_array($filter)) {
  322. $filter = $this->_filterFromArray($filter);
  323. }
  324. return parent::find($filter, $options);
  325. }
  326. /**
  327. * Overrides parent `first()` implementation to enable key/value-based filtering.
  328. *
  329. * @param mixed $filter In addition to a callback (see parent), can also be an array where the
  330. * keys and values must match the property values of the objects being inspected.
  331. * @return object Returns the first object found matching the filter criteria.
  332. */
  333. public function first($filter = null) {
  334. return parent::first(is_array($filter) ? $this->_filterFromArray($filter) : $filter);
  335. }
  336. /**
  337. * Creates a filter based on an array of key/value pairs that must match the items in a
  338. * `Collection`.
  339. *
  340. * @param array $filter An array of key/value pairs used to filter `Collection` items.
  341. * @return closure Returns a closure that wraps the array and attempts to match each value
  342. * against `Collection` item properties.
  343. */
  344. protected function _filterFromArray(array $filter) {
  345. return function($item) use ($filter) {
  346. foreach ($filter as $key => $val) {
  347. if ($item->{$key} != $val) {
  348. return false;
  349. }
  350. }
  351. return true;
  352. };
  353. }
  354. /**
  355. * Returns meta information for this `Collection`.
  356. *
  357. * @return array
  358. */
  359. public function meta() {
  360. return array('model' => $this->_model);
  361. }
  362. /**
  363. * Applies a callback to all data in the collection.
  364. *
  365. * Overridden to load any data that has not yet been loaded.
  366. *
  367. * @param callback $filter The filter to apply.
  368. * @return object This collection instance.
  369. */
  370. public function each($filter) {
  371. $this->offsetGet(null);
  372. return parent::each($filter);
  373. }
  374. /**
  375. * Applies a callback to a copy of all data in the collection
  376. * and returns the result.
  377. *
  378. * Overriden to load any data that has not yet been loaded.
  379. *
  380. * @param callback $filter The filter to apply.
  381. * @param array $options The available options are:
  382. * - `'collect'`: If `true`, the results will be returned wrapped
  383. * in a new `Collection` object or subclass.
  384. * @return object The filtered data.
  385. */
  386. public function map($filter, array $options = array()) {
  387. $defaults = array('collect' => true);
  388. $options += $defaults;
  389. $this->offsetGet(null);
  390. $data = parent::map($filter, $options);
  391. if ($options['collect']) {
  392. foreach (array('_model', '_schema', '_pathKey') as $key) {
  393. $data->{$key} = $this->{$key};
  394. }
  395. }
  396. return $data;
  397. }
  398. /**
  399. * Reduce, or fold, a collection down to a single value
  400. *
  401. * Overridden to load any data that has not yet been loaded.
  402. *
  403. * @param callback $filter The filter to apply.
  404. * @param mixed $initial Initial value
  405. * @return mixed A single reduced value
  406. */
  407. public function reduce($filter, $initial = false) {
  408. if (!$this->closed()) {
  409. while ($this->next()) {}
  410. }
  411. return parent::reduce($filter);
  412. }
  413. /**
  414. * Sorts the objects in the collection, useful in situations where
  415. * you are already using the underlying datastore to sort results.
  416. *
  417. * Overriden to load any data that has not yet been loaded.
  418. *
  419. * @param mixed $field The field to sort the data on, can also be a callback
  420. * to a custom sort function.
  421. * @param array $options The available options are:
  422. * - No options yet implemented
  423. * @return $this, useful for chaining this with other methods.
  424. */
  425. public function sort($field = 'id', array $options = array()) {
  426. $this->offsetGet(null);
  427. if (is_string($field)) {
  428. $sorter = function ($a, $b) use ($field) {
  429. if (is_array($a)) {
  430. $a = (object) $a;
  431. }
  432. if (is_array($b)) {
  433. $b = (object) $b;
  434. }
  435. return strcmp($a->$field, $b->$field);
  436. };
  437. } else if (is_callable($field)) {
  438. $sorter = $field;
  439. }
  440. return parent::sort($sorter, $options);
  441. }
  442. /**
  443. * Converts the current state of the data structure to an array.
  444. *
  445. * @return array Returns the array value of the data in this `Collection`.
  446. */
  447. public function data() {
  448. return $this->to('array', array('indexed' => null));
  449. }
  450. /**
  451. * Converts a `Collection` object to another type of object, or a simple type such as an array.
  452. * The supported values of `$format` depend on the format handlers registered in the static
  453. * property `Collection::$_formats`. The `Collection` class comes with built-in support for
  454. * array conversion, but other formats may be registered.
  455. *
  456. * Once the appropriate handlers are registered, a `Collection` instance can be converted into
  457. * any handler-supported format, i.e.: {{{
  458. * $collection->to('json'); // returns a JSON string
  459. * $collection->to('xml'); // returns an XML string
  460. * }}}
  461. *
  462. * _Please note that Lithium does not ship with a default XML handler, but one can be
  463. * configured easily._
  464. *
  465. * @see lithium\util\Collection::formats()
  466. * @see lithium\util\Collection::$_formats
  467. * @param string $format By default the only supported value is `'array'`. However, additional
  468. * format handlers can be registered using the `formats()` method.
  469. * @param array $options Options for converting this collection:
  470. * - `'internal'` _boolean_: Indicates whether the current internal representation of the
  471. * collection should be exported. Defaults to `false`, which uses the standard iterator
  472. * interfaces. This is useful for exporting record sets, where records are lazy-loaded,
  473. * and the collection must be iterated in order to fetch all objects.
  474. * @return mixed The object converted to the value specified in `$format`; usually an array or
  475. * string.
  476. */
  477. public function to($format, array $options = array()) {
  478. $defaults = array('internal' => false, 'indexed' => true);
  479. $options += $defaults;
  480. $this->offsetGet(null);
  481. $index = $options['indexed'] || ($options['indexed'] === null && $this->_parent === null);
  482. if (!$index) {
  483. $data = array_values($this->_data);
  484. } else {
  485. $data = $options['internal'] ? $this->_data : $this;
  486. }
  487. return $this->_to($format, $data, $options);
  488. }
  489. /**
  490. * Return's the pointer or resource that is used to load entities from the backend
  491. * data source that originated this collection. This is useful in many cases for
  492. * additional methods related to debugging queries.
  493. *
  494. * @return object The pointer or resource from the data source
  495. */
  496. public function result() {
  497. return $this->_result;
  498. }
  499. /**
  500. * Gets the stat or stats associated with this `Collection`.
  501. *
  502. * @param string $name Stat name.
  503. * @return mixed Single stat if `$name` supplied, else all stats for this
  504. * `Collection`.
  505. */
  506. public function stats($name = null) {
  507. if ($name) {
  508. return isset($this->_stats[$name]) ? $this->_stats[$name] : null;
  509. }
  510. return $this->_stats;
  511. }
  512. /**
  513. * Executes when the associated result resource pointer reaches the end of its data set. The
  514. * resource is freed by the connection, and the reference to the connection is unlinked.
  515. *
  516. * @return void
  517. */
  518. public function close() {
  519. if (!empty($this->_result)) {
  520. unset($this->_result);
  521. $this->_result = null;
  522. }
  523. }
  524. /**
  525. * Checks to see if this entity has already fetched all available entities and freed the
  526. * associated result resource.
  527. *
  528. * @return boolean Returns true if all entities are loaded and the database resources have been
  529. * freed, otherwise returns false.
  530. */
  531. public function closed() {
  532. return empty($this->_result);
  533. }
  534. /**
  535. * Ensures that the data set's connection is closed when the object is destroyed.
  536. *
  537. * @return void
  538. */
  539. public function __destruct() {
  540. $this->close();
  541. }
  542. /**
  543. * A method to be implemented by concrete `Collection` classes which, provided a reference to a
  544. * backend data source, and a resource representing a query result cursor, fetches new result
  545. * data and wraps it in the appropriate object type, which is added into the `Collection` and
  546. * returned.
  547. *
  548. * @return mixed Returns the next `Record`, `Document` object or other `Entity` object if
  549. * exists. Returns `null` otherwise.
  550. */
  551. abstract protected function _populate();
  552. /**
  553. * A method to be implemented by concrete `Collection` classes which sets data to a specified
  554. * offset and wraps all data array in its appropriate object type.
  555. *
  556. * @see lithium\data\Collection::_populate()
  557. * @see lithium\data\Collection::_offsetSet()
  558. *
  559. * @param mixed $data An array or an `Entity` object to set.
  560. * @param mixed $offset The offset. If offset is `null` data is simply appended to the set.
  561. * @param array $options Any additional options to pass to the `Entity`'s constructor.
  562. * @return object Returns the inserted `Record`, `Document` object or other `Entity` object.
  563. */
  564. abstract protected function _set($data = null, $offset = null, $options = array());
  565. }
  566. ?>