Permalink
Browse files

feature(entities): give access to original values of modified attributes

Once an entity has been saved, Elgg tracks changes to its attributes, so that
handlers of the `update` and `update:after` events can see which attributes are
changing. Calling `$entity->getOriginalAttributes()` returns keys of changed
attributes, and the original values as the array values. E.g.:

```php
function my_update_handler($event, $type, ElggEntity $object) {
    $orig = $object->getOriginalAttributes();
    if (array_key_exists('title', $orig)) {
        // the title changed from $orig['title'] to $object->title
    }
}
```

Fixes #9187
  • Loading branch information...
mrclay committed Dec 4, 2015
1 parent bb6b87f commit 56ddabbcbd450f6726ae23840f3c7a22bf86fafe
Showing with 130 additions and 5 deletions.
  1. +8 −3 docs/design/events.rst
  2. +4 −0 docs/guides/events-list.rst
  3. +39 −1 engine/classes/ElggEntity.php
  4. +79 −1 engine/tests/ElggEntityTest.php
View
@@ -85,7 +85,7 @@ Elgg event handlers should have the following prototype:
...
}
If the handler returns `false`, the event is cancelled, preventing
If the handler returns ``false``, the event is cancelled, preventing
execution of the other handlers. All other return values are ignored.
Register to handle an Elgg Event
@@ -116,6 +116,11 @@ Example:
// user login event with priority 400.
elgg_register_event_handler('login', 'user', 'myPlugin_handle_login', 400);
.. warning::
If you handle the "update" event on an object, avoid calling ``save()`` in your event handler. For one it's
probably not necessary as the object is saved after the event completes, but also because ``save()`` calls
another "update" event and makes ``$object->getOriginalAttributes()`` no longer available.
Trigger an Elgg Event
---------------------
@@ -186,9 +191,9 @@ Plugin hook handlers should have the following prototype:
...
}
If the handler returns no value (or `null` explicitly), the plugin hook value
If the handler returns no value (or ``null`` explicitly), the plugin hook value
is not altered. Otherwise the return value becomes the new value of the plugin
hook. It will then be passed to the next handler as `$value`.
hook. It will then be passed to the next handler as ``$value``.
Register to handle a Plugin Hook
--------------------------------
@@ -130,9 +130,13 @@ Entity events
**update, <entity type>**
Triggered before an update for the user, group, object, and site entities. Return false to prevent update.
The entity method ``getOriginalAttributes()`` can be used to identify which attributes have changed since
the entity was last saved.
**update:after, <entity type>**
Triggered after an update for the user, group, object, and site entities.
The entity method ``getOriginalAttributes()`` can be used to identify which attributes have changed since
the entity was last saved.
**delete, <entity type>**
Triggered before entity deletion. Return false to prevent deletion.
@@ -74,7 +74,12 @@
* in-memory that isn't sync'd back to the metadata table.
*/
protected $volatile = array();
/**
* Holds the original (persisted) attribute values that have been changed but not yet saved.
*/
protected $orig_attributes = array();
/**
* Tells how many tables are going to need to be searched in order to fully populate this object
*
@@ -197,6 +202,28 @@ public function __set($name, $value) {
return;
}
if (array_key_exists($name, $this->attributes)) {
// if an attribute is 1 (integer) and it's set to "1" (string), don't consider that a change.
if (is_int($this->attributes[$name])
&& is_string($value)
&& ((string)$this->attributes[$name] === $value)) {
return;
}
// Due to https://github.com/Elgg/Elgg/pull/5456#issuecomment-17785173, certain attributes
// will store empty strings as null in the DB. In the somewhat common case that we're re-setting
// the value to empty string, don't consider this a change.
if (in_array($name, ['title', 'name', 'description'])
&& $this->attributes[$name] === null
&& $value === "") {
return;
}
// keep original values
if ($this->guid && !array_key_exists($name, $this->orig_attributes)) {
$this->orig_attributes[$name] = $this->attributes[$name];
}
// Certain properties should not be manually changed!
switch ($name) {
case 'guid':
@@ -238,6 +265,15 @@ public function set($name, $value) {
return true;
}
/**
* Get the original values of attribute(s) that have been modified since the entity was persisted.
*
* @return array
*/
public function getOriginalAttributes() {
return $this->orig_attributes;
}
/**
* Get an attribute or metadata value
*
@@ -1670,6 +1706,8 @@ protected function update() {
_elgg_cache_entity($this);
$this->orig_attributes = [];
// Handle cases where there was no error BUT no rows were updated!
return $ret !== false;
}
@@ -6,7 +6,7 @@
class ElggCoreEntityTest extends \ElggCoreUnitTest {
/**
* @var \ElggEntity
* @var \ElggObject
*/
protected $entity;
@@ -51,6 +51,84 @@ public function testSubtypePropertyReads() {
$this->assertEqual($subtype_prop, get_subtype_id('object', 'elgg_entity_test_subtype'));
}
public function testUnsavedEntitiesDontRecordAttributeSets() {
$entity = new \ElggObject();
$entity->subtype = 'elgg_entity_test_subtype';
$entity->title = 'Foo';
$entity->description = 'Bar';
$entity->container_guid = elgg_get_logged_in_user_guid();
$this->assertEqual($entity->getOriginalAttributes(), []);
}
public function testAlreadyPersistedAttributeSetsAreRecorded() {
$this->entity->title = 'Foo';
$this->entity->description = 'Bar';
$this->entity->container_guid = elgg_get_site_entity()->guid;
$this->assertEqual($this->entity->getOriginalAttributes(), [
'title' => null,
'description' => null,
'container_guid' => elgg_get_logged_in_user_guid(),
]);
}
public function testModifiedAttributesAreAvailableDuringUpdateNotAfter() {
$this->entity->title = 'Foo';
$this->entity->description = 'Bar';
$this->entity->container_guid = elgg_get_site_entity()->guid;
$calls = 0;
$handler = function ($event, $type, \ElggObject $object) use (&$calls) {
$calls++;
$this->assertEqual($object->getOriginalAttributes(), [
'title' => null,
'description' => null,
'container_guid' => elgg_get_logged_in_user_guid(),
]);
};
elgg_register_event_handler('update', 'object', $handler);
elgg_register_event_handler('update:after', 'object', $handler);
$this->entity->save();
$this->assertEqual($calls, 2);
elgg_unregister_event_handler('update', 'object', $handler);
elgg_unregister_event_handler('update:after', 'object', $handler);
$this->assertEqual($this->entity->getOriginalAttributes(), []);
}
public function testModifedAttributesSettingEmptyString() {
$this->entity->title = '';
$this->entity->description = '';
$this->assertEqual($this->entity->getOriginalAttributes(), []);
$this->entity->title = '';
$this->entity->description = '';
$this->assertEqual($this->entity->getOriginalAttributes(), []);
}
public function testModifedAttributesSettingIntsAsStrings() {
$this->entity->container_guid = elgg_get_logged_in_user_guid();
$this->entity->save();
$this->entity->container_guid = (string)elgg_get_logged_in_user_guid();
$this->assertEqual($this->entity->getOriginalAttributes(), []);
}
public function testMultipleAttributeSetsDontOverwriteOriginals() {
$this->entity->title = 'Foo';
$this->entity->title = 'Bar';
$this->assertEqual($this->entity->getOriginalAttributes(), [
'title' => null,
]);
}
public function testGetSubtype() {
$guid = $this->entity->guid;

0 comments on commit 56ddabb

Please sign in to comment.