Entity.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  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. use BadMethodCallException;
  10. use UnexpectedValueException;
  11. use lithium\data\Collection;
  12. use lithium\analysis\Inspector;
  13. /**
  14. * `Entity` is a smart data object which represents data such as a row or document in a
  15. * database. Entities have fields (often known as columns in databases), and track changes to its
  16. * fields, as well as associated validation errors, etc.
  17. *
  18. * The `Entity` class can also be used as a base class for your own custom data objects, and is the
  19. * basis for generating forms with the `Form` helper.
  20. *
  21. * @see lithium\template\helper\Form
  22. */
  23. class Entity extends \lithium\core\Object {
  24. /**
  25. * Fully-namespaced class name of model that this record is bound to. Instance methods declared
  26. * in the model may be called on the entity. See the `Model` class documentation for more
  27. * information.
  28. *
  29. * @see lithium\data\Model
  30. * @see lithium\data\Entity::__call()
  31. * @var string
  32. */
  33. protected $_model = null;
  34. /**
  35. * Associative array of the entity's fields and values.
  36. *
  37. * @var array
  38. */
  39. protected $_data = array();
  40. /**
  41. * An array containing all related records and recordsets, keyed by relationship name, as
  42. * defined in the bound model class.
  43. *
  44. * @var array
  45. */
  46. protected $_relationships = array();
  47. /**
  48. * If this record is chained off of another, contains the origin object.
  49. *
  50. * @var object
  51. */
  52. protected $_parent = null;
  53. /**
  54. * The list of validation errors associated with this object, where keys are field names, and
  55. * values are arrays containing one or more validation error messages.
  56. *
  57. * @see lithium\data\Entity::errors()
  58. * @var array
  59. */
  60. protected $_errors = array();
  61. /**
  62. * Contains the values of updated fields. These values will be persisted to the backend data
  63. * store when the document is saved.
  64. *
  65. * @var array
  66. */
  67. protected $_updated = array();
  68. /**
  69. * An array of key/value pairs corresponding to fields that should be updated using atomic
  70. * incrementing / decrementing operations. Keys match field names, and values indicate the value
  71. * each field should be incremented or decremented by.
  72. *
  73. * @see lithium\data\Entity::increment()
  74. * @see lithium\data\Entity::decrement()
  75. * @var array
  76. */
  77. protected $_increment = array();
  78. /**
  79. * A flag indicating whether or not this entity exists. Set to `false` if this is a
  80. * newly-created entity, or if this entity has been loaded and subsequently deleted. Set to
  81. * `true` if the entity has been loaded from the database, or has been created and subsequently
  82. * saved.
  83. *
  84. * @var boolean
  85. */
  86. protected $_exists = false;
  87. /**
  88. * A local copy of the schema definition. This is the same as `lithium\data\Model::$_schema`,
  89. * but can be defined here if this is a one-off object or class used for a single purpose, i.e.
  90. * to create a form.
  91. *
  92. * @var array
  93. */
  94. protected $_schema = array();
  95. /**
  96. * Auto configuration.
  97. *
  98. * @var array
  99. */
  100. protected $_autoConfig = array(
  101. 'classes' => 'merge', 'parent', 'schema', 'data',
  102. 'model', 'exists', 'pathKey', 'relationships'
  103. );
  104. /**
  105. * Creates a new record object with default values.
  106. *
  107. * Options defined:
  108. * - 'data' _array_: Data to enter into the record. Defaults to an empty array.
  109. * - 'model' _string_: Class name that provides the data-source for this record.
  110. * Defaults to `null`.
  111. *
  112. * @param array $config
  113. * @return object Record object.
  114. */
  115. public function __construct(array $config = array()) {
  116. $defaults = array('model' => null, 'data' => array(), 'relationships' => array());
  117. parent::__construct($config + $defaults);
  118. }
  119. protected function _init() {
  120. parent::_init();
  121. $this->_updated = $this->_data;
  122. }
  123. /**
  124. * Overloading for reading inaccessible properties.
  125. *
  126. * @param string $name Property name.
  127. * @return mixed Result.
  128. */
  129. public function &__get($name) {
  130. if (isset($this->_relationships[$name])) {
  131. return $this->_relationships[$name];
  132. }
  133. if (isset($this->_updated[$name])) {
  134. return $this->_updated[$name];
  135. }
  136. $null = null;
  137. return $null;
  138. }
  139. /**
  140. * Overloading for writing to inaccessible properties.
  141. *
  142. * @param string $name Property name.
  143. * @param string $value Property value.
  144. * @return mixed Result.
  145. */
  146. public function __set($name, $value = null) {
  147. if (is_array($name) && !$value) {
  148. return array_map(array(&$this, '__set'), array_keys($name), array_values($name));
  149. }
  150. $this->_updated[$name] = $value;
  151. }
  152. /**
  153. * Overloading for calling `isset()` or `empty()` on inaccessible properties.
  154. *
  155. * @param string $name Property name.
  156. * @return mixed Result.
  157. */
  158. public function __isset($name) {
  159. return isset($this->_updated[$name]) || isset($this->_relationships[$name]);
  160. }
  161. /**
  162. * Magic method that allows calling of model methods on this record instance, i.e.:
  163. * {{{
  164. * $record->validates();
  165. * }}}
  166. *
  167. * @param string $method Method name caught by `__call()`.
  168. * @param array $params Arguments given to the above `$method` call.
  169. * @return mixed
  170. */
  171. public function __call($method, $params) {
  172. if (($model = $this->_model) && method_exists($model, '_object')) {
  173. array_unshift($params, $this);
  174. $class = $model::invokeMethod('_object');
  175. return call_user_func_array(array(&$class, $method), $params);
  176. }
  177. $message = "No model bound to call `{$method}`.";
  178. throw new BadMethodCallException($message);
  179. }
  180. /**
  181. * Custom check to determine if our given magic methods can be responded to.
  182. *
  183. * @param string $method Method name.
  184. * @param bool $internal Interal call or not.
  185. * @return bool
  186. */
  187. public function respondsTo($method, $internal = false) {
  188. $class = $this->_model;
  189. $modelRespondsTo = false;
  190. $parentRespondsTo = parent::respondsTo($method, $internal);
  191. $staticRespondsTo = $class::respondsTo($method, $internal);
  192. if (method_exists($class, '_object')) {
  193. $model = $class::invokeMethod('_object');
  194. $modelRespondsTo = $model->respondsTo($method);
  195. } else {
  196. $modelRespondsTo = Inspector::isCallable($class, $method, $internal);
  197. }
  198. return $parentRespondsTo || $staticRespondsTo || $modelRespondsTo;
  199. }
  200. /**
  201. * Allows several properties to be assigned at once, i.e.:
  202. * {{{
  203. * $record->set(array('title' => 'Lorem Ipsum', 'value' => 42));
  204. * }}}
  205. *
  206. * @param array $data An associative array of fields and values to assign to this `Entity`
  207. * instance.
  208. * @return void
  209. */
  210. public function set(array $data) {
  211. foreach ($data as $name => $value) {
  212. $this->__set($name, $value);
  213. }
  214. }
  215. /**
  216. * Access the data fields of the record. Can also access a $named field.
  217. *
  218. * @param string $name Optionally included field name.
  219. * @return mixed Entire data array if $name is empty, otherwise the value from the named field.
  220. */
  221. public function data($name = null) {
  222. if ($name) {
  223. return $this->__get($name);
  224. }
  225. return $this->to('array');
  226. }
  227. /**
  228. * Returns the model which this entity is bound to.
  229. *
  230. * @return string The fully qualified model class name.
  231. */
  232. public function model() {
  233. return $this->_model;
  234. }
  235. public function schema($field = null) {
  236. $schema = null;
  237. switch (true) {
  238. case (is_object($this->_schema)):
  239. $schema = $this->_schema;
  240. break;
  241. case ($model = $this->_model):
  242. $schema = $model::schema();
  243. break;
  244. }
  245. if ($schema) {
  246. return $field ? $schema->fields($field) : $schema;
  247. }
  248. return array();
  249. }
  250. /**
  251. * Access the errors of the record.
  252. *
  253. * @see lithium\data\Entity::$_errors
  254. * @param array|string $field If an array, overwrites `$this->_errors`. If a string, and
  255. * `$value` is not `null`, sets the corresponding key in `$this->_errors` to `$value`.
  256. * @param string $value Value to set.
  257. * @return mixed Either the `$this->_errors` array, or single value from it.
  258. */
  259. public function errors($field = null, $value = null) {
  260. if ($field === null) {
  261. return $this->_errors;
  262. }
  263. if (is_array($field)) {
  264. return ($this->_errors = $field);
  265. }
  266. if ($value === null && isset($this->_errors[$field])) {
  267. return $this->_errors[$field];
  268. }
  269. if ($value !== null) {
  270. return $this->_errors[$field] = $value;
  271. }
  272. return $value;
  273. }
  274. /**
  275. * A flag indicating whether or not this record exists.
  276. *
  277. * @return boolean `True` if the record was `read` from the data-source, or has been `create`d
  278. * and `save`d. Otherwise `false`.
  279. */
  280. public function exists() {
  281. return $this->_exists;
  282. }
  283. /**
  284. * Called after an `Entity` is saved. Updates the object's internal state to reflect the
  285. * corresponding database entity, and sets the `Entity` object's key, if this is a newly-created
  286. * object. **Do not** call this method if you intend to update the database's copy of the
  287. * entity. Instead, see `Model::save()`.
  288. *
  289. * @see lithium\data\Model::save()
  290. * @param mixed $id The ID to assign, where applicable.
  291. * @param array $data Any additional generated data assigned to the object by the database.
  292. * @param array $options Method options:
  293. * - `'materialize'` _boolean_: Determines whether or not the flag should be set
  294. * that indicates that this entity exists in the data store. Defaults to `true`.
  295. * - `'dematerialize'` _boolean_: If set to `true`, indicates that this entity has
  296. * been deleted from the data store and no longer exists. Defaults to `false`.
  297. * @return void
  298. */
  299. public function sync($id = null, array $data = array(), array $options = array()) {
  300. $defaults = array('materialize' => true, 'dematerialize' => false);
  301. $options += $defaults;
  302. $model = $this->_model;
  303. $key = array();
  304. if ($options['materialize']) {
  305. $this->_exists = true;
  306. }
  307. if ($options['dematerialize']) {
  308. $this->_exists = false;
  309. }
  310. if ($id && $model) {
  311. $key = $model::meta('key');
  312. $key = is_array($key) ? array_combine($key, $id) : array($key => $id);
  313. }
  314. $this->_data = $this->_updated = ($key + $data + $this->_updated);
  315. }
  316. /**
  317. * Safely (atomically) increments the value of the specified field by an arbitrary value.
  318. * Defaults to `1` if no value is specified. Throws an exception if the specified field is
  319. * non-numeric.
  320. *
  321. * @param string $field The name of the field to be incremented.
  322. * @param string $value The value to increment the field by. Defaults to `1` if this parameter
  323. * is not specified.
  324. * @return integer Returns the current value of `$field`, based on the value retrieved from the
  325. * data source when the entity was loaded, plus any increments applied. Note that it may
  326. * not reflect the most current value in the persistent backend data source.
  327. * @throws UnexpectedValueException Throws an exception when `$field` is set to a non-numeric
  328. * type.
  329. */
  330. public function increment($field, $value = 1) {
  331. if (!isset($this->_updated[$field])) {
  332. return $this->_updated[$field] = $value;
  333. }
  334. if (!is_numeric($this->_updated[$field])) {
  335. throw new UnexpectedValueException("Field '{$field}' cannot be incremented.");
  336. }
  337. return $this->_updated[$field] += $value;
  338. }
  339. /**
  340. * Decrements a field by the specified value. Works identically to `increment()`, but in
  341. * reverse.
  342. *
  343. * @see lithium\data\Entity::increment()
  344. * @param string $field The name of the field to decrement.
  345. * @param string $value The value by which to decrement the field. Defaults to `1`.
  346. * @return integer Returns the new value of `$field`, after modification.
  347. */
  348. public function decrement($field, $value = 1) {
  349. return $this->increment($field, $value * -1);
  350. }
  351. /**
  352. * Gets the current state for a given field or, if no field is given, gets the array of
  353. * fields modified on this entity.
  354. *
  355. * @param string The field name to check its state.
  356. * @return mixed Returns `true` if a field is given and was updated, `false` otherwise and
  357. * `null` if the field was not set at all. If no field is given returns an arra
  358. * where the keys are entity field names, and the values are `true` for changed
  359. * fields.
  360. */
  361. public function modified($field = null) {
  362. if ($field) {
  363. if (!isset($this->_updated[$field]) && !isset($this->_data[$field])) {
  364. return null;
  365. }
  366. if (!array_key_exists($field, $this->_updated)) {
  367. return false;
  368. }
  369. $value = $this->_updated[$field];
  370. if (is_object($value) && method_exists($value, 'modified')) {
  371. $modified = $value->modified();
  372. return $modified === true || is_array($modified) && in_array(true, $modified, true);
  373. }
  374. $isSet = isset($this->_data[$field]);
  375. return !$isSet || ($this->_data[$field] !== $this->_updated[$field]);
  376. }
  377. $fields = array_fill_keys(array_keys($this->_data), false);
  378. foreach ($this->_updated as $field => $value) {
  379. if (is_object($value) && method_exists($value, 'modified')) {
  380. if (!isset($this->_data[$field])) {
  381. $fields[$field] = true;
  382. continue;
  383. }
  384. $modified = $value->modified();
  385. $fields[$field] = (
  386. $modified === true || is_array($modified) && in_array(true, $modified, true)
  387. );
  388. } else {
  389. $fields[$field] = (
  390. !isset($fields[$field]) || $this->_data[$field] !== $this->_updated[$field]
  391. );
  392. }
  393. }
  394. return $fields;
  395. }
  396. public function export(array $options = array()) {
  397. return array(
  398. 'exists' => $this->_exists,
  399. 'data' => $this->_data,
  400. 'update' => $this->_updated,
  401. 'increment' => $this->_increment
  402. );
  403. }
  404. /**
  405. * Configures protected properties of an `Entity` so that it is parented to `$parent`.
  406. *
  407. * @param object $parent
  408. * @param array $config
  409. * @return void
  410. */
  411. public function assignTo($parent, array $config = array()) {
  412. foreach ($config as $key => $val) {
  413. $this->{'_' . $key} = $val;
  414. }
  415. $this->_parent =& $parent;
  416. }
  417. /**
  418. * Converts the data in the record set to a different format, i.e. an array.
  419. *
  420. * @param string $format currently only `array`
  421. * @param array $options
  422. * @return mixed
  423. */
  424. public function to($format, array $options = array()) {
  425. switch ($format) {
  426. case 'array':
  427. $data = $this->_updated;
  428. $rel = array_map(function($obj) { return $obj->data(); }, $this->_relationships);
  429. $data = $rel + $data;
  430. $result = Collection::toArray($data, $options);
  431. break;
  432. case 'string':
  433. $result = $this->__toString();
  434. break;
  435. default:
  436. $result = $this;
  437. break;
  438. }
  439. return $result;
  440. }
  441. /**
  442. * Returns a string representation of the `Entity` instance, based on the result of the
  443. * `'title'` meta value of the bound model class.
  444. *
  445. * @return string Returns the generated title of the object.
  446. */
  447. public function __toString() {
  448. return (string) $this->__call('title', array());
  449. }
  450. }
  451. ?>