Permalink
Browse files

deprecate(entities): adds entityCache service and deprecates old global

The global `$ENTITY_CACHE` existed awhile before it was marked private, so
we cannot remove it until 3.0. This keeps it in sync with a proper
cache object and detects 3rd party modifications to it with a simple
mechanism (`Elgg\DeprecationWrapper` doesn't support wrapping arrays).

`IdenticalEntityExpectation::entityToFilteredArray` now filters out
volatile data before comparing entities.
  • Loading branch information...
mrclay committed Feb 26, 2016
1 parent 958b8b7 commit 9fa45b62a03c3b4ec282042ca5cdbebe5ee0f451
@@ -50,6 +50,11 @@ New API for handling entity icons
* ``ElggEntity::getIconLastChange()`` - return modified time of the icon file
* ``ElggEntity::hasIcon()`` - checks if an icon with given size has been created
Removed APIs
------------
Just a warning that the private entity cache functions (e.g. ``_elgg_retrieve_cached_entity``) have been removed. Some plugins may have been using them. Plugins should not use private APIs as they will more often be removed without notice.
From 2.0 to 2.1
===============
@@ -0,0 +1,169 @@
<?php
namespace Elgg\Cache;
use ElggEntity;
use ElggSession;
/**
* Volatile cache for entities
*
* @access private
*/
class EntityCache {
// @todo Pick a less arbitrary limit
const MAX_SIZE = 256;
/**
* @var ElggEntity[] GUID keys
*/
private $entities = [];
/**
* @var bool[] GUID keys
*/
private $disabled_guids = [];
/**
* @var ElggSession
*/
private $session;
/**
* @var MetadataCache
*/
private $metadata_cache;
/**
* Constructor
*
* @param ElggSession $session Session
* @param MetadataCache $metadata_cache MD cache
*/
public function __construct(ElggSession $session, MetadataCache $metadata_cache) {
$this->session = $session;
$this->metadata_cache = $metadata_cache;
$GLOBALS['ENTITY_CACHE'] = $this->entities;
}
/**
* Retrieve a entity from the cache.
*
* @param int $guid The GUID
*
* @return \ElggEntity|false false if entity not cached, or not fully loaded
*/
public function get($guid) {
$this->checkGlobal();
if (isset($this->entities[$guid]) && $this->entities[$guid]->isFullyLoaded()) {
return $this->entities[$guid];
}
return false;
}
/**
* Cache an entity.
*
* @param ElggEntity $entity Entity to cache
* @return void
*/
public function set(ElggEntity $entity) {
$this->checkGlobal();
$guid = $entity->guid;
if (!$guid || isset($this->entities[$guid]) || isset($this->disabled_guids[$guid])) {
// have it or not saved
return;
}
// Don't cache non-plugin entities while access control is off, otherwise they could be
// exposed to users who shouldn't see them when control is re-enabled.
if (!($entity instanceof \ElggPlugin) && $this->session->getIgnoreAccess()) {
return;
}
// Don't store too many or we'll have memory problems
if (count($this->entities) > self::MAX_SIZE) {
$this->remove(array_rand($this->entities));
}
$this->entities[$guid] = $entity;
$GLOBALS['ENTITY_CACHE'] = $this->entities;
}
/**
* Invalidate this class's entry in the cache.
*
* @param int $guid The entity guid
* @return void
*/
public function remove($guid) {
$this->checkGlobal();
$guid = (int)$guid;
if (!isset($this->entities[$guid])) {
return;
}
unset($this->entities[$guid]);
$GLOBALS['ENTITY_CACHE'] = $this->entities;
// Purge separate metadata cache. Original idea was to do in entity destructor, but that would
// have caused a bunch of unnecessary purges at every shutdown. Doing it this way we have no way
// to know that the expunged entity will be GCed (might be another reference living), but that's
// OK; the metadata will reload if necessary.
$this->metadata_cache->clear($guid);
}
/**
* Clear the entity cache
*
* @return void
*/
public function clear() {
$this->checkGlobal();
$this->entities = [];
$GLOBALS['ENTITY_CACHE'] = $this->entities;
}
/**
* Remove this entity from the entity cache and make sure it is not re-added
*
* @todo this is a workaround until #5604 can be implemented
*
* @param int $guid The entity guid
* @return void
*/
public function disableCachingForEntity($guid) {
$this->remove($guid);
$this->disabled_guids[$guid] = true;
}
/**
* Allow this entity to be stored in the entity cache
*
* @param int $guid The entity guid
* @return void
*/
public function enableCachingForEntity($guid) {
unset($this->disabled_guids[$guid]);
}
/**
* Make sure the global hasn't been altered outside this class
*
* @return void
* @todo remove in 3.0
*/
private function checkGlobal() {
if (!isset($GLOBALS['ENTITY_CACHE']) || ($GLOBALS['ENTITY_CACHE'] !== $this->entities)) {
$GLOBALS['ENTITY_CACHE'] = $this->entities;
elgg_deprecated_notice('Do not access or write to the global $ENTITY_CACHE.', '2.2');
}
}
}
@@ -156,7 +156,7 @@ function get($guid, $type = '') {
}
// Check local cache first
$new_entity = _elgg_retrieve_cached_entity($guid);
$new_entity = _elgg_services()->entityCache->get($guid);
if ($new_entity) {
if ($type) {
return elgg_instanceof($new_entity, $type) ? $new_entity : false;
@@ -484,7 +484,7 @@ function getEntities(array $options = array()) {
foreach ($results as $item) {
// A custom callback could result in items that aren't \ElggEntity's, so check for them
if ($item instanceof \ElggEntity) {
_elgg_cache_entity($item);
_elgg_services()->entityCache->set($item);
// plugins usually have only settings
if (!$item instanceof \ElggPlugin) {
$guids[] = $item->guid;
@@ -618,7 +618,7 @@ function fetchFromSql($sql, \ElggBatch $batch = null) {
if (empty($row->guid) || empty($row->type)) {
throw new \LogicException('Entity row missing guid or type');
}
$entity = _elgg_retrieve_cached_entity($row->guid);
$entity = _elgg_services()->entityCache->get($row->guid);
if ($entity) {
$entity->refresh($row);
$rows[$i] = $entity;
@@ -191,7 +191,7 @@ function makeAdmin($user_guid) {
}
$r = _elgg_services()->db->updateData("UPDATE {$this->CONFIG->dbprefix}users_entity set admin='yes' where guid=$user_guid");
_elgg_invalidate_cache_for_entity($user_guid);
_elgg_services()->entityCache->remove($user_guid);
return $r;
}
@@ -227,7 +227,7 @@ function removeAdmin($user_guid) {
}
$r = _elgg_services()->db->updateData("UPDATE {$this->CONFIG->dbprefix}users_entity set admin='no' where guid=$user_guid");
_elgg_invalidate_cache_for_entity($user_guid);
_elgg_services()->entityCache->remove($user_guid);
return $r;
}
@@ -257,8 +257,8 @@ function getByUsername($username) {
// Caching
if ((isset($USERNAME_TO_GUID_MAP_CACHE[$username]))
&& (_elgg_retrieve_cached_entity($USERNAME_TO_GUID_MAP_CACHE[$username]))) {
return _elgg_retrieve_cached_entity($USERNAME_TO_GUID_MAP_CACHE[$username]);
&& (_elgg_services()->entityCache->get($USERNAME_TO_GUID_MAP_CACHE[$username]))) {
return _elgg_services()->entityCache->get($USERNAME_TO_GUID_MAP_CACHE[$username]);
}
$query = "SELECT e.* FROM {$this->CONFIG->dbprefix}users_entity u
@@ -31,6 +31,7 @@
* @property-read \Elgg\Database\Datalist $datalist
* @property-read \Elgg\Database $db
* @property-read \Elgg\DeprecationService $deprecation
* @property-read \Elgg\Cache\EntityCache $entityCache
* @property-read \Elgg\EntityPreloader $entityPreloader
* @property-read \Elgg\Database\EntityTable $entityTable
* @property-read \Elgg\EventsService $events
@@ -171,6 +172,10 @@ public function __construct(\Elgg\Config $config) {
return new \Elgg\DeprecationService($c->logger);
});
$this->setFactory('entityCache', function(ServiceProvider $c) {
return new \Elgg\Cache\EntityCache($c->session, $c->metadataCache);
});
$this->setClassName('entityPreloader', \Elgg\EntityPreloader::class);
$this->setClassName('entityTable', \Elgg\Database\EntityTable::class);
@@ -1575,7 +1575,7 @@ protected function create() {
$this->temp_private_settings = array();
}
_elgg_cache_entity($this);
_elgg_services()->entityCache->set($this);
return $result;
}
@@ -1650,7 +1650,7 @@ protected function update() {
$this->attributes['time_updated'] = $time;
}
_elgg_cache_entity($this);
_elgg_services()->entityCache->set($this);
$this->orig_attributes = [];
@@ -1692,7 +1692,7 @@ protected function load($guid) {
// Cache object handle
if ($this->attributes['guid']) {
_elgg_cache_entity($this);
_elgg_services()->entityCache->set($this);
}
return true;
@@ -1772,8 +1772,8 @@ public function disable($reason = "", $recursive = true) {
$unban_after = false;
}
_elgg_invalidate_cache_for_entity($this->guid);
_elgg_services()->entityCache->remove($this->guid);
if ($reason) {
$this->disable_reason = $reason;
}
@@ -1945,8 +1945,8 @@ public function delete($recursive = true) {
_elgg_services()->usersTable->markBanned($this->guid, true);
}
_elgg_invalidate_cache_for_entity($guid);
_elgg_services()->entityCache->remove($guid);
// If memcache is available then delete this entry from the cache
static $newentity_cache;
if ((!$newentity_cache) && (is_memcache_available())) {
@@ -520,7 +520,7 @@ protected function load($guid) {
$this->attributes = $attrs;
$this->loadAdditionalSelectValues($attr_loader->getAdditionalSelectValues());
_elgg_cache_entity($this);
_elgg_services()->entityCache->set($this);
return true;
}
@@ -569,7 +569,7 @@ protected function create() {
// TODO(evan): Throw an exception here?
return false;
}
return $guid;
}
@@ -107,7 +107,7 @@ protected function load($guid) {
$this->attributes = $attrs;
$this->loadAdditionalSelectValues($attr_loader->getAdditionalSelectValues());
_elgg_cache_entity($this);
_elgg_services()->entityCache->set($this);
return true;
}
@@ -135,7 +135,7 @@ protected function create() {
// TODO(evan): Throw an exception here?
return false;
}
return $guid;
}
@@ -117,7 +117,7 @@ protected function load($guid) {
$this->attributes = $attrs;
$this->loadAdditionalSelectValues($attr_loader->getAdditionalSelectValues());
_elgg_cache_entity($this);
_elgg_services()->entityCache->set($this);
return true;
}
@@ -118,7 +118,7 @@ protected function load($guid) {
$this->attributes = $attrs;
$this->loadAdditionalSelectValues($attr_loader->getAdditionalSelectValues());
_elgg_cache_entity($this);
_elgg_services()->entityCache->set($this);
return true;
}
@@ -148,7 +148,7 @@ protected function create() {
// TODO(evan): Throw an exception here?
return false;
}
return $guid;
}
Oops, something went wrong.

0 comments on commit 9fa45b6

Please sign in to comment.