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

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