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

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