Document.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  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\entity;
  9. use RuntimeException;
  10. use UnexpectedValueException;
  11. /**
  12. * `Document` is an alternative to the `entity\Record` class, which is optimized for
  13. * organizing collections of entities from document-oriented databases such as CouchDB or MongoDB.
  14. * A `Document` object's fields can represent a collection of both simple and complex data types,
  15. * as well as other `Document` objects. Given the following data (document) structure:
  16. *
  17. * {{{
  18. * {
  19. * _id: 12345.
  20. * name: 'Acme, Inc.',
  21. * employees: {
  22. * 'Larry': { email: '[email protected]' },
  23. * 'Curly': { email: '[email protected]' },
  24. * 'Moe': { email: '[email protected]' }
  25. * }
  26. * }
  27. * }}}
  28. *
  29. * You can query the object as follows:
  30. *
  31. * {{{$acme = Company::find(12345);}}}
  32. *
  33. * This returns a `Document` object, populated with the raw representation of the data.
  34. *
  35. * {{{print_r($acme->to('array'));
  36. *
  37. * // Yields:
  38. * // array(
  39. * // '_id' => 12345,
  40. * // 'name' => 'Acme, Inc.',
  41. * // 'employees' => array(
  42. * // 'Larry' => array('email' => '[email protected]'),
  43. * // 'Curly' => array('email' => '[email protected]'),
  44. * // 'Moe' => array('email' => '[email protected]')
  45. * // )
  46. * //)}}}
  47. *
  48. * As with other database objects, a `Document` exposes its fields as object properties, like so:
  49. *
  50. * {{{echo $acme->name; // echoes 'Acme, Inc.'}}}
  51. *
  52. * However, accessing a field containing a data set will return that data set wrapped in a
  53. * sub-`Document` object., i.e.:
  54. *
  55. * {{{$employees = $acme->employees;
  56. * // returns a Document object with the data in 'employees'}}}
  57. */
  58. class Document extends \lithium\data\Entity implements \Iterator, \ArrayAccess {
  59. /**
  60. * If this `Document` instance has a parent document (see `$_parent`), this value indicates
  61. * the key name of the parent document that contains it.
  62. *
  63. * @see lithium\data\entity\Document::$_parent
  64. * @var string
  65. */
  66. protected $_pathKey = null;
  67. /**
  68. * Contains an array of backend-specific statistics generated by the query that produced this
  69. * `Document` object. These stats are accessible via the `stats()` method.
  70. *
  71. * @see lithium\data\collection\DocumentSet::stats()
  72. * @var array
  73. */
  74. protected $_stats = array();
  75. /**
  76. * Holds the current iteration state. Used by `Document::valid()` to terminate `foreach` loops
  77. * when there are no more fields to iterate over.
  78. *
  79. * @var boolean
  80. */
  81. protected $_valid = false;
  82. /**
  83. * Removed keys list. Contains names of the fields will be removed from the backend data store
  84. *
  85. * @var array
  86. */
  87. protected $_removed = array();
  88. protected function _init() {
  89. parent::_init();
  90. $data = (array) $this->_data;
  91. $this->_data = array();
  92. $this->_updated = array();
  93. $this->_removed = array();
  94. $this->set($data, array('init' => true));
  95. $this->sync(null, array(), array('materialize' => false));
  96. unset($this->_autoConfig);
  97. }
  98. /**
  99. * PHP magic method used when accessing fields as document properties, i.e. `$document->_id`.
  100. *
  101. * @param $name The field name, as specified with an object property.
  102. * @return mixed Returns the value of the field specified in `$name`, and wraps complex data
  103. * types in sub-`Document` objects.
  104. */
  105. public function &__get($name) {
  106. if (strpos($name, '.')) {
  107. return $this->_getNested($name);
  108. }
  109. if (isset($this->_embedded[$name]) && !isset($this->_relationships[$name])) {
  110. throw new RuntimeException("Not implemented.");
  111. }
  112. $result =& parent::__get($name);
  113. if ($result !== null || array_key_exists($name, $this->_updated)) {
  114. return $result;
  115. }
  116. if ($field = $this->schema($name)) {
  117. if (isset($field['default'])) {
  118. $this->set(array($name => $field['default']));
  119. return $this->_updated[$name];
  120. }
  121. if (isset($field['array']) && $field['array'] && ($model = $this->_model)) {
  122. $this->_updated[$name] = $model::connection()->item($model, array(), array(
  123. 'class' => 'set',
  124. 'schema' => $this->schema(),
  125. 'pathKey' => $this->_pathKey ? $this->_pathKey . '.' . $name : $name,
  126. 'parent' => $this,
  127. 'model' => $this->_model
  128. ));
  129. return $this->_updated[$name];
  130. }
  131. }
  132. $null = null;
  133. return $null;
  134. }
  135. public function export(array $options = array()) {
  136. foreach ($this->_updated as $key => $val) {
  137. if ($val instanceof self) {
  138. $path = $this->_pathKey ? "{$this->_pathKey}." : '';
  139. $this->_updated[$key]->_pathKey = "{$path}{$key}";
  140. }
  141. }
  142. return parent::export($options) + array(
  143. 'key' => $this->_pathKey,
  144. 'remove' => $this->_removed
  145. );
  146. }
  147. /**
  148. * Extends the parent implementation to ensure that child documents are properly synced as well.
  149. *
  150. * @param mixed $id
  151. * @param array $data
  152. * @param array $options Options when calling this method:
  153. * - `'recursive'` _boolean_: If `true` attempts to sync nested objects as well.
  154. * Otherwise, only syncs the current object. Defaults to `true`.
  155. * @return void
  156. */
  157. public function sync($id = null, array $data = array(), array $options = array()) {
  158. $defaults = array('recursive' => true);
  159. $options += $defaults;
  160. if (!$options['recursive']) {
  161. return parent::sync($id, $data, $options);
  162. }
  163. foreach ($this->_updated as $key => $val) {
  164. if (is_object($val) && method_exists($val, 'sync')) {
  165. $nested = isset($data[$key]) ? $data[$key] : array();
  166. $this->_updated[$key]->sync(null, $nested, $options);
  167. }
  168. }
  169. parent::sync($id, $data, $options);
  170. }
  171. /**
  172. * Instantiates a new `Document` object as a descendant of the current object, and sets all
  173. * default values and internal state.
  174. *
  175. * @param string $classType The type of class to create, either `'entity'` or `'set'`.
  176. * @param string $key The key name to which the related object is assigned.
  177. * @param array $data The internal data of the related object.
  178. * @param array $options Any other options to pass when instantiating the related object.
  179. * @return object Returns a new `Document` object instance.
  180. */
  181. protected function _relation($classType, $key, $data, $options = array()) {
  182. return parent::_relation($classType, $key, $data, array('exists' => false) + $options);
  183. }
  184. protected function &_getNested($name) {
  185. $current = $this;
  186. $null = null;
  187. $path = explode('.', $name);
  188. $length = count($path) - 1;
  189. foreach ($path as $i => $key) {
  190. if (!isset($current[$key])) {
  191. return $null;
  192. }
  193. $current = $current[$key];
  194. if (is_scalar($current) && $i < $length) {
  195. return $null;
  196. }
  197. }
  198. return $current;
  199. }
  200. /**
  201. * PHP magic method used when setting properties on the `Document` instance, i.e.
  202. * `$document->title = 'Lorem Ipsum'`. If `$value` is a complex data type (i.e. associative
  203. * array), it is wrapped in a sub-`Document` object before being appended.
  204. *
  205. * @param $name The name of the field/property to write to, i.e. `title` in the above example.
  206. * @param $value The value to write, i.e. `'Lorem Ipsum'`.
  207. * @return void
  208. */
  209. public function __set($name, $value = null) {
  210. $this->set(array($name => $value));
  211. }
  212. protected function _setNested($name, $value) {
  213. $current =& $this;
  214. $path = explode('.', $name);
  215. $length = count($path) - 1;
  216. for ($i = 0; $i < $length; $i++) {
  217. $key = $path[$i];
  218. if (isset($current[$key])) {
  219. $next =& $current[$key];
  220. } else {
  221. unset($next);
  222. $next = null;
  223. }
  224. if ($next === null && ($model = $this->_model)) {
  225. $current->set(array($key => $model::connection()->item($model)));
  226. $next =& $current->{$key};
  227. }
  228. $current =& $next;
  229. }
  230. if (is_object($current)) {
  231. $current->set(array(end($path) => $value));
  232. }
  233. }
  234. /**
  235. * PHP magic method used to check the presence of a field as document properties, i.e.
  236. * `$document->_id`.
  237. *
  238. * @param $name The field name, as specified with an object property.
  239. * @return boolean True if the field specified in `$name` exists, false otherwise.
  240. */
  241. public function __isset($name) {
  242. return isset($this->_updated[$name]);
  243. }
  244. /**
  245. * PHP magic method used when unset() is called on a `Document` instance.
  246. * Use case for this would be when you wish to edit a document and remove a field, ie.:
  247. * {{{
  248. * $doc = Post::find($id);
  249. * unset($doc->fieldName);
  250. * $doc->save();
  251. * }}}
  252. *
  253. * @param string $name The name of the field to remove.
  254. * @return void
  255. */
  256. public function __unset($name) {
  257. $parts = explode('.', $name, 2);
  258. if (isset($parts[1])) {
  259. unset($this->{$parts[0]}[$parts[1]]);
  260. } else {
  261. unset($this->_updated[$name]);
  262. $this->_removed[$name] = true;
  263. }
  264. }
  265. /**
  266. * Allows several properties to be assigned at once.
  267. *
  268. * For example:
  269. * {{{
  270. * $doc->set(array('title' => 'Lorem Ipsum', 'value' => 42));
  271. * }}}
  272. *
  273. * @param array $data An associative array of fields and values to assign to the `Document`.
  274. * @param array $options
  275. * @return void
  276. */
  277. public function set(array $data, array $options = array()) {
  278. $defaults = array('init' => false);
  279. $options += $defaults;
  280. $cast = ($schema = $this->schema());
  281. foreach ($data as $key => $val) {
  282. unset($this->_increment[$key]);
  283. if (strpos($key, '.')) {
  284. $this->_setNested($key, $val);
  285. continue;
  286. }
  287. if ($cast) {
  288. $pathKey = $this->_pathKey;
  289. $model = $this->_model;
  290. $parent = $this;
  291. $val = $schema->cast($this, $key, $val, compact('pathKey', 'model', 'parent'));
  292. }
  293. if ($val instanceof self) {
  294. $val->_exists = $options['init'] && $this->_exists;
  295. $val->_pathKey = ($this->_pathKey ? "{$this->_pathKey}." : '') . $key;
  296. $val->_model = $val->_model ?: $this->_model;
  297. $val->_schema = $val->_schema ?: $this->_schema;
  298. }
  299. $this->_updated[$key] = $val;
  300. }
  301. }
  302. /**
  303. * Allows document fields to be accessed as array keys, i.e. `$document['_id']`.
  304. *
  305. * @param mixed $offset String or integer indicating the offset or index of a document in a set,
  306. * or the name of a field in an individual document.
  307. * @return mixed Returns either a sub-object in the document, or a scalar field value.
  308. */
  309. public function offsetGet($offset) {
  310. return $this->__get($offset);
  311. }
  312. /**
  313. * Allows document fields to be assigned as array keys, i.e. `$document['_id'] = $id`.
  314. *
  315. * @param mixed $offset String or integer indicating the offset or the name of a field in an
  316. * individual document.
  317. * @param mixed $value The value to assign to the field.
  318. * @return void
  319. */
  320. public function offsetSet($offset, $value) {
  321. return $this->set(array($offset => $value));
  322. }
  323. /**
  324. * Allows document fields to be tested as array keys, i.e. `isset($document['_id'])`.
  325. *
  326. * @param mixed $offset String or integer indicating the offset or the name of a field in an
  327. * individual document.
  328. * @return boolean Returns `true` if `$offset` is a field in the document, otherwise `false`.
  329. */
  330. public function offsetExists($offset) {
  331. return $this->__isset($offset);
  332. }
  333. /**
  334. * Allows document fields to be unset as array keys, i.e. `unset($document['_id'])`.
  335. *
  336. * @param string $key The name of a field in an individual document.
  337. * @return void
  338. */
  339. public function offsetUnset($key) {
  340. return $this->__unset($key);
  341. }
  342. /**
  343. * Rewinds to the first item.
  344. *
  345. * @return mixed The current item after rewinding.
  346. */
  347. public function rewind() {
  348. reset($this->_data);
  349. reset($this->_updated);
  350. $this->_valid = (count($this->_updated) > 0);
  351. return current($this->_updated);
  352. }
  353. /**
  354. * Used by the `Iterator` interface to determine the current state of the iteration, and when
  355. * to stop iterating.
  356. *
  357. * @return boolean
  358. */
  359. public function valid() {
  360. return $this->_valid;
  361. }
  362. public function current() {
  363. $current = current($this->_data);
  364. return isset($this->_removed[key($this->_data)]) ? null : $current;
  365. }
  366. public function key() {
  367. $key = key($this->_data);
  368. return isset($this->_removed[$key]) ? false : $key;
  369. }
  370. /**
  371. * Adds conversions checks to ensure certain class types and embedded values are properly cast.
  372. *
  373. * @param string $format Currently only `array` is supported.
  374. * @param array $options
  375. * @return mixed
  376. */
  377. public function to($format, array $options = array()) {
  378. $defaults = array('handlers' => array(
  379. 'MongoId' => function($value) { return (string) $value; },
  380. 'MongoDate' => function($value) { return $value->sec; }
  381. ));
  382. $options += $defaults;
  383. $options['internal'] = false;
  384. return parent::to($format, $options);
  385. }
  386. /**
  387. * Returns the next `Document` in the set, and advances the object's internal pointer. If the
  388. * end of the set is reached, a new document will be fetched from the data source connection
  389. * handle (`$_handle`). If no more records can be fetched, returns `null`.
  390. *
  391. * @return mixed Returns the next record in the set, or `null`, if no more records are
  392. * available.
  393. */
  394. public function next() {
  395. $prev = key($this->_data);
  396. $this->_valid = (next($this->_data) !== false);
  397. $cur = key($this->_data);
  398. if (isset($this->_removed[$cur])) {
  399. return $this->next();
  400. }
  401. if (!$this->_valid && $cur !== $prev && $cur !== null) {
  402. $this->_valid = true;
  403. }
  404. return $this->_valid ? $this->__get(key($this->_data)) : null;
  405. }
  406. /**
  407. * Safely (atomically) increments the value of the specified field by an arbitrary value.
  408. * Defaults to `1` if no value is specified. Throws an exception if the specified field is
  409. * non-numeric.
  410. *
  411. * @param string $field The name of the field to be incrememnted.
  412. * @param integer|string $value The value to increment the field by. Defaults to `1` if this
  413. * parameter is not specified.
  414. * @return integer Returns the current value of `$field`, based on the value retrieved from the
  415. * data source when the entity was loaded, plus any increments applied. Note that it
  416. * may not reflect the most current value in the persistent backend data source.
  417. * @throws UnexpectedValueException Throws an exception when `$field` is set to a non-numeric
  418. * type.
  419. */
  420. public function increment($field, $value = 1) {
  421. if (!isset($this->_increment[$field])) {
  422. $this->_increment[$field] = 0;
  423. }
  424. $this->_increment[$field] += $value;
  425. if (!is_numeric($this->_updated[$field])) {
  426. throw new UnexpectedValueException("Field `{$field}` cannot be incremented.");
  427. }
  428. return $this->_updated[$field] += $value;
  429. }
  430. }
  431. ?>