PageRenderTime 53ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/htdocs/lithium/0.9.9/libraries/lithium/data/entity/Document.php

http://github.com/pmjones/php-framework-benchmarks
PHP | 463 lines | 216 code | 49 blank | 198 comment | 39 complexity | 62e57eb2e4f148087a3c045f3773b24c MD5 | raw file
Possible License(s): LGPL-3.0, Apache-2.0, BSD-3-Clause, ISC, AGPL-3.0, LGPL-2.1
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2010, 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 lithium\data\Source;
  10. use lithium\util\Collection;
  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: 'larry@acme.com' },
  23. * 'Curly': { email: 'curly@acme.com' },
  24. * 'Moe': { email: 'moe@acme.com' }
  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' => 'larry@acme.com'),
  43. * // 'Curly' => array('email' => 'curly@acme.com'),
  44. * // 'Moe' => array('email' => 'moe@acme.com')
  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. * Indicates whether this document has already been created in the database.
  69. *
  70. * @var boolean
  71. */
  72. protected $_exists = false;
  73. protected $_errors = array();
  74. /**
  75. * An array containing all related documents, keyed by relationship name, as defined in the
  76. * bound model class.
  77. *
  78. * @var array
  79. */
  80. protected $_relations = array();
  81. /**
  82. * An array of flags to track which fields in this document have been modified, where the keys
  83. * are field names, and the values are always `true`. If, for example, a change to a field is
  84. * reverted, that field's flag should be unset from the list.
  85. *
  86. * @var array
  87. */
  88. protected $_modified = array();
  89. /**
  90. * Contains an array of backend-specific statistics generated by the query that produced this
  91. * `Document` object. These stats are accessible via the `stats()` method.
  92. *
  93. * @see lithium\data\collection\DocumentSet::stats()
  94. * @var array
  95. */
  96. protected $_stats = array();
  97. /**
  98. * Holds the current iteration state. Used by `Document::valid()` to terminate `foreach` loops
  99. * when there are no more fields to iterate over.
  100. *
  101. * @var boolean
  102. */
  103. protected $_valid = false;
  104. protected function _init() {
  105. parent::_init();
  106. $this->_data = (array) $this->_data;
  107. if ($model = $this->_model) {
  108. $pathKey = $this->_pathKey;
  109. $this->_data = $model::connection()->cast($model, $this->_data, compact('pathKey'));
  110. }
  111. unset($this->_autoConfig);
  112. }
  113. /**
  114. * PHP magic method used when accessing fields as document properties, i.e. `$document->_id`.
  115. *
  116. * @param $name The field name, as specified with an object property.
  117. * @return mixed Returns the value of the field specified in `$name`, and wraps complex data
  118. * types in sub-`Document` objects.
  119. */
  120. public function &__get($name) {
  121. $data = null;
  122. $null = null;
  123. $model = $this->_model;
  124. $conn = $model ? $model::connection() : null;
  125. if (isset($this->_relationships[$name])) {
  126. return $this->_relationships[$name];
  127. }
  128. if (strpos($name, '.')) {
  129. return $this->_getNested($name);
  130. }
  131. if ($model && $conn) {
  132. foreach ($model::relations() as $relation => $config) {
  133. if ($config && (($linkKey = $config->data('fieldName')) === $name)) {
  134. $data = isset($this->_data[$name]) ? $this->_data[$name] : array();
  135. $this->_relationships[$name] = $this->_relationship($config);
  136. return $this->_relationships[$name];
  137. }
  138. }
  139. if (!isset($this->_data[$name]) && $schema = $model::schema($name)) {
  140. $schema = array($name => $schema);
  141. $pathKey = $this->_pathKey ? "{$this->_pathKey}.{$name}" : $name;
  142. $options = compact('pathKey', 'schema') + array('first' => true);
  143. $this->_data[$name] = $conn->cast($model, array($name => null), $options);
  144. return $this->_data[$name];
  145. }
  146. }
  147. if (!isset($this->_data[$name])) {
  148. return $null;
  149. }
  150. return $this->_data[$name];
  151. }
  152. public function export(Source $dataSource, array $options = array()) {
  153. $defaults = array('atomic' => true);
  154. $options += $defaults;
  155. list($data, $nested) = $this->_exportRecursive($dataSource, $options);
  156. if ($options['atomic'] && $this->_exists) {
  157. $data = array_intersect_key($data, $this->_modified + $nested);
  158. }
  159. if ($model = $this->_model) {
  160. $name = null;
  161. $options = array('atomic' => false) + $options;
  162. $relations = new Collection(array('data' => $model::relations()));
  163. $find = function($relation) use (&$name) { return $relation->fieldName === $name; };
  164. foreach ($this->_relationships as $name => $subObject) {
  165. if (($rel = $relations->first($find)) && $rel->link == $rel::LINK_EMBEDDED) {
  166. $data[$name] = $subObject->export($dataSource, $options);
  167. }
  168. }
  169. }
  170. return $data;
  171. }
  172. protected function _exportRecursive(Source $dataSource, array $options = array()) {
  173. $data = array();
  174. $nested = array();
  175. foreach ($this->_data as $key => $val) {
  176. if (!is_object($val) || !method_exists($val, 'export')) {
  177. $data[$key] = $val;
  178. continue;
  179. }
  180. $nestedOptions = $options;
  181. if (!$this->_exists || isset($this->_modified[$key])) {
  182. $nestedOptions = array('atomic' => false) + $nestedOptions;
  183. }
  184. if ($data[$key] = $val->export($dataSource, $nestedOptions)) {
  185. $nested[$key] = true;
  186. }
  187. }
  188. return array($data, $nested);
  189. }
  190. public function update($id = null, array $data = array()) {
  191. foreach ($this->_data as $key => $val) {
  192. if (is_object($val) && method_exists($val, 'update')) {
  193. $this->_data[$key]->update(null, isset($data[$key]) ? $data[$key] : array());
  194. }
  195. }
  196. return parent::update($id, $data);
  197. }
  198. /**
  199. * Instantiates a new `Document` object as a descendant of the current object, and sets all
  200. * default values and internal state.
  201. *
  202. * @param string $classType The type of class to create, either `'entity'` or `'set'`.
  203. * @param string $key The key name to which the related object is assigned.
  204. * @param array $data The internal data of the related object.
  205. * @param array $options Any other options to pass when instantiating the related object.
  206. * @return object Returns a new `Document` object instance.
  207. */
  208. protected function _relation($classType, $key, $data, $options = array()) {
  209. $options['exists'] = false;
  210. return parent::_relation($classType, $key, $data, $options);
  211. }
  212. protected function _relationship($relationship) {
  213. $classType = ($relationship->type == 'hasMany') ? 'set' : 'entity';
  214. $config = array('model' => $relationship->to, 'parent' => $this, 'exists' => true);
  215. $class = $this->_classes[$classType];
  216. switch ($relationship->link) {
  217. case $relationship::LINK_EMBEDDED:
  218. $field = $relationship->fieldName;
  219. $config['data'] = isset($this->_data[$field]) ? $this->_data[$field] : array();
  220. break;
  221. }
  222. return new $class($config);
  223. }
  224. protected function &_getNested($name) {
  225. $null = null;
  226. $current = $this;
  227. $path = explode('.', $name);
  228. $length = count($path) - 1;
  229. foreach ($path as $i => $key) {
  230. $current =& $current->__get($key);
  231. if (!$current instanceof Document && $i < $length) {
  232. return $null;
  233. }
  234. }
  235. return $current;
  236. }
  237. /**
  238. * PHP magic method used when setting properties on the `Document` instance, i.e.
  239. * `$document->title = 'Lorem Ipsum'`. If `$value` is a complex data type (i.e. associative
  240. * array), it is wrapped in a sub-`Document` object before being appended.
  241. *
  242. * @param $name The name of the field/property to write to, i.e. `title` in the above example.
  243. * @param $value The value to write, i.e. `'Lorem Ipsum'`.
  244. * @return void
  245. */
  246. public function __set($name, $value = null) {
  247. if (is_array($name)) {
  248. foreach ($name as $key => $val) {
  249. $this->__set($key, $val);
  250. }
  251. return;
  252. }
  253. if (is_string($name) && strpos($name, '.')) {
  254. return $this->_setNested($name, $value);
  255. }
  256. if ($model = $this->_model) {
  257. $pathKey = $this->_pathKey;
  258. $options = compact('pathKey') + array('first' => true);
  259. $value = $model::connection()->cast($model, array($name => $value), $options);
  260. }
  261. $this->_data[$name] = $value;
  262. $this->_modified[$name] = true;
  263. }
  264. protected function _setNested($name, $value) {
  265. $current = $this;
  266. $path = explode('.', $name);
  267. $length = count($path) - 1;
  268. for ($i = 0; $i < $length; $i++) {
  269. $key = $path[$i];
  270. $next = $current->__get($key);
  271. if ($next === null && ($model = $this->_model)) {
  272. $next = $current->_data[$key] = $model::connection()->item($model);
  273. }
  274. $current = $next;
  275. }
  276. if (is_object($current)) {
  277. $current->__set(end($path), $value);
  278. }
  279. }
  280. /**
  281. * PHP magic method used to check the presence of a field as document properties, i.e.
  282. * `$document->_id`.
  283. *
  284. * @param $name The field name, as specified with an object property.
  285. * @return boolean True if the field specified in `$name` exists, false otherwise.
  286. */
  287. public function __isset($name) {
  288. return isset($this->_data[$name]);
  289. }
  290. /**
  291. * PHP magic method used when unset() is called on a `Document` instance.
  292. * Use case for this would be when you wish to edit a document and remove a field, ie. :
  293. * {{{ $doc = Post::find($id); unset($doc->fieldName); $doc->save(); }}}
  294. *
  295. * @param unknown_type $name
  296. * @return unknown_type
  297. */
  298. public function __unset($name) {
  299. unset($this->_data[$name]);
  300. }
  301. /**
  302. * Allows several properties to be assigned at once.
  303. *
  304. * For example:
  305. * {{{
  306. * $doc->set(array('title' => 'Lorem Ipsum', 'value' => 42));
  307. * }}}
  308. *
  309. * @param $values An associative array of fields and values to assign to the `Document`.
  310. * @return void
  311. */
  312. public function set($values) {
  313. $this->__set($values);
  314. }
  315. /**
  316. * Allows document fields to be accessed as array keys, i.e. `$document['_id']`.
  317. *
  318. * @param mixed $offset String or integer indicating the offset or index of a document in a set,
  319. * or the name of a field in an individual document.
  320. * @return mixed Returns either a sub-object in the document, or a scalar field value.
  321. */
  322. public function offsetGet($offset) {
  323. return $this->__get($offset);
  324. }
  325. /**
  326. * Allows document fields to be assigned as array keys, i.e. `$document['_id'] = $id`.
  327. *
  328. * @param mixed $offset String or integer indicating the offset or the name of a field in an
  329. * individual document.
  330. * @param mixed $value The value to assign to the field.
  331. * @return void
  332. */
  333. public function offsetSet($offset, $value) {
  334. return $this->__set(array($offset => $value));
  335. }
  336. /**
  337. * Allows document fields to be tested as array keys, i.e. `isset($document['_id'])`.
  338. *
  339. * @param mixed $offset String or integer indicating the offset or the name of a field in an
  340. * individual document.
  341. * @param mixed $value The value to assign to the field.
  342. * @return boolean Returns `true` if `$offset` is a field in the document, otherwise `false`.
  343. */
  344. public function offsetExists($offset) {
  345. return $this->__isset($offset);
  346. }
  347. /**
  348. * Allows document fields to be unset as array keys, i.e. `unset($document['_id'])`.
  349. *
  350. * @param mixed $offset String or integer indicating the offset or the name of a field in an
  351. * individual document.
  352. * @return void
  353. */
  354. public function offsetUnset($offset) {
  355. return $this->__unset($offset);
  356. }
  357. /**
  358. * Rewinds to the first item.
  359. *
  360. * @return mixed The current item after rewinding.
  361. */
  362. public function rewind() {
  363. reset($this->_data);
  364. $this->_valid = (count($this->_data) > 0);
  365. return current($this->_data);
  366. }
  367. /**
  368. * Used by the `Iterator` interface to determine the current state of the iteration, and when
  369. * to stop iterating.
  370. *
  371. * @return boolean
  372. */
  373. public function valid() {
  374. return $this->_valid;
  375. }
  376. public function current() {
  377. return current($this->_data);
  378. }
  379. public function key() {
  380. return key($this->_data);
  381. }
  382. /**
  383. * Returns the next `Document` in the set, and advances the object's internal pointer. If the
  384. * end of the set is reached, a new document will be fetched from the data source connection
  385. * handle (`$_handle`). If no more records can be fetched, returns `null`.
  386. *
  387. * @return object|null Returns the next record in the set, or `null`, if no more records are
  388. * available.
  389. */
  390. public function next() {
  391. $prev = key($this->_data);
  392. $this->_valid = (next($this->_data) !== false);
  393. $cur = key($this->_data);
  394. if (!$this->_valid && $cur !== $prev && $cur !== null) {
  395. $this->_valid = true;
  396. }
  397. return $this->_valid ? $this->__get(key($this->_data)) : null;
  398. }
  399. /**
  400. * Gets the raw data associated with this `Document`, or single item if `$field` is defined.
  401. *
  402. * @param string $field if included will only return the named item
  403. * @return array Returns a raw array of `Document` data, or individual field value
  404. */
  405. public function data($field = null) {
  406. if ($field) {
  407. return isset($this->_data[$field]) ? $this->_data[$field] : null;
  408. }
  409. return $this->to('array') + array_map(
  410. function($relationship) { return $relationship->data(); }, $this->_relationships
  411. );
  412. }
  413. }
  414. ?>