1: <?php
2: /**
3: * ElggVolatileMetadataCache
4: * In memory cache of known metadata values stored by entity.
5: *
6: * @package Elgg.Core
7: * @subpackage Cache
8: *
9: * @access private
10: */
11: class ElggVolatileMetadataCache {
12:
13: /**
14: * The cached values (or null for known to be empty). If the portion of the cache
15: * is synchronized, missing values are assumed to indicate that values do not
16: * exist in storage, otherwise, we don't know what's there.
17: *
18: * @var array
19: */
20: protected $values = array();
21:
22: /**
23: * Does the cache know that it contains all names fetch-able from storage?
24: * The keys are entity GUIDs and either the value exists (true) or it's not set.
25: *
26: * @var array
27: */
28: protected $isSynchronized = array();
29:
30: /**
31: * @var null|bool
32: */
33: protected $ignoreAccess = null;
34:
35: /**
36: * Cache metadata for an entity
37: *
38: * @param int $entity_guid The GUID of the entity
39: * @param array $values The metadata values to cache
40: * @return void
41: */
42: public function saveAll($entity_guid, array $values) {
43: if (!$this->getIgnoreAccess()) {
44: $this->values[$entity_guid] = $values;
45: $this->isSynchronized[$entity_guid] = true;
46: }
47: }
48:
49: /**
50: * Get the metadata for an entity
51: *
52: * @param int $entity_guid The GUID of the entity
53: * @return array
54: */
55: public function loadAll($entity_guid) {
56: if (isset($this->values[$entity_guid])) {
57: return $this->values[$entity_guid];
58: } else {
59: return array();
60: }
61: }
62:
63: /**
64: * Declare that there may be fetch-able metadata names in storage that this
65: * cache doesn't know about
66: *
67: * @param int $entity_guid The GUID of the entity
68: * @return void
69: */
70: public function markOutOfSync($entity_guid) {
71: unset($this->isSynchronized[$entity_guid]);
72: }
73:
74: /**
75: * Have all the metadata for this entity been cached?
76: *
77: * @param int $entity_guid The GUID of the entity
78: * @return bool
79: */
80: public function isSynchronized($entity_guid) {
81: return isset($this->isSynchronized[$entity_guid]);
82: }
83:
84: /**
85: * Cache a piece of metadata
86: *
87: * @param int $entity_guid The GUID of the entity
88: * @param string $name The metadata name
89: * @param array|int|string|null $value The metadata value. null means it is
90: * known that there is no fetch-able
91: * metadata under this name
92: * @param bool $allow_multiple Can the metadata be an array
93: * @return void
94: */
95: public function save($entity_guid, $name, $value, $allow_multiple = false) {
96: if ($this->getIgnoreAccess()) {
97: // we don't know if what gets saves here will be available to user once
98: // access control returns, hence it's best to forget :/
99: $this->markUnknown($entity_guid, $name);
100: } else {
101: if ($allow_multiple) {
102: if ($this->isKnown($entity_guid, $name)) {
103: $existing = $this->load($entity_guid, $name);
104: if ($existing !== null) {
105: $existing = (array) $existing;
106: $existing[] = $value;
107: $value = $existing;
108: }
109: } else {
110: // we don't know whether there are unknown values, so it's
111: // safest to leave that assumption
112: $this->markUnknown($entity_guid, $name);
113: return;
114: }
115: }
116: $this->values[$entity_guid][$name] = $value;
117: }
118: }
119:
120: /**
121: * Warning: You should always call isKnown() beforehand to verify that this
122: * function's return value should be trusted (otherwise a null return value
123: * is ambiguous).
124: *
125: * @param int $entity_guid The GUID of the entity
126: * @param string $name The metadata name
127: * @return array|string|int|null null = value does not exist
128: */
129: public function load($entity_guid, $name) {
130: if (isset($this->values[$entity_guid]) && array_key_exists($name, $this->values[$entity_guid])) {
131: return $this->values[$entity_guid][$name];
132: } else {
133: return null;
134: }
135: }
136:
137: /**
138: * Forget about this metadata entry. We don't want to try to guess what the
139: * next fetch from storage will return
140: *
141: * @param int $entity_guid The GUID of the entity
142: * @param string $name The metadata name
143: * @return void
144: */
145: public function markUnknown($entity_guid, $name) {
146: unset($this->values[$entity_guid][$name]);
147: $this->markOutOfSync($entity_guid);
148: }
149:
150: /**
151: * If true, load() will return an accurate value for this name
152: *
153: * @param int $entity_guid The GUID of the entity
154: * @param string $name The metadata name
155: * @return bool
156: */
157: public function isKnown($entity_guid, $name) {
158: if (isset($this->isSynchronized[$entity_guid])) {
159: return true;
160: } else {
161: return (isset($this->values[$entity_guid]) && array_key_exists($name, $this->values[$entity_guid]));
162: }
163:
164: }
165:
166: /**
167: * Declare that metadata under this name is known to be not fetch-able from storage
168: *
169: * @param int $entity_guid The GUID of the entity
170: * @param string $name The metadata name
171: * @return array
172: */
173: public function markEmpty($entity_guid, $name) {
174: $this->values[$entity_guid][$name] = null;
175: }
176:
177: /**
178: * Forget about all metadata for an entity
179: *
180: * @param int $entity_guid The GUID of the entity
181: * @return void
182: */
183: public function clear($entity_guid) {
184: $this->values[$entity_guid] = array();
185: $this->markOutOfSync($entity_guid);
186: }
187:
188: /**
189: * Clear entire cache and mark all entities as out of sync
190: *
191: * @return void
192: */
193: public function flush() {
194: $this->values = array();
195: $this->isSynchronized = array();
196: }
197:
198: /**
199: * Use this value instead of calling elgg_get_ignore_access(). By default that
200: * function will be called.
201: *
202: * This setting makes this component a little more loosely-coupled.
203: *
204: * @param bool $ignore Whether to ignore access or not
205: * @return void
206: */
207: public function setIgnoreAccess($ignore) {
208: $this->ignoreAccess = (bool) $ignore;
209: }
210:
211: /**
212: * Tell the cache to call elgg_get_ignore_access() to determing access status.
213: *
214: * @return void
215: */
216: public function unsetIgnoreAccess() {
217: $this->ignoreAccess = null;
218: }
219:
220: /**
221: * Get the ignore access value
222: *
223: * @return bool
224: */
225: protected function getIgnoreAccess() {
226: if (null === $this->ignoreAccess) {
227: return elgg_get_ignore_access();
228: } else {
229: return $this->ignoreAccess;
230: }
231: }
232:
233: /**
234: * Invalidate based on options passed to the global *_metadata functions
235: *
236: * @param string $action Action performed on metadata. "delete", "disable", or "enable"
237: * @param array $options Options passed to elgg_(delete|disable|enable)_metadata
238: * "guid" if given, invalidation will be limited to this entity
239: * "metadata_name" if given, invalidation will be limited to metadata with this name
240: * @return void
241: */
242: public function invalidateByOptions($action, array $options) {
243: // remove as little as possible, optimizing for common cases
244: if (empty($options['guid'])) {
245: // safest to clear everything unless we want to make this even more complex :(
246: $this->flush();
247: } else {
248: if (empty($options['metadata_name'])) {
249: // safest to clear the whole entity
250: $this->clear($options['guid']);
251: } else {
252: switch ($action) {
253: case 'delete':
254: $this->markEmpty($options['guid'], $options['metadata_name']);
255: break;
256: default:
257: $this->markUnknown($options['guid'], $options['metadata_name']);
258: }
259: }
260: }
261: }
262:
263: /**
264: * Populate the cache from a set of entities
265: *
266: * @param int|array $guids Array of or single GUIDs
267: * @return void
268: */
269: public function populateFromEntities($guids) {
270: if (empty($guids)) {
271: return;
272: }
273: if (!is_array($guids)) {
274: $guids = array($guids);
275: }
276: $guids = array_unique($guids);
277:
278: // could be useful at some point in future
279: //$guids = $this->filterMetadataHeavyEntities($guids);
280:
281: $db_prefix = elgg_get_config('dbprefix');
282: $options = array(
283: 'guids' => $guids,
284: 'limit' => 0,
285: 'callback' => false,
286: 'joins' => array(
287: "JOIN {$db_prefix}metastrings v ON n_table.value_id = v.id",
288: "JOIN {$db_prefix}metastrings n ON n_table.name_id = n.id",
289: ),
290: 'selects' => array('n.string AS name', 'v.string AS value'),
291: 'order_by' => 'n_table.entity_guid, n_table.time_created ASC',
292:
293: // @todo don't know why this is necessary
294: 'wheres' => array(get_access_sql_suffix('n_table')),
295: );
296: $data = elgg_get_metadata($options);
297:
298: // build up metadata for each entity, save when GUID changes (or data ends)
299: $last_guid = null;
300: $metadata = array();
301: $last_row_idx = count($data) - 1;
302: foreach ($data as $i => $row) {
303: $name = $row->name;
304: $value = ($row->value_type === 'text') ? $row->value : (int) $row->value;
305: $guid = $row->entity_guid;
306: if ($guid !== $last_guid) {
307: if ($last_guid) {
308: $this->saveAll($last_guid, $metadata);
309: }
310: $metadata = array();
311: }
312: if (isset($metadata[$name])) {
313: $metadata[$name] = (array) $metadata[$name];
314: $metadata[$name][] = $value;
315: } else {
316: $metadata[$name] = $value;
317: }
318: if (($i == $last_row_idx)) {
319: $this->saveAll($guid, $metadata);
320: }
321: $last_guid = $guid;
322: }
323: }
324:
325: /**
326: * Filter out entities whose concatenated metadata values (INTs casted as string)
327: * exceed a threshold in characters. This could be used to avoid overpopulating the
328: * cache if RAM usage becomes an issue.
329: *
330: * @param array $guids GUIDs of entities to examine
331: * @param int $limit Limit in characters of all metadata (with ints casted to strings)
332: * @return array
333: */
334: public function filterMetadataHeavyEntities(array $guids, $limit = 1024000) {
335: $db_prefix = elgg_get_config('dbprefix');
336:
337: $options = array(
338: 'guids' => $guids,
339: 'limit' => 0,
340: 'callback' => false,
341: 'joins' => "JOIN {$db_prefix}metastrings v ON n_table.value_id = v.id",
342: 'selects' => array('SUM(LENGTH(v.string)) AS bytes'),
343: 'order_by' => 'n_table.entity_guid, n_table.time_created ASC',
344: 'group_by' => 'n_table.entity_guid',
345: );
346: $data = elgg_get_metadata($options);
347: // don't cache if metadata for entity is over 10MB (or rolled INT)
348: foreach ($data as $row) {
349: if ($row->bytes > $limit || $row->bytes < 0) {
350: array_splice($guids, array_search($row->entity_guid, $guids), 1);
351: }
352: }
353: return $guids;
354: }
355: }
356: