Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

engine/classes/Elgg/Database/MetadataTable.php (8 issues)

1
<?php
2
3
namespace Elgg\Database;
4
5
use Elgg\Cache\MetadataCache;
6
use Elgg\Database;
7
use Elgg\Database\Clauses\MetadataWhereClause;
8
use Elgg\EventsService as Events;
9
use Elgg\TimeUsing;
10
use ElggMetadata;
11
use ElggEntity;
12
13
/**
14
 * This class interfaces with the database to perform CRUD operations on metadata
15
 *
16
 * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
17
 *
18
 * @access private
19
 */
20
class MetadataTable {
21
22
	use TimeUsing;
23
24
	/**
25
	 * @var MetadataCache
26
	 */
27
	protected $metadata_cache;
28
29
	/**
30
	 * @var Database
31
	 */
32
	protected $db;
33
34
	/**
35
	 * @var Events
36
	 */
37
	protected $events;
38
39
	/**
40
	 * @var string[]
41
	 */
42
	protected $tag_names = [];
43
44
	const MYSQL_TEXT_BYTE_LIMIT = 65535;
45
46
	/**
47
	 * Constructor
48
	 *
49
	 * @param MetadataCache $metadata_cache A cache for this table
50
	 * @param Database      $db             The Elgg database
51
	 * @param Events        $events         The events registry
52
	 */
53 4417
	public function __construct(
54
		MetadataCache $metadata_cache,
55
		Database $db,
56
		Events $events
57
	) {
58 4417
		$this->metadata_cache = $metadata_cache;
59 4417
		$this->db = $db;
60 4417
		$this->events = $events;
61 4417
	}
62
63
	/**
64
	 * Registers a metadata name as containing tags for an entity.
65
	 *
66
	 * @param string $name Tag name
67
	 *
68
	 * @return bool
69
	 */
70 32
	public function registerTagName($name) {
71 32
		if (!in_array($name, $this->tag_names)) {
72 19
			$this->tag_names[] = $name;
73
		}
74
75 32
		return true;
76
	}
77
78
	/**
79
	 * Unregisters a metadata tag name
80
	 *
81
	 * @param string $name Tag name
82
	 *
83
	 * @return bool
84
	 */
85 1
	public function unregisterTagName($name) {
86 1
		$index = array_search($name, $this->tag_names);
87 1
		if ($index >= 0) {
88 1
			unset($this->tag_names[$index]);
89
90 1
			return true;
91
		}
92
93
		return false;
94
	}
95
96
	/**
97
	 * Returns an array of valid metadata names for tags.
98
	 *
99
	 * @return string[]
100
	 */
101 3
	public function getTagNames() {
102 3
		return $this->tag_names;
103
	}
104
105
	/**
106
	 * Get a specific metadata object by its id
107
	 *
108
	 * @see MetadataTable::getAll()
109
	 *
110
	 * @param int $id The id of the metadata object being retrieved.
111
	 *
112
	 * @return ElggMetadata|false  false if not found
113
	 */
114 9
	public function get($id) {
115 9
		$qb = Select::fromTable('metadata');
116 9
		$qb->select('*');
117
118 9
		$where = new MetadataWhereClause();
119 9
		$where->ids = $id;
1 ignored issue
show
Documentation Bug introduced by
It seems like $id of type integer is incompatible with the declared type integer[] of property $ids.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
120 9
		$qb->addClause($where);
121
122 9
		$row = $this->db->getDataRow($qb);
123 9
		if ($row) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $row of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
124 9
			return new ElggMetadata($row);
125
		}
126
127
		return false;
128
	}
129
130
	/**
131
	 * Deletes metadata using its ID
132
	 *
133
	 * @param ElggMetadata $metadata Metadata
134
	 *
135
	 * @return bool
136
	 */
137 199
	public function delete(ElggMetadata $metadata) {
138 199
		if (!$metadata->id || !$metadata->canEdit()) {
139
			return false;
140
		}
141
142 199
		if (!elgg_trigger_event('delete', 'metadata', $metadata)) {
143
			return false;
144
		}
145
146 199
		$qb = Delete::fromTable('metadata');
147 199
		$qb->where($qb->compare('id', '=', $metadata->id, ELGG_VALUE_INTEGER));
148
149 199
		$deleted = $this->db->deleteData($qb);
150
151 199
		if ($deleted) {
152 199
			$this->metadata_cache->clear($metadata->entity_guid);
153
		}
154
155 199
		return $deleted;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $deleted returns the type integer which is incompatible with the documented return type boolean.
Loading history...
156
	}
157
158
	/**
159
	 * Create a new metadata object, or update an existing one (if multiple is allowed)
160
	 *
161
	 * Metadata can be an array by setting allow_multiple to true, but it is an
162
	 * indexed array with no control over the indexing
163
	 *
164
	 * @param ElggMetadata $metadata       Metadata
165
	 * @param bool         $allow_multiple Allow multiple values for one key. Default is false
166
	 *
167
	 * @return int|false id of metadata or false if failure
168
	 */
169 1028
	public function create(ElggMetadata $metadata, $allow_multiple = false) {
170 1028
		if (!isset($metadata->value) || !isset($metadata->entity_guid)) {
171
			elgg_log("Metadata must have a value and entity guid", 'ERROR');
172
			return false;
173
		}
174
175 1028
		if (!is_scalar($metadata->value)) {
176
			elgg_log("To set multiple metadata values use ElggEntity::setMetadata", 'ERROR');
177
			return false;
178
		}
179
180 1028
		if ($metadata->id) {
181
			if ($this->update($metadata)) {
182
				return $metadata->id;
183
			}
184
		}
185
186 1028
		if (strlen($metadata->value) > self::MYSQL_TEXT_BYTE_LIMIT) {
187
			elgg_log("Metadata '$metadata->name' is above the MySQL TEXT size limit and may be truncated.", 'WARNING');
188
		}
189
190 1028
		if (!$allow_multiple) {
191 1027
			$id = $this->getIdsByName($metadata->entity_guid, $metadata->name);
192
193 1027
			if (is_array($id)) {
194
				throw new \LogicException("Multiple '{$metadata->name}' metadata values exist for entity [guid: {$metadata->entity_guid}]. Use ElggEntity::setMetadata()");
195
			}
196
197 1027
			if ($id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type null|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
198 198
				$metadata->id = $id;
199
200 198
				if ($this->update($metadata)) {
201 198
					return $metadata->id;
202
				}
203
			}
204
		}
205
206 1028
		if (!$this->events->triggerBefore('create', 'metadata', $metadata)) {
0 ignored issues
show
$metadata of type ElggMetadata is incompatible with the type string expected by parameter $object of Elgg\EventsService::triggerBefore(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

206
		if (!$this->events->triggerBefore('create', 'metadata', /** @scrutinizer ignore-type */ $metadata)) {
Loading history...
207
			return false;
208
		}
209
210 1028
		$time_created = $this->getCurrentTime()->getTimestamp();
211
212 1028
		$qb = Insert::intoTable('metadata');
213 1028
		$qb->values([
214 1028
			'name' => $qb->param($metadata->name, ELGG_VALUE_STRING),
215 1028
			'entity_guid' => $qb->param($metadata->entity_guid, ELGG_VALUE_INTEGER),
216 1028
			'value' => $qb->param($metadata->value, $metadata->value_type === 'integer' ? ELGG_VALUE_INTEGER : ELGG_VALUE_STRING),
217 1028
			'value_type' => $qb->param($metadata->value_type, ELGG_VALUE_STRING),
218 1028
			'time_created' => $qb->param($time_created, ELGG_VALUE_INTEGER),
219
		]);
220
221 1028
		$id = $this->db->insertData($qb);
222
223 1028
		if ($id === false) {
224
			return false;
225
		}
226
227 1028
		$metadata->id = (int) $id;
228 1028
		$metadata->time_created = $time_created;
229
230 1028
		if ($this->events->trigger('create', 'metadata', $metadata)) {
231 1028
			$this->metadata_cache->clear($metadata->entity_guid);
232
233 1028
			$this->events->triggerAfter('create', 'metadata', $metadata);
0 ignored issues
show
$metadata of type ElggMetadata is incompatible with the type string expected by parameter $object of Elgg\EventsService::triggerAfter(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

233
			$this->events->triggerAfter('create', 'metadata', /** @scrutinizer ignore-type */ $metadata);
Loading history...
234
235 1028
			return $id;
236
		} else {
237
			$this->delete($metadata);
238
239
			return false;
240
		}
241
	}
242
243
	/**
244
	 * Update a specific piece of metadata
245
	 *
246
	 * @param ElggMetadata $metadata Updated metadata
247
	 *
248
	 * @return bool
249
	 */
250 198
	public function update(ElggMetadata $metadata) {
251 198
		if (!$metadata->canEdit()) {
252 1
			return false;
253
		}
254
255 198
		if (!$this->events->triggerBefore('update', 'metadata', $metadata)) {
0 ignored issues
show
$metadata of type ElggMetadata is incompatible with the type string expected by parameter $object of Elgg\EventsService::triggerBefore(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

255
		if (!$this->events->triggerBefore('update', 'metadata', /** @scrutinizer ignore-type */ $metadata)) {
Loading history...
256
			return false;
257
		}
258
259 198
		if (strlen($metadata->value) > self::MYSQL_TEXT_BYTE_LIMIT) {
260
			elgg_log("Metadata '$metadata->name' is above the MySQL TEXT size limit and may be truncated.", 'WARNING');
261
		}
262
263 198
		$qb = Update::table('metadata');
264 198
		$qb->set('name', $qb->param($metadata->name, ELGG_VALUE_STRING))
265 198
			->set('value', $qb->param($metadata->value, $metadata->value_type === 'integer' ? ELGG_VALUE_INTEGER : ELGG_VALUE_STRING))
266 198
			->set('value_type', $qb->param($metadata->value_type, ELGG_VALUE_STRING))
267 198
			->where($qb->compare('id', '=', $metadata->id, ELGG_VALUE_INTEGER));
268
269 198
		$result = $this->db->updateData($qb);
270
271 198
		if ($result === false) {
272
			return false;
273
		}
274
275 198
		$this->metadata_cache->clear($metadata->entity_guid);
276
277 198
		$this->events->trigger('update', 'metadata', $metadata);
278 198
		$this->events->triggerAfter('update', 'metadata', $metadata);
0 ignored issues
show
$metadata of type ElggMetadata is incompatible with the type string expected by parameter $object of Elgg\EventsService::triggerAfter(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

278
		$this->events->triggerAfter('update', 'metadata', /** @scrutinizer ignore-type */ $metadata);
Loading history...
279
280 198
		return true;
281
	}
282
283
	/**
284
	 * Returns metadata
285
	 *
286
	 * Accepts all {@link elgg_get_entities()} options for entity restraints.
287
	 *
288
	 * @see     elgg_get_entities()
289
	 *
290
	 * @param array $options Options
291
	 *
292
	 * @return ElggMetadata[]|mixed
293
	 */
294 590
	public function getAll(array $options = []) {
295
296 590
		$options['metastring_type'] = 'metadata';
297 590
		$options = _elgg_normalize_metastrings_options($options);
298
299 590
		return Metadata::find($options);
300
	}
301
302
	/**
303
	 * Deletes metadata based on $options.
304
	 *
305
	 * @warning Unlike elgg_get_metadata() this will not accept an empty options array!
306
	 *          This requires at least one constraint:
307
	 *          metadata_name(s), metadata_value(s), or guid(s) must be set.
308
	 *
309
	 * @see     elgg_get_metadata()
310
	 * @see     elgg_get_entities()
311
	 *
312
	 * @param array $options Options
313
	 *
314
	 * @return bool|null true on success, false on failure, null if no metadata to delete.
315
	 */
316 271
	public function deleteAll(array $options) {
317 271
		if (!_elgg_is_valid_options_for_batch_operation($options, 'metadata')) {
318
			return false;
319
		}
320
321
		// This moved last in case an object's constructor sets metadata. Currently the batch
322
		// delete process has to create the entity to delete its metadata. See #5214
323 271
		$this->metadata_cache->invalidateByOptions($options);
324
325 271
		$options['batch'] = true;
326 271
		$options['batch_size'] = 50;
327 271
		$options['batch_inc_offset'] = false;
328
329 271
		$metadata = Metadata::find($options);
330 271
		$count = $metadata->count();
331
332 271
		if (!$count) {
333 91
			return;
334
		}
335
336 199
		$success = 0;
337 199
		foreach ($metadata as $md) {
338 199
			if ($md->delete()) {
339 199
				$success++;
340
			}
341
		}
342
343 199
		return $success == $count;
344
	}
345
346
	/**
347
	 * Returns ID(s) of metadata with a particular name attached to an entity
348
	 *
349
	 * @param int    $entity_guid Entity guid
350
	 * @param string $name        Metadata name
351
	 *
352
	 * @return int[]|int|null
353
	 */
354 1027
	public function getIdsByName($entity_guid, $name) {
355 1027
		if ($this->metadata_cache->isLoaded($entity_guid)) {
356 666
			$ids = $this->metadata_cache->getSingleId($entity_guid, $name);
357
		} else {
358 1027
			$qb = Select::fromTable('metadata');
359 1027
			$qb->select('id')
360 1027
				->where($qb->compare('entity_guid', '=', $entity_guid, ELGG_VALUE_INTEGER))
361 1027
				->andWhere($qb->compare('name', '=', $name, ELGG_VALUE_STRING));
362
363 1027
			$callback = function (\stdClass $row) {
364 1
				return (int) $row->id;
365 1027
			};
366
367
			$ids = $this->db->getData($qb, $callback);
368
		}
369
370
		if (empty($ids)) {
371
			return null;
372
		}
373
374
		if (is_array($ids) && count($ids) === 1) {
375
			return array_shift($ids);
376
		}
377
378
		return $ids;
379
	}
380
}
381