Completed
Push — master ( 45a001...92a1b3 )
by Jerome
62:35 queued 12s
created

engine/classes/Elgg/Cache/MetadataCache.php (1 issue)

1
<?php
2
namespace Elgg\Cache;
3
4
use Elgg\Database\Clauses\OrderByClause;
5
use Elgg\Values;
6
use ElggCache;
7
use ElggMetadata;
8
use Elgg\Database\Clauses\GroupByClause;
9
10
/**
11
 * In memory cache of known metadata values stored by entity.
12
 *
13
 * @access private
14
 */
15
class MetadataCache {
16
17
	/**
18
	 * @var ElggCache
19
	 */
20
	protected $cache;
21
22
	/**
23
	 * Constructor
24
	 *
25
	 * @param ElggCache $cache Cache
26
	 */
27 5090
	public function __construct(ElggCache $cache) {
28 5090
		$this->cache = $cache;
29 5090
	}
30
31
	/**
32
	 * Set the visible metadata for an entity in the cache
33
	 *
34
	 * Note this does NOT invalidate any other part of the cache.
35
	 *
36
	 * @param int   $entity_guid The GUID of the entity
37
	 * @param array $values      The metadata values to cache
38
	 *
39
	 * @return void
40
	 *
41
	 * @interal For testing only
42
	 */
43 5
	public function inject($entity_guid, array $values = []) {
44 5
		$metadata = [];
45 5
		foreach ($values as $key => $value) {
46 5
			if ($value instanceof ElggMetadata) {
47
				$md = $value;
48
			} else {
49 5
				$md = new ElggMetadata();
50 5
				$md->name = $key;
51 5
				$md->value = $value;
52 5
				$md->entity_guid = $entity_guid;
53
			}
54
55 5
			$metadata[] = $md->toObject();
56
		}
57
58 5
		$this->cache->save($entity_guid, $metadata);
59 5
	}
60
61
	/**
62
	 * Get all entity metadata
63
	 *
64
	 * @param int $entity_guid Entity guid
65
	 * @return array
66
	 */
67 6211
	public function getAll($entity_guid) {
68 6211
		$metadata = $this->getEntityMetadata($entity_guid);
69 6211
		if (empty($metadata)) {
70 6077
			return [];
71
		}
72
73 1140
		$metadata_values = [];
74
75 1140
		foreach ($metadata as $md) {
76 1140
			$metadata_values[$md->name][] = $md->value;
77
		}
78
79 1140
		return array_map(function($values) {
80 1140
			return count($values) > 1 ? $values : $values[0];
81 1140
		}, $metadata_values);
82
	}
83
84
	/**
85
	 * Get the metadata for a particular name. Note, this can return an array of values.
86
	 *
87
	 * Warning: You should always call isLoaded() beforehand to verify that this
88
	 * function's return value can be trusted.
89
	 *
90
	 * @see isLoaded
91
	 *
92
	 * @param int    $entity_guid The GUID of the entity
93
	 * @param string $name        The metadata name
94
	 *
95
	 * @return array|string|int|null null = value does not exist
96
	 */
97 3
	public function getSingle($entity_guid, $name) {
98 3
		$metadata = $this->getEntityMetadata($entity_guid);
99 3
		if (empty($metadata)) {
100 1
			return null;
101
		}
102
103 3
		$values = [];
104
105 3
		foreach ($metadata as $md) {
106 3
			if ($md->name !== $name) {
107 3
				continue;
108
			}
109
110 3
			$values[] = $md->value;
111
		}
112
113 3
		if (empty($values)) {
114 1
			return null;
115
		}
116
117 3
		return count($values) > 1 ? $values : $values[0];
118
119
	}
120
121
	/**
122
	 * Get the metadata id for a particular name
123
	 *
124
	 * Warning: You should always call isLoaded() beforehand to verify that this
125
	 * function's return value can be trusted.
126
	 *
127
	 * @see isLoaded
128
	 *
129
	 * @param int    $entity_guid The GUID of the entity
130
	 * @param string $name        The metadata name
131
	 *
132
	 * @return int[]|int|null
133
	 */
134 1290
	public function getSingleId($entity_guid, $name) {
135 1290
		$metadata = $this->getEntityMetadata($entity_guid);
136 1290
		if (empty($metadata)) {
137 1290
			return null;
138
		}
139
140 874
		$ids = [];
141
142 874
		foreach ($metadata as $md) {
143 874
			if ($md->name !== $name) {
144 874
				continue;
145
			}
146
147 413
			$ids[] = $md->id;
148
		}
149
150 874
		if (empty($ids)) {
151 874
			return null;
152
		}
153
154 413
		return count($ids) > 1 ? $ids : $ids[0];
155
	}
156
157
	/**
158
	 * Forget about all metadata for an entity.
159
	 *
160
	 * @param int $entity_guid The GUID of the entity
161
	 *
162
	 * @return void
163
	 */
164 1295
	public function clear($entity_guid) {
165 1295
		$this->invalidateByOptions([
166 1295
			'guid' => $entity_guid,
167
		]);
168 1295
	}
169
170
	/**
171
	 * If true, getSingle() will return an accurate values from the DB
172
	 *
173
	 * @param int $entity_guid The GUID of the entity
174
	 *
175
	 * @return bool
176
	 */
177 1296
	public function isLoaded($entity_guid) {
178 1296
		return $this->cache->load($entity_guid) !== null;
179
	}
180
181
	/**
182
	 * Clear entire cache
183
	 *
184
	 * @return void
185
	 */
186 9
	public function clearAll() {
187 9
		$this->invalidateByOptions([]);
188 9
	}
189
190
	/**
191
	 * Returns loaded entity metadata
192
	 *
193
	 * @param int $entity_guid Entity guid
194
	 *
195
	 * @return \stdClass[]|null
196
	 */
197 6212
	public function getEntityMetadata($entity_guid) {
198 6212
		$entity_guid = (int) $entity_guid;
199 6212
		$metadata = $this->cache->load($entity_guid);
200 6212
		if ($metadata === null) {
201 6150
			$metadata = elgg_extract($entity_guid, $this->populateFromEntities($entity_guid));
202
		}
203
		
204 6212
		return $metadata;
205
	}
206
207
	/**
208
	 * Invalidate based on options passed to the global *_metadata functions
209
	 *
210
	 * @param array $options Options passed to elgg_(delete|disable|enable)_metadata
211
	 *                       "guid" if given, invalidation will be limited to this entity
212
	 * @return void
213
	 */
214 1325
	public function invalidateByOptions(array $options) {
215 1325
		if (empty($options['guid'])) {
216 9
			_elgg_services()->sessionCache->clear();
217 9
			_elgg_services()->dataCache->clear();
218
		} else {
219 1322
			_elgg_services()->entityTable->invalidateCache($options['guid']);
220
		}
221 1325
	}
222
223
	/**
224
	 * Populate the cache from a set of entities
225
	 *
226
	 * @param int[] ...$guids Array of or single GUIDs
227
	 * @return array|null [guid => [metadata]]
228
	 */
229 6211
	public function populateFromEntities(...$guids) {
230
		try {
231 6211
			$guids = Values::normalizeGuids($guids);
232
		} catch (\DataFormatException $e) {
233
			return null;
234
		}
235
236 6211
		if (empty($guids)) {
237 27
			return null;
238
		}
239
240 6211
		$version = (int) _elgg_config()->version;
241 6211
		if (!empty($version) && ($version < 2016110900)) {
242
			// can't use this during upgrade from 2.x to 3.0
243
			return null;
244
		}
245
246 6211
		$cached_values = [];
247
248 6211
		foreach ($guids as $i => $guid) {
249 6211
			$value = $this->cache->load($guid);
250 6211
			if ($value !== null) {
251 418
				$cached_values[$guid] = $value;
252 6211
				unset($guids[$i]);
253
			}
254
		}
255
256 6211
		if (empty($guids)) {
257 417
			return $cached_values;
258
		}
259
260
		// could be useful at some point in future
261
		//$guids = $this->filterMetadataHeavyEntities($guids);
262
263
		$options = [
264 6211
			'guids' => $guids,
265 6211
			'limit' => 0,
266
			'callback' => false,
267
			'distinct' => false,
268
			'order_by' => [
269 6211
				new OrderByClause('n_table.entity_guid', 'asc'),
270 6211
				new OrderByClause('n_table.time_created', 'asc'),
271 6211
				new OrderByClause('n_table.id', 'asc')
272
			],
273
		];
274
275
		// We already have a loaded entity, so we can ignore entity access clauses
276 6211
		$ia = _elgg_services()->session->setIgnoreAccess(true);
277 6211
		$data = _elgg_services()->metadataTable->getAll($options);
278 6211
		_elgg_services()->session->setIgnoreAccess($ia);
279
280 6211
		$values = [];
281
282 6211
		foreach ($data as $i => $row) {
283 1139
			$row->value = ($row->value_type === 'text') ? $row->value : (int) $row->value;
284 1139
			$values[$row->entity_guid][] = $row;
285
		}
286
287
		// store always for every guid, even if there is no metadata
288 6211
		foreach ($guids as $guid) {
289 6211
			$metadata = elgg_extract($guid, $values, []);
290
			
291 6211
			$this->cache->save($guid, $metadata);
292 6211
			$cached_values[$guid] = $metadata;
293
		}
294
		
295 6211
		return $cached_values;
296
	}
297
298
	/**
299
	 * Filter out entities whose concatenated metadata values (INTs casted as string)
300
	 * exceed a threshold in characters. This could be used to avoid overpopulating the
301
	 * cache if RAM usage becomes an issue.
302
	 *
303
	 * @param array $guids GUIDs of entities to examine
304
	 * @param int   $limit Limit in characters of all metadata (with ints casted to strings)
305
	 *
306
	 * @return array
307
	 */
308 1
	public function filterMetadataHeavyEntities(array $guids, $limit = 1024000) {
309
310 1
		$guids = _elgg_services()->metadataTable->getAll([
311 1
			'guids' => $guids,
312 1
			'limit' => 0,
313 1
			'callback' => function($e) {
314 1
				return (int) $e->entity_guid;
315 1
			},
316
			'selects' => ['SUM(LENGTH(n_table.value)) AS bytes'],
317
			'order_by' => [
318 1
				new OrderByClause('n_table.entity_guid'),
319 1
				new OrderByClause('n_table.time_created'),
320
			],
321
			'group_by' => [
322 1
				new GroupByClause('n_table.entity_guid'),
323
			],
324
			'having' => [
325 1
				"bytes < $limit",
326
			]
327
		]);
328
329 1
		return $guids ? : [];
1 ignored issue
show
Bug Best Practice introduced by
The expression return $guids ?: array() also could return the type integer which is incompatible with the documented return type array.
Loading history...
330
	}
331
}
332