Permalink
Browse files

Add virtual fields

define in the model as functions
identify in the model using $_properties
  • Loading branch information...
1 parent 86ec504 commit 49d785126be05deb0e666bedbf31cecb60cfd4be @hans-d committed Jul 17, 2012
Showing with 250 additions and 20 deletions.
  1. +141 −6 data/Entity.php
  2. +5 −14 data/entity/Document.php
  3. +48 −0 tests/cases/data/EntityTest.php
  4. +56 −0 tests/mocks/data/MockModelVirtual.php
View
147 data/Entity.php
@@ -75,6 +75,16 @@ class Entity extends \lithium\core\Object {
protected $_updated = array();
/**
+ * Contains the callables for recognized virtual fields. These values will not be persisted.
+ * Virtual fields are derived fields that are made available via functions in the model.
+ *
+ * @see lithium\data\Entity::_initVirtual();
+ * @var mixed - array when holding data
+ * - null when not yet initialized
+ */
+ protected $_virtual = null;
+
+ /**
* 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 decremented by.
@@ -130,6 +140,67 @@ public function __construct(array $config = array()) {
parent::__construct($config + $defaults);
}
+ /**
+ * Initializes the virtual field access.
+ *
+ * Only called when fields are accessed that are not yet in _updated or
+ * _relationships.
+ * Information is retrieved the static property `_properties` defined in the model
+ * where the Entity is bound too.
+ * {{{
+ * class Examples extends \lithium\data\Model {
+ *
+ * public static $_properties = array(
+ * 'field_x' => 'MyFieldX',
+ * 'field_y' => array('SomeOtherField', 'set' => false),
+ * 'field_z' => array('get' => 'mySpecialFunction')
+ * );
+ *
+ * // rest of the class ...
+ * }}}
+ *
+ * The example above will introduce the fields 'field_x', 'field_z' that can be used
+ * like normal fields, eg `echo $entity->field_x;`.
+ * When a field is read, set, or isset, methods defined in the Model will be called.
+ * For 'field_x', those will be 'getMyFieldX()', 'setMyFieldX()', 'issetMyFieldX()';
+ * the value given prefixed with 'get', 'set' and 'isset'. As first paramater they
+ * should accept $entity, and only 'set' needs a second parameter for the $value to
+ * be set.
+ * For 'field_y', no setter method is called as indicated by setting 'set' to false.
+ * The entity will set the value just to _updated[] like any other set action.
+ * For 'field_z', the getter method is named 'mySpecialFunction', for 'set' and 'isset'
+ * the method base name will be 'field_z'.
+ */
+ protected function _initVirtual() {
+ $this->_virtual = array('get' => array(), 'set' => array(), 'isset' => array());
+ if (!$model = $this->_model) {
+ return;
+ }
+ if (!class_exists($model) || !isset($model::$_properties) || !is_array($model::$_properties)) {
+ return;
+ }
+ foreach ($model::$_properties as $property => $options) {
+ if (!is_string($property)) {
+ continue;
+ }
+ $defaults = array(
+ 0 => $property, 'get' => true, 'set' => true, 'isset' => true
+ );
+ $options = (array)$options + $defaults;
+ $baseName = $options[0];
+ foreach (array('get', 'set', 'isset') as $key) {
+ $name = $options[$key];
+ if (!$name) {
+ continue;
+ }
+ if ($name === true) {
+ $name = $key . $baseName;
+ }
+ $this->_virtual[$key][$property] = $name;
+ }
+ }
+ }
+
protected function _init() {
parent::_init();
$this->_updated = $this->_data;
@@ -138,6 +209,11 @@ protected function _init() {
/**
* Overloading for reading inaccessible properties.
*
+ * Check '_relationships' and '_updated' first. When
+ * not found, make sure that '_virtual' is initialized and
+ * try to access it as a virtual property.
+ * If nothing is found, null will be returned.
+ *
* @param string $name Property name.
* @return mixed Result.
*/
@@ -148,13 +224,27 @@ public function &__get($name) {
if (isset($this->_updated[$name])) {
return $this->_updated[$name];
}
+ if (!isset($this->_virtual)) {
+ $this->_initVirtual();
+ }
+ if (isset($this->_virtual['get'][$name])) {
+ $getter = $this->_virtual['get'][$name];
+ $result = $this->$getter();
+ return $result;
+ }
$null = null;
return $null;
}
/**
* Overloading for writing to inaccessible properties.
*
+ * If called with only an array as first parameter, call __set
+ * on the individual entries.
+ *
+ * First checks if the property is already known. Otherwise try it via
+ * the virtual setter. Else treat is a new normal property.
+ *
* @param string $name Property name.
* @param string $value Property value.
* @return mixed Result.
@@ -163,17 +253,39 @@ public function __set($name, $value = null) {
if (is_array($name) && !$value) {
return array_map(array(&$this, '__set'), array_keys($name), array_values($name));
}
- $this->_updated[$name] = $value;
+ if (isset($this->_updated[$name])) {
+ return $this->_updated[$name] = $value;
+ }
+ if (!isset($this->_virtual)) {
+ $this->_initVirtual();
+ }
+ if (isset($this->_virtual['set'][$name])) {
+ $setter = $this->_virtual['set'][$name];
+ return $this->$setter($value);
+ }
+ return $this->_updated[$name] = $value;
}
/**
* Overloading for calling `isset()` or `empty()` on inaccessible properties.
*
+ * Will check existing properties first before going virtual.
+ *
* @param string $name Property name.
* @return mixed Result.
*/
public function __isset($name) {
- return isset($this->_updated[$name]) || isset($this->_relationships[$name]);
+ if (isset($this->_updated[$name]) || isset($this->_relationships[$name])) {
+ return true;
+ }
+ if (!isset($this->_virtual)) {
+ $this->_initVirtual();
+ }
+ if (isset($this->_virtual['isset'][$name])) {
+ $issetter = $this->_virtual['isset'][$name];
+ return $this->$issetter();
+ }
+ return false;
}
/**
@@ -384,28 +496,51 @@ public function modified() {
return $fields;
}
- public function export() {
- return array(
+ public function export(array $options=array()) {
+ $options += array('virtual' => false);
+ $export = array(
'exists' => $this->_exists,
'data' => $this->_data,
'update' => $this->_updated,
'increment' => $this->_increment
);
+ if ($options['virtual']) {
+ $export['virtual'] = $this->_exportVirtual();
+ }
+ return $export;
+ }
+
+ protected function _exportVirtual() {
+ if (!isset($this->_virtual)) {
+ $this->_initVirtual();
+ }
+ $export = array();
+ foreach ($this->_virtual['get'] as $name => $getter) {
+ $export[$name] = $this->$getter();
+ }
+ return $export;
}
/**
* Converts the data in the record set to a different format, i.e. an array.
*
+ * Optionally exports virtual fields.
+ *
* @param string $format currently only `array`
- * @param array $options
+ * @param array $options 'virtual' true will export virtual fields. Default false
* @return mixed
*/
public function to($format, array $options = array()) {
+ $options += array('virtual' => false);
switch ($format) {
case 'array':
$data = $this->_updated;
$rel = array_map(function($obj) { return $obj->data(); }, $this->_relationships);
- $data = $rel + $data;
+ $extra = array();
+ if ($options['virtual']) {
+ $extra = $this->_exportVirtual();
+ }
+ $data = $rel + $data + $extra;
$result = Collection::toArray($data, $options);
break;
default:
View
19 data/entity/Document.php
@@ -115,7 +115,9 @@ public function &__get($name) {
}
$result = parent::__get($name);
- if ($result !== null || array_key_exists($name, $this->_updated)) {
+ if ($result !== null || array_key_exists($name, $this->_updated)
+ || array_key_exists($name, $this->_virtual['get'])
+ ) {
return $result;
}
@@ -135,14 +137,14 @@ public function &__get($name) {
return $null;
}
- public function export() {
+ public function export(array $options=array()) {
foreach ($this->_updated as $key => $val) {
if ($val instanceof self) {
$path = $this->_pathKey ? "{$this->_pathKey}." : '';
$this->_updated[$key]->_pathKey = "{$path}{$key}";
}
}
- return parent::export() + array('key' => $this->_pathKey);
+ return parent::export($options) + array('key' => $this->_pathKey);
}
/**
@@ -246,17 +248,6 @@ protected function _setNested($name, $value) {
}
/**
- * PHP magic method used to check the presence of a field as document properties, i.e.
- * `$document->_id`.
- *
- * @param $name The field name, as specified with an object property.
- * @return boolean True if the field specified in `$name` exists, false otherwise.
- */
- public function __isset($name) {
- return isset($this->_updated[$name]);
- }
-
- /**
* PHP magic method used when unset() is called on a `Document` instance.
* Use case for this would be when you wish to edit a document and remove a field, ie.:
* {{{
View
48 tests/cases/data/EntityTest.php
@@ -119,6 +119,54 @@ public function testModified() {
$entity->set($data);
$this->assertEqual(array('foo' => true, 'baz' => true), $entity->modified());
}
+
+ public function testVirtual() {
+ $model = 'lithium\tests\mocks\data\MockModelVirtual';
+ $entity = new Entity(array('model' => $model, 'data' => array('foo' => true)));
+ $this->assertTrue($entity->validates());
+
+ $this->assertEqual(true, $entity->foo);
+ $this->assertTrue(isset($entity->foo));
+ $this->assertFalse(isset($entity->bar));
+
+ $this->assertFalse(isset($entity->fielda));
+ $entity->fielda = 'a';
+ $this->assertTrue(isset($entity->fielda));
+ $this->assertTrue(isset($entity->bar));
+ $this->assertEqual('a', $entity->fielda);
+ $this->assertEqual('a', $entity->bar);
+ $entity->bar = null;
+
+ $this->assertFalse(isset($entity->fieldb));
+ $entity->fieldb = 'b';
+ $this->assertTrue(isset($entity->fieldb));
+ $this->assertFalse(isset($entity->bar));
+ $this->assertEqual('b', $entity->fieldb);
+ $this->assertEqual(null, $entity->bar);
+ $entity->bar = null;
+
+ $this->assertFalse(isset($entity->field_c));
+ $entity->bar = 'c';
+ $this->assertTrue(isset($entity->field_c));
+ $this->assertTrue(isset($entity->bar));
+ $this->assertEqual('c', $entity->field_c);
+ $this->assertEqual('c', $entity->bar);
+
+ $export = $entity->export();
+ $expected = array('exists', 'data', 'update', 'increment');
+ $this->assertEqual($expected, array_keys($export));
+ $expected = array('foo' => true, 'bar' => 'c', 'fieldb' => 'b');
+ $this->assertEqual($expected, $export['update']);
+
+ $export = $entity->export(array('virtual' => true));
+ $expected = array('exists', 'data', 'update', 'increment', 'virtual');
+ $this->assertEqual($expected, array_keys($export));
+ $expected = array('fielda' => 'c', 'fieldb' => 'c', 'field_c' => 'c');
+ $this->assertEqual($expected, $export['virtual']);
+
+ $this->expectException('No model bound or unhandled method call `setfield_c`.');
+ $this->assertTrue($entity->field_c = 'd');
+ }
}
?>
View
56 tests/mocks/data/MockModelVirtual.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2012, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\data;
+
+use lithium\tests\mocks\data\source\database\adapter\MockAdapter;
+
+class MockModelVirtual extends \lithium\data\Model {
+
+ protected $_meta = array(
+ 'connection' => false
+ );
+
+ public static $_properties = array(
+ 'fielda' => 'FieldA',
+ 'fieldb' => array('FieldB', 'get' => 'getFieldZ', 'set' => false),
+ 'field_c' => array('get' => 'myTest')
+ );
+
+ public function getFieldA($entity) {
+ return $entity->bar;
+ }
+
+ public function setFieldA($entity, $value) {
+ $entity->bar = $value;
+ }
+
+ public function issetFieldA($entity) {
+ return isset($entity->bar);
+ }
+
+ public function issetFieldB($entity) {
+ return $entity->bar;
+ }
+
+ public function getFieldZ($entity) {
+ return $entity->bar;
+ }
+
+ public function myTest($entity) {
+ return $entity->bar;
+ }
+
+ public function issetfield_c($entity) {
+ return isset($entity->bar);
+ }
+
+ // must be missing public function setfield_c
+}
+
+?>

0 comments on commit 49d7851

Please sign in to comment.