Permalink
Browse files

Rewriting change-tracking in data entities. Original state and change…

…s are now tracked in separate arrays. Implementing `Exporter` class to convert entity changes to MongoDB commands. This breaks relational databases, which need to be updated. Sorry.
  • Loading branch information...
1 parent cfd2867 commit eaa57702c37f00c64430fbf5910f1e311462adae @nateabele nateabele committed Dec 29, 2010
@@ -25,7 +25,13 @@
class Entity extends \lithium\core\Object {
/**
- * Namespaced name of model that this record is linked to.
+ * Fully-namespaced class name of model that this record is bound to. Instance methods declared
+ * in the model may be called on the entity. See the `Model` class documentation for more
+ * information.
+ *
+ * @see lithium\data\Model
+ * @see lithium\data\Entity::__call()
+ * @var string
*/
protected $_model = null;
@@ -59,23 +65,38 @@ class Entity extends \lithium\core\Object {
protected $_handle = null;
/**
- * Validation errors
+ * The list of validation errors associated with this object, where keys are field names, and
+ * values are arrays containing one or more validation error messages.
+ *
+ * @see lithium\data\Entity::errors()
+ * @var array
*/
protected $_errors = array();
/**
- * An array of flags to track which fields in this record have been modified, where the keys
- * are field names, and the values are always `true`. If, for example, a change to a field is
- * reverted, that field's flag should be unset from the list.
+ * Contains the values of updated fields. These values will be persisted to the backend data
+ * store when the document is saved.
+ *
+ * @var array
+ */
+ protected $_updated = array();
+
+ /**
+ * An array of key/value pairs corresponding to fields that should be updated using atomic
+ * incrementing / decrementing operations. Keys match field names, and values indicate the value
+ * each field should be incremented or decrememnted by.
*
+ * @see lithium\data\Entity::increment()
+ * @see lithium\data\Entity::decrement()
* @var array
*/
- protected $_modified = array();
+ protected $_increment = array();
/**
- * A flag indicating whether or not this record exists. Set to false if this is a newly-created
- * record, or if this record has been loaded and subsequently deleted. True if the record has
- * been loaded from the database, or has been created and subsequently saved.
+ * A flag indicating whether or not this entity exists. Set to `false` if this is a
+ * newly-created entity, or if this entity has been loaded and subsequently deleted. Set to
+ * `true` if the entity has been loaded from the database, or has been created and subsequently
+ * saved.
*
* @var boolean
*/
@@ -157,13 +178,9 @@ public function &__get($name) {
*/
public function __set($name, $value = null) {
if (is_array($name) && !$value) {
- foreach ($name as $key => $value) {
- $this->__set($key, $value);
- }
- return;
+ return array_map(array(&$this, '__set'), array_keys($name), array_values($name));
}
- $this->_modified[$name] = true;
- $this->_data[$name] = $value;
+ $this->_updated[$name] = $value;
}
/**
@@ -173,7 +190,7 @@ public function __set($name, $value = null) {
* @return mixed Result.
*/
public function __isset($name) {
- return array_key_exists($name, $this->_data);
+ return isset($this->_data[$name]) || isset($this->_updated[$name]);
}
/**
@@ -219,7 +236,10 @@ public function set($values) {
* field.
*/
public function data($name = null) {
- return empty($name) ? $this->_data : $this->__get($name);
+ if ($name) {
+ return $this->__get($name);
+ }
+ return array_merge($this->_data, $this->_updated);
}
/**
@@ -232,16 +252,15 @@ public function model() {
}
public function schema($field = null) {
+ $schema = array();
+
switch (true) {
case ($this->_schema):
$schema = $this->_schema;
break;
case ($model = $this->_model):
$schema = $model::schema();
break;
- default:
- $schema = array();
- break;
}
if ($field) {
return isset($self->_schema[$field]) ? $self->_schema[$field] : null;
@@ -252,18 +271,18 @@ public function schema($field = null) {
/**
* Access the errors of the record.
*
- * @param array|string $field If an array, overwrites `$this->_errors`. If a string, and $value
- * is not null, sets the corresponding key in $this->_errors to $value
+ * @see lithium\data\Entity::$_errors
+ * @param array|string $field If an array, overwrites `$this->_errors`. If a string, and
+ * `$value` is not `null`, sets the corresponding key in `$this->_errors` to `$value`.
* @param string $value Value to set.
- * @return array|string Either the $this->_errors array, or single value from it.
+ * @return array|string Either the `$this->_errors` array, or single value from it.
*/
public function errors($field = null, $value = null) {
if ($field === null) {
return $this->_errors;
}
if (is_array($field)) {
- $this->_errors = $field;
- return $this->_errors;
+ return ($this->_errors = $field);
}
if ($value === null && isset($this->_errors[$field])) {
return $this->_errors[$field];
@@ -286,34 +305,61 @@ public function exists() {
/**
* Called after an `Entity` is saved. Updates the object's internal state to reflect the
- * corresponding database record, and sets the `Record`'s primary key, if this is a
- * newly-created object.
+ * corresponding database entity, and sets the `Entity` object's key, if this is a newly-created
+ * object.
*
* @param mixed $id The ID to assign, where applicable.
* @param array $data Any additional generated data assigned to the object by the database.
* @return void
*/
public function update($id = null, array $data = array()) {
- $this->_modified = array();
$this->_exists = true;
+ $model = $this->_model;
+ $key = array();
- if (!$id) {
- return;
+ if ($id && $model) {
+ $key = $model::meta('key');
+ $key = is_array($key) ? array_combine($key, $id) : array($key => $id);
}
+ $this->_data = ($key + $data + $this->_updated + $this->_data);
+ $this->_updated = array();
+ }
- $model = $this->_model;
- $key = $model::meta('key');
-
- if (is_array($key)) {
- foreach ($key as $i => $k) {
- $this->_data[$k] = $id[$i];
- }
- } else {
- $this->_data[$key] = $id;
+ /**
+ * Safely (atomically) increments the value of the specified field by an arbitrary value.
+ * Defaults to `1` if no value is specified. Throws an exception if the specified field is
+ * non-numeric.
+ *
+ * @param string $field The name of the field to be incrememnted.
+ * @param string $value The value to increment the field by. Defaults to `1` if this parameter
+ * is not specified.
+ * @return int Returns the current value of `$field`, based on the value retrieved from the data
+ * source when the entity was loaded, plus any increments applied. Note that it may not
+ * reflect the most current value in the persistent backend data source.
+ * @throws UnexpectedValueException Throws an exception when `$field` is set to a non-numeric
+ * type.
+ */
+ public function increment($field, $value = 1) {
+ if (!isset($this->_data[$field])) {
+ return $this->_data[$field] = $value;
}
- foreach ($data as $key => $value) {
- $this->_data[$key] = $value;
+ if (!is_numeric($this->_data[$field])) {
+ throw new UnexpectedValueException("Field '{$field}' cannot be incremented.");
}
+ $this->_data[$field] += $value;
+ }
+
+ /**
+ * Decrements a field by the specified value. Works identically to `increment()`, but in
+ * reverse.
+ *
+ * @see lithium\data\Entity::increment()
+ * @param string $field The name of the field to decrement.
+ * @param string $value The value by which to decrement the field. Defaults to `1`.
+ * @return int Returns the new value of `$field`, after modification.
+ */
+ public function decrement($field, $value = 1) {
+ return $this->increment($field, $value * -1);
}
/**
@@ -330,8 +376,13 @@ public function modified() {
return $this->_modified;
}
- public function export(Source $dataSource, array $options = array()) {
- return array_intersect_key($this->_data, $this->_modified);
+ public function export() {
+ return array(
+ 'exists' => $this->_exists,
+ 'data' => $this->_data,
+ 'update' => $this->_updated,
+ 'increment' => $this->_increment,
+ );
}
/**
@@ -399,7 +450,7 @@ protected function _relation($classType, $key, $data, $options = array()) {
if ($model) {
$exists = $this->_exists;
$options += compact('parent', 'exists', 'pathKey');
- return $model::connection()->cast($model, $data, $options);
+ return $model::connection()->cast($this, $data, $options);
}
}
}
@@ -580,13 +580,18 @@ public static function bind($type, $name, array $config = array()) {
* Lazy-initialize the schema for this Model object, if it is not already manually set in the
* object. You can declare `protected $_schema = array(...)` to define the schema manually.
*
- * @param string $field Optional. You may pass a field name to get schema information for just
- * one field. Otherwise, an array containing all fields is returned.
+ * @param mixed $field Optional. You may pass a field name to get schema information for just
+ * one field. Otherwise, an array containing all fields is returned. If `false`, the
+ * schema is reset to an empty value. If an array, field definitions contained are
+ * appended to the schema.
* @return array
*/
public static function schema($field = null) {
$self = static::_object();
+ if ($field === false) {
+ return $self->_schema = array();
+ }
if (!$self->_schema) {
$self->_schema = static::connection()->describe($self::meta('source'), $self->_meta);
}
@@ -707,8 +712,8 @@ public function save($entity, $data = null, array $options = array()) {
return false;
}
}
- if ($options['whitelist'] || $options['locked']) {
- $whitelist = $options['whitelist'] ?: array_keys($_schema);
+ if (($whitelist = $options['whitelist']) || $options['locked']) {
+ $whitelist = $whitelist ?: array_keys($_schema);
}
$type = $entity->exists() ? 'update' : 'create';
@@ -210,7 +210,8 @@ public function name($name) {
/**
* Casts data into proper format when added to a collection or entity object.
*
- * @param string $model The name of the model class to which the entity or collection is bound.
+ * @param mixed $entity The entity or collection for which data is being cast, or the name of
+ * the model class to which the entity/collection is bound.
* @param array $data An array of data being assigned.
* @param array $options Any associated options with, for example, instantiating new objects in
* which to wrap the data. Options implemented by `cast()` itself:
@@ -220,7 +221,7 @@ public function name($name) {
* @return mixed Returns the value of `$data`, cast to the proper format according to the schema
* definition of the model class specified by `$model`.
*/
- public function cast($model, array $data, array $options = array()) {
+ public function cast($entity, array $data, array $options = array()) {
$defaults = array('first' => false);
$options += $defaults;
return $options['first'] ? reset($data) : $data;
@@ -13,6 +13,26 @@
class DocumentArray extends \lithium\data\Collection {
+ protected $_exists = false;
+
+ /**
+ * Holds an array of values that should be processed on initialization.
+ *
+ * @var array
+ */
+ protected $_autoConfig = array(
+ 'data', 'model', 'result', 'query', 'parent', 'stats', 'pathKey', 'exists'
+ );
+
+ public function exists() {
+ return $this->_exists;
+ }
+
+ public function update($id = null, array $data = array()) {
+ $this->_exists = true;
+ $this->_data = $data ?: $this->_data;
+ }
+
/**
* Adds conversions checks to ensure certain class types and embedded values are properly cast.
*
@@ -71,9 +91,8 @@ public function offsetGet($offset) {
public function offsetSet($offset, $data) {
if ($model = $this->_model) {
- $data = $model::connection()->cast($model, array($this->_pathKey => $data), array(
- 'first' => true
- ));
+ $options = array('first' => true, 'schema' => $model::schema());
+ $data = $model::connection()->cast($this, array($this->_pathKey => $data), $options);
}
if ($offset) {
return $this->_data[$offset] = $data;
@@ -115,17 +134,12 @@ public function next() {
return $this->_valid ? $this->offsetGet(key($this->_data)) : null;
}
- public function export(Source $dataSource, array $options = array()) {
- $result = array();
-
- foreach ($this->_data as $key => $doc) {
- if (is_object($doc) && method_exists($doc, 'export')) {
- $result[$key] = $doc->export($dataSource, $options);
- continue;
- }
- $result[$key] = $doc;
- }
- return $result;
+ public function export() {
+ return array(
+ 'exists' => $this->_exists,
+ 'key' => $this->_pathKey,
+ 'data' => $this->_data,
+ );
}
protected function _populate($data = null, $key = null) {}
Oops, something went wrong.

0 comments on commit eaa5770

Please sign in to comment.