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

engine/classes/Elgg/Database/Metadata.php (4 issues)

1
<?php
2
3
namespace Elgg\Database;
4
5
use Closure;
6
use Doctrine\DBAL\Query\Expression\CompositeExpression;
7
use Elgg\Database\Clauses\AnnotationWhereClause;
8
use Elgg\Database\Clauses\EntityWhereClause;
9
use Elgg\Database\Clauses\MetadataWhereClause;
10
use Elgg\Database\Clauses\PrivateSettingWhereClause;
11
use Elgg\Database\Clauses\RelationshipWhereClause;
12
use ElggData;
13
use ElggEntity;
14
use ElggMetadata;
15
use InvalidArgumentException;
16
use InvalidParameterException;
17
use LogicException;
18
19
/**
20
 * Metadata repository contains methods for fetching metadata from database or performing
21
 * calculations on entity properties.
22
 *
23
 * API IN FLUX Do not access the methods directly, use elgg_get_metadata() instead
24
 *
25
 * @access private
26
 */
27
class Metadata extends Repository {
28
29
	/**
30
	 * {@inheritdoc}
31
	 */
32 273
	public function count() {
33 273
		$qb = Select::fromTable('metadata', 'n_table');
34
35 273
		$count_expr = $this->options->distinct ? "DISTINCT n_table.id" : "*";
36 273
		$qb->select("COUNT({$count_expr}) AS total");
37
38 273
		$qb = $this->buildQuery($qb);
39
40 272
		$result = _elgg_services()->db->getDataRow($qb);
41
42 272
		if (!$result) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result 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...
43 44
			return 0;
44
		}
45
46 228
		return (int) $result->total;
47
	}
48
49
	/**
50
	 * Performs a mathematical calculation on metadata or metadata entity's properties
51
	 *
52
	 * @param string $function      Valid numeric function
53
	 * @param string $property      Property name
54
	 * @param string $property_type 'attribute'|'metadata'|'annotation'|'private_setting'
55
	 *
56
	 * @return int|float
57
	 * @throws InvalidParameterException
58
	 */
59 8
	public function calculate($function, $property, $property_type = null) {
60
61 8
		if (!in_array(strtolower($function), QueryBuilder::$calculations)) {
62 1
			throw new InvalidArgumentException("'$function' is not a valid numeric function");
63
		}
64
65 7
		if (!isset($property_type)) {
66 1
			$property_type = 'metadata';
67
		}
68
69 7
		$qb = Select::fromTable('metadata', 'n_table');
70
71 7
		switch ($property_type) {
72
			case 'attribute':
73 2
				if (!in_array($property, ElggEntity::$primary_attr_names)) {
74 1
					throw new InvalidParameterException("'$property' is not a valid attribute");
75
				}
76
77
				/**
78
				 * @todo When no entity constraints are present, do we need to ensure that entity access clause is added?
79
				 */
80 1
				$alias = $qb->joinEntitiesTable('n_table', 'entity_guid', 'inner', 'e');
81 1
				$qb->addSelect("{$function}({$alias}.{$property}) AS calculation");
82 1
				break;
83
84
			case 'metadata' :
85 3
				$alias = 'n_table';
86 3
				if (!empty($this->options->metadata_name_value_pairs) && $this->options->metadata_name_value_pairs[0]->names != $property) {
87 1
					$alias = $qb->joinMetadataTable('n_table', 'entity_guid', $property);
88
				}
89 3
				$qb->addSelect("{$function}($alias.value) AS calculation");
90 3
				break;
91
92
			case 'annotation' :
93 1
				$alias = $qb->joinAnnotationTable('n_table', 'entity_guid', $property);
94 1
				$qb->addSelect("{$function}({$alias}.value) AS calculation");
95 1
				break;
96
97
			case 'private_setting' :
98 1
				$alias = $qb->joinPrivateSettingsTable('n_table', 'entity_guid', $property);
99 1
				$qb->addSelect("{$function}({$alias}.value) AS calculation");
100 1
				break;
101
		}
102
103 6
		$qb = $this->buildQuery($qb);
104
105 6
		$result = _elgg_services()->db->getDataRow($qb);
106
107 6
		if (!$result) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result 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...
108
			return 0;
109
		}
110
111 6
		return (int) $result->calculation;
112
	}
113
114
	/**
115
	 * Fetch metadata
116
	 *
117
	 * @param int      $limit    Limit
118
	 * @param int      $offset   Offset
119
	 * @param callable $callback Custom callback
120
	 *
121
	 * @return ElggMetadata[]
122
	 */
123 602
	public function get($limit = null, $offset = null, $callback = null) {
124
125 602
		$qb = Select::fromTable('metadata', 'n_table');
126
127 602
		$distinct = $this->options->distinct ? "DISTINCT" : "";
128 602
		$qb->select("$distinct n_table.*");
129
130 602
		$this->expandInto($qb, 'n_table');
131
132 602
		$qb = $this->buildQuery($qb);
133
134
		// Keeping things backwards compatible
135 601
		$original_order = elgg_extract('order_by', $this->options->__original_options);
136 601
		if (empty($original_order) && $original_order !== false) {
137 476
			$qb->addOrderBy('n_table.time_created', 'asc');
138 476
			$qb->addOrderBy('n_table.id', 'asc');
139
		}
140
141 601
		if ($limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit 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...
142 211
			$qb->setMaxResults((int) $limit);
143 211
			$qb->setFirstResult((int) $offset);
144
		}
145
146 601
		$callback = $callback ? : $this->options->callback;
147 601
		if (!isset($callback)) {
148 476
			$callback = function ($row) {
149 200
				return new ElggMetadata($row);
150 476
			};
151
		}
152
153 601
		return _elgg_services()->db->getData($qb, $callback);
154
	}
155
156
	/**
157
	 * Execute the query resolving calculation, count and/or batch options
158
	 *
159
	 * @return array|ElggData[]|ElggMetadata[]|false|int
160
	 * @throws LogicException
161
	 */
162 652
	public function execute() {
163
164 652
		if ($this->options->annotation_calculation) {
165 2
			$clauses = $this->options->annotation_name_value_pairs;
166 2
			if (count($clauses) > 1 && $this->options->annotation_name_value_pairs_operator !== 'OR') {
167 1
				throw new LogicException("Annotation calculation can not be performed on multiple annotation name value pairs merged with AND");
168
			}
169
170 1
			$clause = array_shift($clauses);
171
172 1
			return $this->calculate($this->options->annotation_calculation, $clause->names, 'annotation');
173 650
		} else if ($this->options->metadata_calculation) {
174 2
			$clauses = $this->options->metadata_name_value_pairs;
175 2
			if (count($clauses) > 1 && $this->options->metadata_name_value_pairs_operator !== 'OR') {
176 1
				throw new LogicException("Metadata calculation can not be performed on multiple metadata name value pairs merged with AND");
177
			}
178
179 1
			$clause = array_shift($clauses);
180
181 1
			return $this->calculate($this->options->metadata_calculation, $clause->names, 'metadata');
182 648
		} else if ($this->options->count) {
183 273
			return $this->count();
184 646
		} else if ($this->options->batch) {
185 272
			return $this->batch($this->options->limit, $this->options->offset, $this->options->callback);
186
		} else {
187 602
			return $this->get($this->options->limit, $this->options->offset, $this->options->callback);
188
		}
189
	}
190
191
	/**
192
	 * Build a database query
193
	 *
194
	 * @param QueryBuilder $qb
195
	 *
196
	 * @return QueryBuilder
197
	 */
198 654
	protected function buildQuery(QueryBuilder $qb) {
199
200 654
		$ands = [];
201
202 654
		foreach ($this->options->joins as $join) {
203 1
			$join->prepare($qb, 'n_table');
204
		}
205
206 654
		foreach ($this->options->wheres as $where) {
207 2
			$ands[] = $where->prepare($qb, 'n_table');
208
		}
209
210 654
		$ands[] = $this->buildPairedMetadataClause($qb, $this->options->metadata_name_value_pairs, $this->options->metadata_name_value_pairs_operator);
211 654
		$ands[] = $this->buildEntityWhereClause($qb);
212 652
		$ands[] = $this->buildPairedMetadataClause($qb, $this->options->search_name_value_pairs, 'OR');
213 652
		$ands[] = $this->buildPairedAnnotationClause($qb, $this->options->annotation_name_value_pairs, $this->options->annotation_name_value_pairs_operator);
214 652
		$ands[] = $this->buildPairedPrivateSettingsClause($qb, $this->options->private_setting_name_value_pairs, $this->options->private_setting_name_value_pairs_operator);
215 652
		$ands[] = $this->buildPairedRelationshipClause($qb, $this->options->relationship_pairs);
216
217 652
		$ands = $qb->merge($ands);
218
219 652
		if (!empty($ands)) {
220 652
			$qb->andWhere($ands);
221
		}
222
223 652
		return $qb;
224
	}
225
226
	/**
227
	 * Process entity attribute wheres
228
	 * Joins entities table on entity guid in metadata table and applies where clauses
229
	 *
230
	 * @param QueryBuilder $qb Query builder
231
	 *
232
	 * @return Closure|CompositeExpression|mixed|null|string
233
	 */
234 654
	protected function buildEntityWhereClause(QueryBuilder $qb) {
235
236
		// Even if all of these properties are empty, we want to add this clause regardless,
237
		// to ensure that entity access clauses are appended to the query
238
239 654
		$joined_alias = $qb->joinEntitiesTable('n_table', 'entity_guid', 'inner', 'e');
240 654
		return EntityWhereClause::factory($this->options)->prepare($qb, $joined_alias);
241
	}
242
243
	/**
244
	 * Process metadata name value pairs
245
	 * Applies where clauses to the selected metadata table
246
	 *
247
	 * @param QueryBuilder          $qb      Query builder
248
	 * @param MetadataWhereClause[] $clauses Where clauses
249
	 * @param string                $boolean Merge boolean
250
	 *
251
	 * @return CompositeExpression|string
252
	 */
253 654
	protected function buildPairedMetadataClause(QueryBuilder $qb, $clauses, $boolean = 'AND') {
254 654
		$parts = [];
255
256
257 654
		foreach ($clauses as $clause) {
258 523
			$parts[] = $clause->prepare($qb, 'n_table');
259
		}
260
261 654
		return $qb->merge($parts, $boolean);
262
	}
263
264
	/**
265
	 * Process annotation name value pairs
266
	 * Joins annotation table on entity_guid in the metadata table and applies where clauses
267
	 *
268
	 * @param QueryBuilder            $qb      Query builder
269
	 * @param AnnotationWhereClause[] $clauses Where clauses
270
	 * @param string                  $boolean Merge boolean
271
	 *
272
	 * @return CompositeExpression|string
273
	 */
274 652
	protected function buildPairedAnnotationClause(QueryBuilder $qb, $clauses, $boolean = 'AND') {
275 652
		$parts = [];
276
277 652
		foreach ($clauses as $clause) {
278 3
			if (strtoupper($boolean) === 'OR' || count($clauses) > 1) {
279 2
				$joined_alias = $qb->joinAnnotationTable('n_table', 'entity_guid');
280
			} else {
281 1
				$joined_alias = $qb->joinAnnotationTable('n_table', 'entity_guid', $clause->names);
282
			}
283 3
			$parts[] = $clause->prepare($qb, $joined_alias);
284
		}
285
286 652
		return $qb->merge($parts, $boolean);
287
	}
288
289
	/**
290
	 * Process private settings name value pairs
291
	 * Joins private settings table on entity_guid in the metadata table and applies where clauses
292
	 *
293
	 * @param QueryBuilder                $qb      Query builder
294
	 * @param PrivateSettingWhereClause[] $clauses Where clauses
295
	 * @param string                      $boolean Merge boolean
296
	 *
297
	 * @return CompositeExpression|string
298
	 */
299 652
	protected function buildPairedPrivateSettingsClause(QueryBuilder $qb, $clauses, $boolean = 'AND') {
300 652
		$parts = [];
301
302 652
		foreach ($clauses as $clause) {
303 3
			if (strtoupper($boolean) === 'OR' || count($clauses) > 1) {
304 2
				$joined_alias = $qb->joinPrivateSettingsTable('n_table', 'entity_guid');
305
			} else {
306 1
				$joined_alias = $qb->joinPrivateSettingsTable('n_table', 'entity_guid', $clause->names);
307
			}
308 3
			$parts[] = $clause->prepare($qb, $joined_alias);
309
		}
310
311 652
		return $qb->merge($parts, $boolean);
312
	}
313
314
	/**
315
	 * Process relationship name value pairs
316
	 * Joins relationship table on entity_guid in the metadata table and applies where clauses
317
	 *
318
	 * @note Note that $relationship_join_on does not apply here, as there is only one guid column in the metadata table
319
	 *
320
	 * @param QueryBuilder              $qb      Query builder
321
	 * @param RelationshipWhereClause[] $clauses Where clauses
322
	 * @param string                    $boolean Merge boolean
323
	 *
324
	 * @return CompositeExpression|string
325
	 */
326 652
	protected function buildPairedRelationshipClause(QueryBuilder $qb, $clauses, $boolean = 'AND') {
327 652
		$parts = [];
328
329 652
		foreach ($clauses as $clause) {
330 2
			if (strtoupper($boolean) == 'OR' || count($clauses) > 1) {
331 1
				$joined_alias = $qb->joinRelationshipTable('n_table', 'entity_guid', null, $clause->inverse);
332
			} else {
333 1
				$joined_alias = $qb->joinRelationshipTable('n_table', 'entity_guid', $clause->names, $clause->inverse);
1 ignored issue
show
$clause->names of type string[] is incompatible with the type string expected by parameter $name of Elgg\Database\QueryBuild...joinRelationshipTable(). ( Ignorable by Annotation )

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

333
				$joined_alias = $qb->joinRelationshipTable('n_table', 'entity_guid', /** @scrutinizer ignore-type */ $clause->names, $clause->inverse);
Loading history...
334
			}
335 2
			$parts[] = $clause->prepare($qb, $joined_alias);
336
		}
337
338 652
		return $qb->merge($parts, $boolean);
339
	}
340
}
341