Permalink
Browse files

Refactoring MongoDB changeset calculation for update operations, to i…

…mprove efficiency. Renaming `keys` config field in `\data\model\Relationship` to `key`, to better reflect default use case.
  • Loading branch information...
1 parent b58092f commit 18b6a8c58766d9a561d24917a899f86e1a7a28df @nateabele nateabele committed Jul 14, 2011
View
2 data/Collection.php
@@ -93,7 +93,7 @@
* @var array
*/
protected $_autoConfig = array(
- 'data', 'model', 'result', 'query', 'parent', 'stats', 'pathKey'
+ 'data', 'model', 'result', 'query', 'parent', 'stats', 'pathKey', 'schema'
);
/**
View
37 data/Entity.php
@@ -130,6 +130,11 @@ public function __construct(array $config = array()) {
parent::__construct($config + $defaults);
}
+ protected function _init() {
+ parent::_init();
+ $this->_updated = $this->_data;
+ }
+
/**
* Overloading for reading inaccessible properties.
*
@@ -143,9 +148,6 @@ public function &__get($name) {
if (isset($this->_updated[$name])) {
return $this->_updated[$name];
}
- if (isset($this->_data[$name])) {
- return $this->_data[$name];
- }
$null = null;
return $null;
}
@@ -171,7 +173,7 @@ public function __set($name, $value = null) {
* @return mixed Result.
*/
public function __isset($name) {
- return isset($this->_data[$name]) || isset($this->_updated[$name]);
+ return isset($this->_updated[$name]);
}
/**
@@ -200,11 +202,12 @@ public function __call($method, $params) {
* $record->set(array('title' => 'Lorem Ipsum', 'value' => 42));
* }}}
*
- * @param $values An associative array of fields and values to assign to the `Record`.
+ * @param array $data An associative array of fields and values to assign to this `Entity`
+ * instance.
* @return void
*/
- public function set($values) {
- foreach ($values as $name => $value) {
+ public function set(array $data) {
+ foreach ($data as $name => $value) {
$this->__set($name, $value);
}
}
@@ -310,8 +313,7 @@ public function sync($id = null, array $data = array(), array $options = array()
$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();
+ $this->_data = $this->_updated = ($key + $data + $this->_updated);
}
/**
@@ -329,14 +331,13 @@ public function sync($id = null, array $data = array(), array $options = array()
* type.
*/
public function increment($field, $value = 1) {
- if (!isset($this->_data[$field])) {
- return $this->_data[$field] = $value;
+ if (!isset($this->_updated[$field])) {
+ return $this->_updated[$field] = $value;
}
- if (!is_numeric($this->_data[$field])) {
+ if (!is_numeric($this->_updated[$field])) {
throw new UnexpectedValueException("Field '{$field}' cannot be incremented.");
}
- $base = isset($this->_updated[$field]) ? $this->_updated[$field] : $this->_data[$field];
- return $this->_updated[$field] = ($base + $value);
+ return $this->_updated[$field] += $value;
}
/**
@@ -359,11 +360,7 @@ public function decrement($field, $value = 1) {
* always `true`.
*/
public function modified() {
- if (!$this->_exists) {
- $keys = array_keys($this->_data + $this->_updated);
- } else {
- $keys = array_keys($this->_updated);
- }
+ $keys = array_keys($this->_updated);
return array_combine($keys, array_fill(0, count($keys), true));
}
@@ -386,7 +383,7 @@ public function export() {
public function to($format, array $options = array()) {
switch ($format) {
case 'array':
- $data = $this->_updated + $this->_data;
+ $data = $this->_updated;
$rel = array_map(function($obj) { return $obj->data(); }, $this->_relationships);
$data = array_merge($data, $rel);
$result = Collection::toArray($data, $options);
View
10 data/Model.php
@@ -491,15 +491,21 @@ public static function meta($key = null, $value = null) {
if (is_array($key)) {
$self->_meta = $key + $self->_meta;
}
+
if (!$self->_meta['initialized']) {
$self->_meta['initialized'] = true;
+
if ($self->_meta['source'] === null) {
$self->_meta['source'] = Inflector::tableize($self->_meta['name']);
}
- $titleKeys = array('title', 'name', $self->_meta['key']);
+ $titleKeys = array('title', 'name');
+
+ if (isset($self->_meta['key'])) {
+ $titleKeys = array_merge($titleKeys, (array) $self->_meta['key']);
+ }
$self->_meta['title'] = $self->_meta['title'] ?: static::hasField($titleKeys);
}
- if (is_array($key) || empty($key) || !empty($value)) {
+ if (is_array($key) || !$key || $value) {
return $self->_meta;
}
return isset($self->_meta[$key]) ? $self->_meta[$key] : null;
View
40 data/collection/DocumentArray.php
@@ -21,7 +21,8 @@ class DocumentArray extends \lithium\data\Collection {
protected $_exists = false;
/**
- * Contains an array that is matched against .
+ * Contains the updated value of the array. This value will be persisted to the backend data
+ * store when the array is saved.
*
* @var array
*/
@@ -36,13 +37,18 @@ class DocumentArray extends \lithium\data\Collection {
'data', 'model', 'result', 'query', 'parent', 'stats', 'pathKey', 'exists'
);
+ protected function _init() {
+ parent::_init();
+ $this->_updated = $this->_data;
+ }
+
public function exists() {
return $this->_exists;
}
public function sync($id = null, array $data = array()) {
$this->_exists = true;
- $this->_data = $data ?: $this->_data;
+ $this->_data = $this->_updated;
}
/**
@@ -60,7 +66,7 @@ public function to($format, array $options = array()) {
if ($format == 'array') {
$options += $defaults;
- return Collection::toArray($this->_data, $options);
+ return Collection::toArray($this->_updated, $options);
}
return parent::to($format, $options);
}
@@ -73,7 +79,7 @@ public function to($format, array $options = array()) {
* @return boolean Returns `true` if the field specified in `$name` exists, otherwise `false`.
*/
public function __isset($name) {
- return isset($this->_data[$name]);
+ return isset($this->_updated[$name]);
}
/**
@@ -89,7 +95,7 @@ public function __isset($name) {
* @return void
*/
public function __unset($name) {
- unset($this->_data[$name]);
+ unset($this->_updated[$name]);
}
/**
@@ -99,7 +105,7 @@ public function __unset($name) {
* @return mixed Value at offset.
*/
public function offsetGet($offset) {
- return isset($this->_data[$offset]) ? $this->_data[$offset] : null;
+ return isset($this->_updated[$offset]) ? $this->_updated[$offset] : null;
}
public function offsetSet($offset, $data) {
@@ -108,9 +114,9 @@ public function offsetSet($offset, $data) {
$data = $model::connection()->cast($this, array($this->_pathKey => $data), $options);
}
if ($offset) {
- return $this->_data[$offset] = $data;
+ return $this->_updated[$offset] = $data;
}
- return $this->_data[] = $data;
+ return $this->_updated[] = $data;
}
/**
@@ -120,12 +126,12 @@ public function offsetSet($offset, $data) {
*/
public function rewind() {
$data = parent::rewind();
- $key = key($this->_data);
+ $key = key($this->_updated);
return $this->offsetGet($key);
}
public function current() {
- return $this->offsetGet(key($this->_data));
+ return $this->offsetGet(key($this->_updated));
}
/**
@@ -137,25 +143,27 @@ public function current() {
* available.
*/
public function next() {
- $prev = key($this->_data);
- $this->_valid = (next($this->_data) !== false);
- $cur = key($this->_data);
+ $prev = key($this->_updated);
+ $this->_valid = (next($this->_updated) !== false);
+ $cur = key($this->_updated);
if (!$this->_valid && $cur !== $prev && $cur !== null) {
$this->_valid = true;
}
- return $this->_valid ? $this->offsetGet(key($this->_data)) : null;
+ return $this->_valid ? $this->offsetGet(key($this->_updated)) : null;
}
public function export() {
return array(
'exists' => $this->_exists,
'key' => $this->_pathKey,
- 'data' => $this->_data
+ 'data' => $this->_data,
+ 'update' => $this->_updated
);
}
- protected function _populate($data = null, $key = null) {}
+ protected function _populate($data = null, $key = null) {
+ }
}
?>
View
148 data/entity/Document.php
@@ -69,14 +69,6 @@ class Document extends \lithium\data\Entity implements \Iterator, \ArrayAccess {
protected $_pathKey = null;
/**
- * Contains an array of removed fields, where the field names are the keys, and the values are
- * always `true`.
- *
- * @var array
- */
- protected $_removed = array();
-
- /**
* Contains an array of backend-specific statistics generated by the query that produced this
* `Document` object. These stats are accessible via the `stats()` method.
*
@@ -95,15 +87,13 @@ class Document extends \lithium\data\Entity implements \Iterator, \ArrayAccess {
protected function _init() {
parent::_init();
+
$data = (array) $this->_data;
$this->_data = array();
- $this->set($data);
- $exists = $this->_exists;
-
- $this->_data = $this->_updated;
$this->_updated = array();
- $this->update();
- $this->_exists = $exists;
+
+ $this->set($data, array('init' => true));
+ $this->sync(null, array(), array('materialize' => false));
unset($this->_autoConfig);
}
@@ -118,50 +108,43 @@ public function &__get($name) {
if (strpos($name, '.')) {
return $this->_getNested($name);
}
- if (isset($this->_removed[$name])) {
- $null = null;
- return $null;
- }
+
if (isset($this->_embedded[$name]) && !isset($this->_relationships[$name])) {
- $this->_relationships[$name] = $this->_relate(
- $this->_embedded[$name], isset($this->_data[$name]) ? $this->_data[$name] : array()
- );
+ $item = isset($this->_data[$name]) ? $this->_data[$name] : array();
+ var_dump($this->_relationships[$name]);
+ die('#WINNING');
+ // $this->_relationships[$name] = $this->_relate($this->_embedded[$name], $item);
}
+ $result = parent::__get($name);
- $model = $this->_model;
- $conn = $model ? $model::connection() : null;
+ if ($result !== null || array_key_exists($name, $this->_updated)) {
+ return $result;
+ }
- if ($model && $conn && $schema = $model::schema($name)) {
- if (isset($this->_updated[$name])) {
+ if ($field = $this->schema($name)) {
+ if (isset($field['default'])) {
+ $this->set(array($name => $field['default']));
return $this->_updated[$name];
}
- if (!isset($this->_data[$name])) {
- $schema = array($name => $schema);
- $pathKey = $this->_pathKey ? $this->_pathKey : null;
- $options = compact('pathKey', 'schema') + array('first' => true);
- if (($value = $conn->cast($this, array($name => null), $options)) !== null) {
- $this->_data[$name] = $value;
- return $this->_data[$name];
- }
+ if (isset($field['array']) && $field['array'] && ($model = $this->_model)) {
+ $this->_updated[$name] = $model::connection()->item($model, array(), array(
+ 'class' => 'array'
+ ));
+ return $this->_updated[$name];
}
}
- return parent::__get($name);
- }
-
- protected function _relate($config, $data) {
- if ($model = $this->_model) {
- }
+ $null = null;
+ return $null;
}
public function export() {
foreach ($this->_updated as $key => $val) {
if (is_a($val, __CLASS__)) {
$path = $this->_pathKey ? "{$this->_pathKey}." : '';
$this->_updated[$key]->_pathKey = "{$path}{$key}";
- $this->_updated[$key]->_exists = false;
}
}
- return parent::export() + array('key' => $this->_pathKey, 'remove' => $this->_removed);
+ return parent::export() + array('key' => $this->_pathKey);
}
/**
@@ -206,19 +189,16 @@ protected function _relation($classType, $key, $data, $options = array()) {
}
protected function &_getNested($name) {
- $current =& $this;
+ $current = $this;
$null = null;
$path = explode('.', $name);
$length = count($path) - 1;
foreach ($path as $i => $key) {
- if (is_array($current)) {
- $current =& $current[$key];
- } elseif (isset($current->{$key})) {
- $current =& $current->{$key};
- } else {
+ if (!isset($current[$key])) {
return $null;
}
+ $current = $current[$key];
if (is_scalar($current) && $i < $length) {
return $null;
@@ -237,21 +217,7 @@ protected function &_getNested($name) {
* @return void
*/
public function __set($name, $value = null) {
- $data = is_array($name) ? $name : array($name => $value);
-
- foreach ($data as $key => $val) {
- if (strpos($key, '.')) {
- $this->_setNested($key, $val);
- unset($data[$key]);
- }
- unset($this->_increment[$key], $this->_removed[$key]);
- }
-
- if ($model = $this->_model) {
- $pathKey = $this->_pathKey;
- $data = $model::connection()->cast($this, $data, compact('pathKey'));
- }
- $this->_updated = $data + $this->_updated;
+ $this->set(array($name => $value));
}
protected function _setNested($name, $value) {
@@ -262,24 +228,22 @@ protected function _setNested($name, $value) {
for ($i = 0; $i < $length; $i++) {
$key = $path[$i];
- if (is_array($current) && isset($current[$key])) {
+ if (isset($current[$key])) {
$next =& $current[$key];
- } elseif (isset($current->{$key})) {
- $next =& $current->{$key};
} else {
unset($next);
$next = null;
}
if ($next === null && ($model = $this->_model)) {
- $current->__set($key, $model::connection()->item($model));
+ $current->set(array($key => $model::connection()->item($model)));
$next =& $current->{$key};
}
$current =& $next;
}
if (is_object($current)) {
- $current->__set(end($path), $value);
+ $current->set(array(end($path) => $value));
}
}
@@ -291,8 +255,7 @@ protected function _setNested($name, $value) {
* @return boolean True if the field specified in `$name` exists, false otherwise.
*/
public function __isset($name) {
- $exists = isset($this->_data[$name]) || isset($this->_updated[$name]);
- return ($exists && !isset($this->_removed[$name]));
+ return isset($this->_updated[$name]);
}
/**
@@ -308,9 +271,7 @@ public function __isset($name) {
* @return void
*/
public function __unset($name) {
- $this->_removed[$name] = true;
unset($this->_updated[$name]);
- unset($this->_data[$name]);
}
/**
@@ -321,11 +282,38 @@ public function __unset($name) {
* $doc->set(array('title' => 'Lorem Ipsum', 'value' => 42));
* }}}
*
- * @param $values An associative array of fields and values to assign to the `Document`.
+ * @param array $data An associative array of fields and values to assign to the `Document`.
+ * @param array $options
* @return void
*/
- public function set($values) {
- $this->__set($values);
+ public function set(array $data, array $options = array()) {
+ $defaults = array('init' => false);
+ $options += $defaults;
+
+ foreach ($data as $key => $val) {
+ if (strpos($key, '.')) {
+ $this->_setNested($key, $val);
+ unset($data[$key]);
+ }
+ unset($this->_increment[$key]);
+ }
+
+ if ($data && $model = $this->_model) {
+ $pathKey = $this->_pathKey;
+ $data = $model::connection()->cast($this, $data, compact('pathKey'));
+ }
+
+ foreach ($data as $key => $value) {
+ if (is_a($value, __CLASS__)) {
+ if (!$options['init']) {
+ $value->_exists = false;
+ }
+ $value->_pathKey = ($this->_pathKey ? "{$this->_pathKey}." : '') . $key;
+ $value->_model = $value->_model ?: $this->_model;
+ $value->_schema = $value->_schema ?: $this->_schema;
+ }
+ }
+ $this->_updated = $data + $this->_updated;
}
/**
@@ -348,7 +336,7 @@ public function offsetGet($offset) {
* @return void
*/
public function offsetSet($offset, $value) {
- return $this->__set(array($offset => $value));
+ return $this->set(array($offset => $value));
}
/**
@@ -378,9 +366,9 @@ public function offsetUnset($key) {
* @return mixed The current item after rewinding.
*/
public function rewind() {
- reset($this->_data);
- $this->_valid = (count($this->_data) > 0);
- return current($this->_data);
+ reset($this->_updated);
+ $this->_valid = (count($this->_updated) > 0);
+ return current($this->_updated);
}
/**
@@ -462,10 +450,10 @@ public function increment($field, $value = 1) {
}
$this->_increment[$field] += $value;
- if (!is_numeric($this->_data[$field])) {
+ if (!is_numeric($this->_updated[$field])) {
throw new UnexpectedValueException("Field `{$field}` cannot be incremented.");
}
- $this->_data[$field] += $value;
+ return $this->_updated[$field] += $value;
}
}
View
15 data/model/Relationship.php
@@ -61,7 +61,7 @@ class Relationship extends \lithium\core\Object {
* In this case, the relationship is bound to the `Users` model, but `'Author'` would be the
* relationship name. This is the name with which the relationship is referenced in the
* originating model.
- * - `'keys'` _array_: An array of fields that define the relationship, where the
+ * - `'key'` _mixed_: An array of fields that define the relationship, where the
* keys are fields in the originating model, and the values are fields in the
* target model. If the relationship is not deined by keys, this array should be
* empty.
@@ -94,7 +94,7 @@ class Relationship extends \lithium\core\Object {
public function __construct(array $config = array()) {
$defaults = array(
'name' => null,
- 'keys' => array(),
+ 'key' => array(),
'type' => null,
'to' => null,
'from' => null,
@@ -110,17 +110,16 @@ protected function _init() {
parent::_init();
$config =& $this->_config;
$type = $config['type'];
+
$name = ($type == 'hasOne') ? Inflector::pluralize($config['name']) : $config['name'];
+ $config['fieldName'] = $config['fieldName'] ?: lcfirst($name);
if (!$config['to']) {
$assoc = preg_replace("/\\w+$/", "", $config['from']) . $name;
$config['to'] = Libraries::locate('models', $assoc);
}
- if (!$config['fieldName']) {
- $config['fieldName'] = lcfirst($name);
- }
- if (!$config['keys'] || !is_array($config['keys'])) {
- $config['keys'] = $this->_keys($config['keys']);
+ if (!$config['key'] || !is_array($config['key'])) {
+ $config['key'] = $this->_keys($config['key']);
}
}
@@ -137,7 +136,7 @@ public function constraints() {
$relFrom = $config['from']::meta('name');
$relTo = $config['name'];
- foreach ($this->_config['keys'] as $from => $to) {
+ foreach ($this->_config['key'] as $from => $to) {
$constraints["{$relFrom}.{$from}"] = "{$relTo}.{$to}";
}
return $constraints + (array) $this->_config['constraint'];
View
8 data/source/Database.php
@@ -403,22 +403,22 @@ public function calculation($type, $query, array $options = array()) {
*/
public function relationship($class, $type, $name, array $config = array()) {
$field = Inflector::underscore(Inflector::singularize($name));//($type == 'hasMany') ? : ;
- $keys = "{$field}_id";
+ $key = "{$field}_id";
$primary = $class::meta('key');
if (is_array($primary)) {
- $keys = array_combine($primary, $primary);
+ $key = array_combine($primary, $primary);
} elseif ($type == 'hasMany' || $type == 'hasOne') {
if ($type == 'hasMany') {
$field = Inflector::pluralize($field);
}
$secondary = Inflector::underscore(Inflector::singularize($class::meta('name')));
- $keys = array($primary => "{$secondary}_id");
+ $key = array($primary => "{$secondary}_id");
}
$from = $class;
$fieldName = $field;
- $config += compact('type', 'name', 'keys', 'from', 'fieldName');
+ $config += compact('type', 'name', 'key', 'from', 'fieldName');
return $this->_instance('relationship', $config);
}
View
3 data/source/Http.php
@@ -316,8 +316,7 @@ public function delete($query, array $options = array()) {
*/
public function relationship($class, $type, $name, array $options = array()) {
if (isset($this->_classes['relationship'])) {
- $class = $this->_classes['relationship'];
- return ($class) ? new $class() : null;
+ return $this->_instance('relationship', compact('type', 'name') + $options);
}
return null;
}
View
22 data/source/MongoDb.php
@@ -584,9 +584,9 @@ public function calculation($type, $query, array $options = array()) {
* @return array
*/
public function relationship($class, $type, $name, array $config = array()) {
- $keys = Inflector::camelize($type == 'belongsTo' ? $class::meta('name') : $name, false);
+ $key = Inflector::camelize($type == 'belongsTo' ? $class::meta('name') : $name, false);
- $config += compact('name', 'type', 'keys');
+ $config += compact('name', 'type', 'key');
$config['from'] = $class;
$relationship = $this->_classes['relationship'];
@@ -776,9 +776,10 @@ public function order($order, $context) {
}
public function cast($entity, array $data, array $options = array()) {
- $defaults = array('schema' => null, 'first' => false, 'pathKey' => null);
+ $defaults = array('schema' => null, 'first' => false);
$options += $defaults;
$model = null;
+ $exists = false;
if (!$data) {
return $data;
@@ -788,18 +789,19 @@ public function cast($entity, array $data, array $options = array()) {
$model = $entity;
$entity = null;
$options['schema'] = $options['schema'] ?: $model::schema();
- }
- if ($entity && !$options['schema']) {
- $options['schema'] = $entity->schema();
- }
- if ($entity) {
+ } elseif ($entity) {
+ $options['schema'] = $options['schema'] ?: $entity->schema();
$model = $entity->model();
+
+ if (is_a($entity, $this->_classes['entity'])) {
+ $exists = $entity->exists();
+ }
}
$schema = $options['schema'] ?: array('_id' => array('type' => 'id'));
unset($options['schema']);
- $exporter = $this->_classes['exporter'];
- $options += compact('model') + array('handlers' => $this->_handlers);
+ $exporter = $this->_classes['exporter'];
+ $options += compact('model', 'exists') + array('handlers' => $this->_handlers);
return parent::cast($entity, $exporter::cast($data, $schema, $this, $options), $options);
}
View
131 data/source/mongo_db/Exporter.php
@@ -12,7 +12,15 @@
class Exporter extends \lithium\core\StaticObject {
- protected static $_map = array(
+ protected static $_commands = array(
+ 'create' => null,
+ 'update' => '$set',
+ 'increment' => '$inc',
+ 'remove' => '$unset',
+ 'rename' => '$rename'
+ );
+
+ protected static $_types = array(
'MongoId' => 'id',
'MongoDate' => 'date',
'MongoCode' => 'code',
@@ -34,13 +42,16 @@ public static function get($type, $export, array $options = array()) {
public static function cast($data, $schema, $database, array $options = array()) {
$defaults = array(
- 'handlers' => array(), 'model' => null, 'arrays' => true
+ 'handlers' => array(),
+ 'model' => null,
+ 'arrays' => true,
+ 'pathKey' => null
);
$options += $defaults;
foreach ($data as $key => $value) {
- $pathKey = !empty($options['pathKey']) ? "{$options['pathKey']}.{$key}" : $key;
- $field = (isset($schema[$pathKey]) ? $schema[$pathKey] : array());
+ $pathKey = $options['pathKey'] ? "{$options['pathKey']}.{$key}" : $key;
+ $field = isset($schema[$pathKey]) ? $schema[$pathKey] : array();
$field += array('type' => null, 'array' => null);
$data[$key] = static::_cast($value, $field, $database, compact('pathKey') + $options);
}
@@ -52,7 +63,7 @@ protected static function _cast($value, $def, $database, $options) {
return $value;
}
$pathKey = $options['pathKey'];
- $typeMap = static::$_map;
+ $typeMap = static::$_types;
$type = isset($typeMap[$def['type']]) ? $typeMap[$def['type']] : $def['type'];
$isObject = ($type == 'object');
@@ -78,20 +89,14 @@ protected static function _cast($value, $def, $database, $options) {
$arrayType = !$isObject && (array_keys($value) === range(0, count($value) - 1));
$opts = $arrayType ? array('class' => 'array') + $options : $options;
}
+ unset($opts['handlers'], $opts['first']);
return $database->item($options['model'], $value, compact('pathKey') + $opts);
}
public static function toCommand($changes) {
- $map = array(
- 'create' => null,
- 'update' => '$set',
- 'increment' => '$inc',
- 'remove' => '$unset',
- 'rename' => '$rename'
- );
$result = array();
- foreach ($map as $from => $to) {
+ foreach (static::$_commands as $from => $to) {
if (!isset($changes[$from])) {
continue;
}
@@ -105,9 +110,8 @@ public static function toCommand($changes) {
}
protected static function _create($export, array $options) {
- $export += array('data' => array(), 'update' => array(), 'remove' => array(), 'key' => '');
- $data = array_merge($export['data'], $export['update']);
- $data = array_diff_key($data, $export['remove']);
+ $export += array('data' => array(), 'update' => array(), 'key' => '');
+ $data = $export['update'];
$result = array('create' => array());
$localOpts = array('finalize' => false) + $options;
@@ -120,47 +124,88 @@ protected static function _create($export, array $options) {
return ($options['finalize']) ? array('create' => $data) : $data;
}
+ /**
+ * Calculates changesets for update operations, and produces an array which can be converted to
+ * a set of native MongoDB update operations.
+ *
+ * @todo Implement remove and rename.
+ * @param array $export An array of fields exported from a call to `Document::export()`, and
+ * should contain the following keys:
+ * - `'data'` _array_: An array representing the original data loaded from the
+ * database for the document.
+ * - `'update'` _array_: An array representing the current state of the document,
+ * containing any modifications made.
+ * - `'key'` _string_: If this is a nested document, this is a dot-separated path
+ * from the root-level document.
+ * @return array Returns an array representing the changes to be made to the document. These
+ * are converted to database-native commands by the `toCommand()` method.
+ */
protected static function _update($export) {
- $export += array('update' => array(), 'remove' => array(), 'key' => '');
+ $export += array(
+ 'data' => array(),
+ 'update' => array(),
+ 'remove' => array(),
+ 'rename' => array(),
+ 'key' => ''
+ );
$path = $export['key'] ? "{$export['key']}." : "";
- $data = $export['update'];
- $result = array();
+ $result = array('update' => array(), 'remove' => array());
- if (!$export['exists']) {
- $data = array_merge($export['data'], $data);
- }
- $data = array_diff_key($data, $export['remove']);
- $nested = array_diff_key($export['data'], $data);
+ $left = static::_diff($export['data'], $export['update']);
+ $right = static::_diff($export['update'], $export['data']);
- foreach ($export['remove'] as $key => $val) {
- $result['remove']["{$path}{$key}"] = $val;
- }
+ $objects = array_filter($export['update'], function($value) {
+ return (is_object($value) && method_exists($value, 'export'));
+ });
- foreach ($data as $key => $val) {
- if (is_object($val) && method_exists($val, 'export')) {
- $result = static::_appendObject($result, $path, $key, $val);
- continue;
- }
- $result['update']["{$path}{$key}"] = $val;
+ foreach (array_merge($right, $objects) as $key => $value) {
+ $result = static::_append($result, "{$path}{$key}", $value);
}
+ return array_filter($result);
+ }
- foreach (array_diff_key($nested, $export['remove']) as $key => $val) {
- if (is_object($val) && method_exists($val, 'export')) {
- $result = static::_appendObject($result, $path, $key, $val);
+ /**
+ * Handle diffing operations between `Document` object states. Implemented because all of PHP's
+ * array comparison functions are broken when working with objects.
+ *
+ * @param array $left The left-hand comparison array.
+ * @param array $right The right-hand comparison array.
+ * @return array Returns an array of the differences of `$left` compared to `$right`.
+ */
+ protected static function _diff($left, $right) {
+ $result = array();
+
+ foreach ($left as $key => $value) {
+ if (!isset($right[$key]) || $left[$key] !== $right[$key]) {
+ $result[$key] = $value;
}
}
return $result;
}
- protected static function _appendObject($changes, $path, $key, $object) {
- $options = array('finalize' => false);
-
- if ($object->exists()) {
- return Set::merge($changes, static::_update($object->export()));
+ /**
+ * Handles appending nested objects to document changesets.
+ *
+ * @param array $changes The full set of changes to be made to the database.
+ * @param string $key The key of the field to append, which may be a dot-separated path if the
+ * value is or is contained within a nested object.
+ * @param mixed $value The value to append to the changeset. Can be a scalar value, array, a
+ * nested object, or part of a nested object.
+ * @return array Returns the value of `$changes`, with any new changed values appended.
+ */
+ protected static function _append($changes, $key, $value) {
+ $options = array('finalize' => false);
+
+ if (!is_object($value) || !method_exists($value, 'export')) {
+ $changes['update'][$key] = $value;
+ return $changes;
+ }
+ if ($value->exists()) {
+ return Set::merge($changes, static::_update($value->export()));
}
- $changes['update']["{$path}{$key}"] = static::_create($object->export(), $options);
+ $changes['update'][$key] = static::_create($value->export(), $options);
return $changes;
}
}
-?>
+?>
View
4 tests/cases/data/ModelTest.php
@@ -163,7 +163,7 @@ public function testRelationshipIntrospection() {
$expected = array(
'name' => 'MockPost',
'type' => 'belongsTo',
- 'keys' => array('mock_post_id' => 'id'),
+ 'key' => array('mock_post_id' => 'id'),
'from' => 'lithium\tests\mocks\data\MockComment',
'to' => 'lithium\tests\mocks\data\MockPost',
'link' => 'key',
@@ -183,7 +183,7 @@ public function testRelationshipIntrospection() {
'from' => 'lithium\tests\mocks\data\MockPost',
'to' => 'lithium\tests\mocks\data\MockComment',
'fields' => true,
- 'keys' => array('id' => 'mock_post_id'),
+ 'key' => array('id' => 'mock_post_id'),
'link' => 'key',
'fieldName' => 'mock_comments',
'constraint' => array(),
View
94 tests/cases/data/entity/DocumentTest.php
@@ -201,15 +201,13 @@ public function testSetMultipleNested() {
$doc = new Document(array('model' => $this->_model));
$doc->id = 123;
$doc->type = 'father';
+
$doc->set(array('children' => array(
array('id' => 124, 'type' => 'child', 'children' => null),
array('id' => 125, 'type' => 'child', 'children' => null)
)));
$this->assertEqual('father', $doc->type);
-
- $this->assertTrue(is_object($doc->children), 'children is not an object');
-
$this->assertTrue($doc->children instanceof DocumentArray);
$expected = array('id' => 124, 'type' => 'child', 'children' => null);
@@ -279,7 +277,7 @@ public function testUpdateWithSingleKey() {
$doc->_id = 3;
$this->assertFalse($doc->exists());
- $doc->update(12);
+ $doc->sync(12);
$this->assertTrue($doc->exists());
$this->assertEqual(12, $doc->_id);
}
@@ -295,7 +293,7 @@ public function testUpdateWithMultipleKeys() {
$doc->id = 3;
$this->assertFalse($doc->exists());
- $doc->update(array(12, '1-2'));
+ $doc->sync(array(12, '1-2'));
$this->assertTrue($doc->exists());
$this->assertEqual(12, $doc->id);
@@ -545,8 +543,7 @@ public function testExport() {
$expected = array(
'data' => array('foo' => 'bar', 'baz' => 'dib'),
- 'update' => array(),
- 'remove' => array(),
+ 'update' => array('foo' => 'bar', 'baz' => 'dib'),
'increment' => array(),
'key' => '',
'exists' => false
@@ -555,10 +552,25 @@ public function testExport() {
}
/**
+ * Tests that documents nested within existing documents also exist, and vice versa.
+ */
+ public function testNestedObjectExistence() {
+ $model = $this->_model;
+ $data = array('foo' => array('bar' => 'bar', 'baz' => 'dib'));
+ $doc = new Document(compact('model', 'data') + array('exists' => false));
+
+ $this->assertFalse($doc->exists());
+ $this->assertFalse($doc->foo->exists());
+
+ $doc = new Document(compact('model', 'data') + array('exists' => true));
+
+ $this->assertTrue($doc->exists());
+ $this->assertTrue($doc->foo->exists());
+ }
+
+ /**
* Tests that a modified `Document` exports the proper fields in a newly-appended nested
* `Document`.
- *
- * @return void
*/
public function testModifiedExport() {
$model = $this->_model;
@@ -571,63 +583,64 @@ public function testModifiedExport() {
$expected = array('foo' => 'bar', 'baz' => 'dib', 'nested.more' => 'data');
$this->assertFalse($newData['exists']);
$this->assertEqual(array('foo' => 'bar', 'baz' => 'dib'), $newData['data']);
- $this->assertEqual(1, count($newData['update']));
+ $this->assertEqual(3, count($newData['update']));
$this->assertTrue($newData['update']['nested'] instanceof Document);
$result = $newData['update']['nested']->export();
$this->assertFalse($result['exists']);
$this->assertEqual(array('more' => 'data'), $result['data']);
- $this->assertFalse($result['update']);
+ $this->assertEqual(array('more' => 'data'), $result['update']);
$this->assertEqual('nested', $result['key']);
$doc = new Document(compact('model') + array('exists' => true, 'data' => array(
'foo' => 'bar', 'baz' => 'dib'
)));
$result = $doc->export();
- $this->assertFalse($result['update']);
+ $this->assertEqual($result['data'], $result['update']);
$doc->nested = array('more' => 'data');
$this->assertEqual('data', $doc->nested->more);
$modified = $doc->export();
$this->assertTrue($modified['exists']);
$this->assertEqual(array('foo' => 'bar', 'baz' => 'dib'), $modified['data']);
- $this->assertEqual(array('nested'), array_keys($modified['update']));
+ $this->assertEqual(array('nested', 'foo', 'baz'), array_keys($modified['update']));
$this->assertNull($modified['key']);
$nested = $modified['update']['nested']->export();
$this->assertFalse($nested['exists']);
$this->assertEqual(array('more' => 'data'), $nested['data']);
$this->assertEqual('nested', $nested['key']);
- $doc->update();
+ $doc->sync();
$result = $doc->export();
- $this->assertFalse($result['update']);
+ $this->assertEqual($result['data'], $result['update']);
$doc->more = 'cowbell';
$doc->nested->evenMore = 'cowbell';
$modified = $doc->export();
- $expected = array('more' => 'cowbell');
+ $expected = array('more' => 'cowbell') + $modified['data'];
$this->assertEqual($expected, $modified['update']);
$this->assertEqual(array('nested', 'foo', 'baz'), array_keys($modified['data']));
$this->assertEqual('bar', $modified['data']['foo']);
$this->assertEqual('dib', $modified['data']['baz']);
+ $this->assertTrue($modified['exists']);
$nested = $modified['data']['nested']->export();
$this->assertTrue($nested['exists']);
$this->assertEqual(array('more' => 'data'), $nested['data']);
- $this->assertEqual(array('evenMore' => 'cowbell'), $nested['update']);
+ $this->assertEqual(array('evenMore' => 'cowbell') + $nested['data'], $nested['update']);
$this->assertEqual('nested', $nested['key']);
- $doc->update();
+ $doc->sync();
$doc->nested->evenMore = 'foo!';
$modified = $doc->export();
- $this->assertFalse($modified['update']);
+ $this->assertEqual($modified['data'], $modified['update']);
$nested = $modified['data']['nested']->export();
- $this->assertEqual(array('evenMore' => 'foo!'), $nested['update']);
+ $this->assertEqual(array('evenMore' => 'foo!') + $nested['data'], $nested['update']);
}
public function testArrayConversion() {
@@ -640,6 +653,31 @@ public function testArrayConversion() {
$this->assertEqual(time(), $result['date']);
}
+ public function testArrayInterface() {
+ $doc = new Document();
+ $doc->field = 'value';
+ $this->assertEqual('value', $doc['field']);
+
+ $doc['field'] = 'newvalue';
+ $this->assertEqual('newvalue', $doc->field);
+
+ unset($doc['field']);
+ $this->assertNull($doc->field);
+ }
+
+ /**
+ * Tests that unassigned fields with default schema values are auto-populated at access time.
+ */
+ public function testSchemaValueInitialization() {
+ $doc = new Document(array('schema' => array(
+ 'foo' => array('type' => 'string', 'default' => 'bar')
+ )));
+ $this->assertFalse($doc->data());
+
+ $this->assertEqual('bar', $doc->foo);
+ $this->assertEqual(array('foo' => 'bar'), $doc->data());
+ }
+
public function testInitializationWithNestedFields() {
$doc = new Document(array('model' => $this->_model, 'data' => array(
'simple' => 'value',
@@ -656,6 +694,22 @@ public function testInitializationWithNestedFields() {
$this->assertEqual(array('nested', 'really', 'simple'), $result);
}
+ public function testWithArraySchemaReusedName() {
+ $model = $this->_model;
+ $schema = array(
+ '_id' => array('type' => 'id'),
+ 'bar' => array('array' => true),
+ 'foo' => array('type' => 'object', 'array' => true),
+ 'foo.foo' => array('type' => 'integer'),
+ 'foo.bar' => array('type' => 'integer')
+ );
+ $doc = new Document(compact('model', 'schema'));
+ $doc->foo[] = array('foo' => 1, 'bar' => 100);
+
+ $expected = array('foo' => array(array('foo' => 1, 'bar' => 100)));
+ $this->assertEqual($expected, $doc->data());
+ }
+
public function testIdGetDoesNotSet() {
$document = MockDocumentPost::create();
$message = 'The `_id` key should not be set.';
View
4 tests/cases/data/source/DatabaseTest.php
@@ -628,13 +628,13 @@ public function testRelationshipGeneration() {
$hasMany = $this->db->relationship($this->_model, 'hasMany', 'Comments', array(
'to' => $comment
));
- $this->assertEqual(array('id' => 'mock_database_post_id'), $hasMany->keys());
+ $this->assertEqual(array('id' => 'mock_database_post_id'), $hasMany->key());
$this->assertEqual('comments', $hasMany->fieldName());
$belongsTo = $this->db->relationship($comment, 'belongsTo', 'Posts', array(
'to' => $this->_model
));
- $this->assertEqual(array('post_id' => 'id'), $belongsTo->keys());
+ $this->assertEqual(array('post_id' => 'id'), $belongsTo->key());
$this->assertEqual('post', $belongsTo->fieldName());
}
View
23 tests/cases/data/source/MongoDbTest.php
@@ -464,7 +464,7 @@ public function testRelationshipGeneration() {
$expected = array(
'name' => 'MockPost',
'type' => 'belongsTo',
- 'keys' => array('mockComment' => '_id'),
+ 'key' => array('mockComment' => '_id'),
'from' => $from,
'link' => 'contained',
'to' => $to,
@@ -620,29 +620,12 @@ public function testUpdateWithEmbeddedObjects() {
$query = new Query(array('type' => 'update') + compact('entity'));
$result = $query->export($this->db);
- $this->assertEqual(array('updated'), array_keys($result['data']['update']));
+ $expected = array('updated', '_id', 'created', 'list');
+ $this->assertEqual($expected, array_keys($result['data']['update']));
$this->assertTrue($result['data']['update']['updated'] instanceof MongoDate);
}
/**
- * Test that subobjects are properly casted on createing a new Document
- *
- * @return void
- */
- public function testSubobjectCastingOnSave() {
- $model = $this->_model; // 'lithium\tests\mocks\data\source\MockMongoPost'
- $schema = array('sub.foo' => array('type'=>'boolean'), 'bar' => array('type'=>'boolean'));
- $data = array('sub' => array('foo' => '0'), 'bar' => '1');
- $entity = new Document(compact('data', 'schema', 'model'));
- $this->assertIdentical(true, $entity->bar);
- $this->assertIdentical(false, $entity->sub->foo);
- $data = array('sub.foo' => '1', 'bar' => '0');
- $entity = new Document(compact('data', 'schema', 'model'));
- $this->assertIdentical(false, $entity->bar);
- $this->assertIdentical(true, $entity->sub->foo);
- }
-
- /**
* Assert that Mongo and the Mongo Exporter don't mangle manual geospatial queries.
*
* @return void
View
82 tests/cases/data/source/mongo_db/ExporterTest.php
@@ -81,6 +81,7 @@ public function testCreateWithFixedData() {
$this->assertTrue($doc->_id instanceof MongoId);
$result = Exporter::get('create', $doc->export());
+ $data = $doc->export();
$this->assertTrue($result['create']['_id'] instanceof MongoId);
$this->assertTrue($result['create']['created'] instanceof MongoDate);
$this->assertIdentical(time(), $result['create']['created']->sec);
@@ -127,13 +128,20 @@ public function testUpdateWithNoChanges() {
}
public function testUpdateWithSubObjects() {
- $doc = new Document(array('exists' => true, 'data' => array(
- 'numbers' => new DocumentArray(array('data' => array(7, 8, 9))),
- 'deeply' => new Document(array(
- 'pathKey' => 'deeply', 'exists' => true, 'data' => array('nested' => 'object')
+ $model = $this->_model;
+ $exists = true;
+ $model::config(array('key' => '_id'));
+
+ $doc = new Document(compact('model', 'exists') + array('data' => array(
+ 'numbers' => new DocumentArray(compact('model', 'exists') + array(
+ 'data' => array(7, 8, 9), 'pathKey' => 'numbers'
+ )),
+ 'deeply' => new Document(compact('model', 'exists') + array(
+ 'pathKey' => 'deeply', 'data' => array('nested' => 'object')
)),
'foo' => 'bar'
)));
+
$doc->field = 'value';
$doc->deeply->nested = 'foo';
$doc->newObject = new Document(array(
@@ -144,10 +152,12 @@ public function testUpdateWithSubObjects() {
$this->assertEqual('subValue', $doc->newObject->subField);
$result = Exporter::get('update', $doc->export());
- $this->assertFalse(isset($result['update']['foo']));
- $this->assertEqual('value', $result['update']['field']);
- $this->assertEqual(array('subField' => 'subValue'), $result['update']['newObject']);
- $this->assertEqual('foo', $result['update']['deeply.nested']);
+ $expected = array(
+ 'newObject' => array('subField' => 'subValue'),
+ 'field' => 'value',
+ 'deeply.nested' => 'foo'
+ );
+ $this->assertEqual($expected, $result['update']);
}
public function testFieldRemoval() {
@@ -165,8 +175,7 @@ public function testFieldRemoval() {
$expected = array(
'foo' => true, 'flagged' => true, 'numbers' => true, 'deeply.nested' => true
);
- $this->assertEqual($expected, $result['remove']);
- $this->assertEqual(array('bar' => 'dib'), $result['update']);
+ $this->assertEqual(array('update' => array('bar' => 'dib')), $result);
}
/**
@@ -279,9 +288,9 @@ public function testWithArraySchema() {
$model = $this->_model;
$model::schema(array(
'_id' => array('type' => 'id'),
- 'list' => array('array' => true),
- 'list.foo' => array('type' => 'string'),
- 'list.bar' => array('type' => 'string')
+ 'list' => array('type' => 'string', 'array' => true),
+ 'obj.foo' => array('type' => 'string'),
+ 'obj.bar' => array('type' => 'string')
));
$doc = new Document(compact('model'));
$doc->list[] = array('foo' => '!!', 'bar' => '??');
@@ -309,20 +318,45 @@ public function testWithArraySchema() {
$this->assertEqual($result['update'], $data);
}
- public function testWithArraySchemaReusedName() {
+ /**
+ * Test that subobjects are properly casted on creating a new Document
+ */
+ public function testSubObjectCastingOnSave() {
$model = $this->_model;
$model::schema(array(
- '_id' => array('type' => 'id'),
- 'bar' => array('array' => true),
- 'foo' => array('array' => true),
- 'foo.foo' => array('type' => 'integer'),
- 'foo.bar' => array('type' => 'integer')
- ));
- $doc = new Document(compact('model'));
- $doc->foo[] = array('foo' => 1, 'bar' => 100);
+ 'sub.foo' => array('type' => 'boolean'),
+ 'bar' => array('type' => 'boolean')
+ ));
+ $data = array('sub' => array('foo' => 0), 'bar' => 1);
+ $doc = new Document(compact('data', 'model'));
+
+ $this->assertIdentical(true, $doc->bar);
+ $this->assertIdentical(false, $doc->sub->foo);
- $expected = array('foo' => array(array('foo' => 1, 'bar' => 100)));
- $this->assertEqual($expected, $doc->data());
+ $data = array('sub.foo' => '1', 'bar' => '0');
+ $doc = new Document(compact('data', 'model', 'schema'));
+
+ $this->assertIdentical(false, $doc->bar);
+ $this->assertIdentical(true, $doc->sub->foo);
+ }
+
+ /**
+ * Tests that a nested key on a previously saved document gets updated properly.
+ */
+ public function testExistingNestedKeyOverwrite() {
+ $doc = new Document(array('model' => $this->_model));
+ $doc->{'this.that'} = 'value1';
+ $this->assertEqual(array('this' => array('that' => 'value1')), $doc->data());
+
+ $result = Exporter::get('create', $doc->export());
+ $this->assertEqual(array('create' => array('this' => array('that' => 'value1'))), $result);
+
+ $doc->sync();
+ $doc->{'this.that'} = 'value2';
+ $this->assertEqual(array('this' => array('that' => 'value2')), $doc->data());
+
+ $result = Exporter::get('update', $doc->export());
+ $this->assertEqual(array('update' => array('this.that' => 'value2')), $result);
}
/**
View
8 tests/mocks/data/MockSource.php
@@ -137,22 +137,22 @@ public function cast($entity, array $data = array(), array $options = array()) {
public function relationship($class, $type, $name, array $config = array()) {
$field = Inflector::underscore(Inflector::singularize($name));//($type == 'hasMany') ? : ;
- $keys = "{$field}_id";
+ $key = "{$field}_id";
$primary = $class::meta('key');
if (is_array($primary)) {
- $keys = array_combine($primary, $primary);
+ $key = array_combine($primary, $primary);
} elseif ($type == 'hasMany' || $type == 'hasOne') {
if ($type == 'hasMany') {
$field = Inflector::pluralize($field);
}
$secondary = Inflector::underscore(Inflector::singularize($class::meta('name')));
- $keys = array($primary => "{$secondary}_id");
+ $key = array($primary => "{$secondary}_id");
}
$from = $class;
$fieldName = $field;
- $config += compact('type', 'name', 'keys', 'from', 'fieldName');
+ $config += compact('type', 'name', 'key', 'from', 'fieldName');
return $this->_instance('relationship', $config);
}
View
9 tests/mocks/data/model/MockDocumentSource.php
@@ -48,6 +48,7 @@ public function cast($entity, array $data, array $options = array()) {
$defaults = array('schema' => null, 'first' => false, 'pathKey' => null, 'arrays' => true);
$options += $defaults;
$model = null;
+ $exists = false;
if (!$data) {
return $data;
@@ -57,8 +58,12 @@ public function cast($entity, array $data, array $options = array()) {
$options['schema'] = $entity->schema() ?: array('_id' => array('type' => 'id'));
}
if ($entity) {
+ if (!is_a($entity, $this->_classes['set'])) {
+ $exists = $entity->exists();
+ }
$model = $entity->model();
}
+ $options['exists'] = $exists;
$schema = $options['schema'];
unset($options['schema']);
@@ -137,9 +142,9 @@ public function result($type, $resource, $context) {
}
public function relationship($class, $type, $name, array $options = array()) {
- $keys = Inflector::camelize($type == 'belongsTo' ? $name : $class::meta('name'));
+ $key = Inflector::camelize($type == 'belongsTo' ? $name : $class::meta('name'));
- $options += compact('name', 'type', 'keys');
+ $options += compact('name', 'type', 'key');
$options['from'] = $class;
$relationship = $this->_classes['relationship'];

0 comments on commit 18b6a8c

Please sign in to comment.