MetadataCache.php 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. <?php
  2. namespace Elgg\Cache;
  3. /**
  4. * In memory cache of known metadata values stored by entity.
  5. *
  6. * @access private
  7. */
  8. class MetadataCache {
  9. /**
  10. * The cached values (or null for known to be empty).
  11. *
  12. * @var array
  13. */
  14. protected $values = array();
  15. /**
  16. * @var \ElggSession
  17. */
  18. protected $session;
  19. /**
  20. * Constructor
  21. *
  22. * @param \ElggSession $session The session service
  23. */
  24. public function __construct(\ElggSession $session) {
  25. $this->session = $session;
  26. }
  27. /**
  28. * Set the visible metadata for an entity in the cache
  29. *
  30. * Note this does NOT invalidate any other part of the cache.
  31. *
  32. * @param int $entity_guid The GUID of the entity
  33. * @param array $values The metadata values to cache
  34. * @return void
  35. *
  36. * @access private For testing only
  37. */
  38. public function inject($entity_guid, array $values) {
  39. $this->values[$this->getAccessKey()][$entity_guid] = $values;
  40. }
  41. /**
  42. * Get the metadata for a particular name. Note, this can return an array of values.
  43. *
  44. * Warning: You should always call isLoaded() beforehand to verify that this
  45. * function's return value can be trusted.
  46. *
  47. * @see isLoaded
  48. *
  49. * @param int $entity_guid The GUID of the entity
  50. * @param string $name The metadata name
  51. *
  52. * @return array|string|int|null null = value does not exist
  53. */
  54. public function getSingle($entity_guid, $name) {
  55. $access_key = $this->getAccessKey();
  56. if (isset($this->values[$access_key][$entity_guid])
  57. && array_key_exists($name, $this->values[$access_key][$entity_guid])) {
  58. return $this->values[$access_key][$entity_guid][$name];
  59. } else {
  60. return null;
  61. }
  62. }
  63. /**
  64. * Forget about all metadata for an entity. For safety this affects all access states.
  65. *
  66. * @param int $entity_guid The GUID of the entity
  67. * @return void
  68. */
  69. public function clear($entity_guid) {
  70. foreach (array_keys($this->values) as $access_key) {
  71. unset($this->values[$access_key][$entity_guid]);
  72. }
  73. }
  74. /**
  75. * If true, getSingle() will return an accurate values from the DB
  76. *
  77. * @param int $entity_guid The GUID of the entity
  78. * @return bool
  79. */
  80. public function isLoaded($entity_guid) {
  81. $access_key = $this->getAccessKey();
  82. if (empty($this->values[$access_key])) {
  83. return false;
  84. }
  85. return array_key_exists($entity_guid, $this->values[$access_key]);
  86. }
  87. /**
  88. * Clear entire cache
  89. *
  90. * @return void
  91. */
  92. public function clearAll() {
  93. $this->values = array();
  94. }
  95. /**
  96. * Invalidate based on options passed to the global *_metadata functions
  97. *
  98. * @param array $options Options passed to elgg_(delete|disable|enable)_metadata
  99. * "guid" if given, invalidation will be limited to this entity
  100. * @return void
  101. */
  102. public function invalidateByOptions(array $options) {
  103. if (empty($options['guid'])) {
  104. $this->clearAll();
  105. } else {
  106. $this->clear($options['guid']);
  107. }
  108. }
  109. /**
  110. * Populate the cache from a set of entities
  111. *
  112. * @param int|array $guids Array of or single GUIDs
  113. * @return void
  114. */
  115. public function populateFromEntities($guids) {
  116. if (empty($guids)) {
  117. return;
  118. }
  119. $access_key = $this->getAccessKey();
  120. if (!is_array($guids)) {
  121. $guids = array($guids);
  122. }
  123. $guids = array_unique($guids);
  124. // could be useful at some point in future
  125. //$guids = $this->filterMetadataHeavyEntities($guids);
  126. $db_prefix = _elgg_services()->db->getTablePrefix();
  127. $options = array(
  128. 'guids' => $guids,
  129. 'limit' => 0,
  130. 'callback' => false,
  131. 'distinct' => false,
  132. 'joins' => array(
  133. "JOIN {$db_prefix}metastrings v ON n_table.value_id = v.id",
  134. "JOIN {$db_prefix}metastrings n ON n_table.name_id = n.id",
  135. ),
  136. 'selects' => array('n.string AS name', 'v.string AS value'),
  137. 'order_by' => 'n_table.entity_guid, n_table.time_created ASC, n_table.id ASC',
  138. // @todo don't know why this is necessary
  139. 'wheres' => array(_elgg_get_access_where_sql(array(
  140. 'table_alias' => 'n_table',
  141. 'guid_column' => 'entity_guid',
  142. ))),
  143. );
  144. $data = _elgg_services()->metadataTable->getAll($options);
  145. // make sure we show all entities as loaded
  146. foreach ($guids as $guid) {
  147. $this->values[$access_key][$guid] = null;
  148. }
  149. // build up metadata for each entity, save when GUID changes (or data ends)
  150. $last_guid = null;
  151. $metadata = array();
  152. $last_row_idx = count($data) - 1;
  153. foreach ($data as $i => $row) {
  154. $name = $row->name;
  155. $value = ($row->value_type === 'text') ? $row->value : (int) $row->value;
  156. $guid = $row->entity_guid;
  157. if ($guid !== $last_guid) {
  158. if ($last_guid) {
  159. $this->values[$access_key][$last_guid] = $metadata;
  160. }
  161. $metadata = array();
  162. }
  163. if (isset($metadata[$name])) {
  164. $metadata[$name] = (array) $metadata[$name];
  165. $metadata[$name][] = $value;
  166. } else {
  167. $metadata[$name] = $value;
  168. }
  169. if (($i == $last_row_idx)) {
  170. $this->values[$access_key][$guid] = $metadata;
  171. }
  172. $last_guid = $guid;
  173. }
  174. }
  175. /**
  176. * Filter out entities whose concatenated metadata values (INTs casted as string)
  177. * exceed a threshold in characters. This could be used to avoid overpopulating the
  178. * cache if RAM usage becomes an issue.
  179. *
  180. * @param array $guids GUIDs of entities to examine
  181. * @param int $limit Limit in characters of all metadata (with ints casted to strings)
  182. * @return array
  183. */
  184. public function filterMetadataHeavyEntities(array $guids, $limit = 1024000) {
  185. $db_prefix = _elgg_services()->config->get('dbprefix');
  186. $options = array(
  187. 'guids' => $guids,
  188. 'limit' => 0,
  189. 'callback' => false,
  190. 'joins' => "JOIN {$db_prefix}metastrings v ON n_table.value_id = v.id",
  191. 'selects' => array('SUM(LENGTH(v.string)) AS bytes'),
  192. 'order_by' => 'n_table.entity_guid, n_table.time_created ASC',
  193. 'group_by' => 'n_table.entity_guid',
  194. );
  195. $data = _elgg_services()->metadataTable->getAll($options);
  196. // don't cache if metadata for entity is over 10MB (or rolled INT)
  197. foreach ($data as $row) {
  198. if ($row->bytes > $limit || $row->bytes < 0) {
  199. array_splice($guids, array_search($row->entity_guid, $guids), 1);
  200. }
  201. }
  202. return $guids;
  203. }
  204. /**
  205. * Get a key to represent the access ability of the system. This is used to shard the cache array.
  206. *
  207. * @return string E.g. "ignored" or "123"
  208. */
  209. protected function getAccessKey() {
  210. if ($this->session->getIgnoreAccess()) {
  211. return "ignored";
  212. }
  213. return (string)$this->session->getLoggedInUserGuid();
  214. }
  215. }