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

engine/classes/Elgg/Database/River.php (5 issues)

1
<?php
2
3
namespace Elgg\Database;
4
5
use Doctrine\DBAL\Query\Expression\CompositeExpression;
6
use Elgg\Database\Clauses\AnnotationWhereClause;
7
use Elgg\Database\Clauses\EntityWhereClause;
8
use Elgg\Database\Clauses\RelationshipWhereClause;
9
use Elgg\Database\Clauses\RiverWhereClause;
10
use ElggEntity;
11
use ElggRiverItem;
12
use InvalidArgumentException;
13
use InvalidParameterException;
14
15
/**
16
 * River repository contains methods for fetching/counting river items
17
 *
18
 * API IN FLUX Do not access the methods directly, use elgg_get_river() instead
19
 *
20
 * @access private
21
 */
22
class River extends Repository {
23
24
	/**
25
	 * {@inheritdoc}
26
	 */
27 228
	public function __construct(array $options = []) {
28
		$singulars = [
29 228
			'id',
30
			'subject_guid',
31
			'object_guid',
32
			'target_guid',
33
			'annotation_id',
34
			'action_type',
35
			'type',
36
			'subtype'
37
		];
38
39 228
		$options = _elgg_normalize_plural_options_array($options, $singulars);
40
41
		$defaults = [
42 228
			'ids' => null,
43
			'subject_guids' => null,
44
			'object_guids' => null,
45
			'target_guids' => null,
46
			'annotation_ids' => null,
47
			'views' => null,
48
			'action_types' => null,
49
			'posted_time_lower' => null,
50
			'posted_time_upper' => null,
51
			'limit' => 20,
52
			'offset' => 0,
53
		];
54
55 228
		$options = array_merge($defaults, $options);
56 228
		parent::__construct($options);
57 228
	}
58
59
	/**
60
	 * Build and execute a new query from an array of legacy options
61
	 *
62
	 * @param array $options Options
63
	 *
64
	 * @return ElggRiverItem[]|int|mixed
65
	 */
66 228
	public static function find(array $options = []) {
67 228
		return parent::find($options);
68
	}
69
70
	/**
71
	 * {@inheritdoc}
72
	 */
73 218
	public function count() {
74 218
		$qb = Select::fromTable('river', 'rv');
75
76 218
		$count_expr = $this->options->distinct ? "DISTINCT rv.id" : "*";
77 218
		$qb->select("COUNT({$count_expr}) AS total");
78
79 218
		$qb = $this->buildQuery($qb);
80
81 217
		$result = _elgg_services()->db->getDataRow($qb);
82
83 217
		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...
84 1
			return 0;
85
		}
86
87 216
		return (int) $result->total;
88
	}
89
90
	/**
91
	 * Performs a mathematical calculation on river annotations
92
	 *
93
	 * @param string $function      Valid numeric function
94
	 * @param string $property      Property name
95
	 * @param string $property_type 'annotation'
96
	 *
97
	 * @return int|float
98
	 * @throws InvalidParameterException
99
	 */
100 1
	public function calculate($function, $property, $property_type = 'annotation') {
101
102 1
		if (!in_array(strtolower($function), QueryBuilder::$calculations)) {
103
			throw new InvalidArgumentException("'$function' is not a valid numeric function");
104
		}
105
106 1
		$qb = Select::fromTable('river', 'rv');
107
108 1
		$alias = 'n_table';
109 1
		if (!empty($this->options->annotation_name_value_pairs) && $this->options->annotation_name_value_pairs[0]->names != $property) {
110
			$alias = $qb->getNextJoinAlias();
111
112
			$annotation = new AnnotationWhereClause();
113
			$annotation->names = $property;
0 ignored issues
show
Documentation Bug introduced by
It seems like $property of type string is incompatible with the declared type string[] of property $names.

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...
114
			$qb->addClause($annotation, $alias);
115
		}
116
117 1
		$qb->join('rv', 'annotations', $alias, "rv.annotation_id = $alias.id");
118 1
		$qb->select("{$function}(n_table.value) AS calculation");
119
120 1
		$qb = $this->buildQuery($qb);
121
122 1
		$result = _elgg_services()->db->getDataRow($qb);
123
124 1
		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...
125
			return 0;
126
		}
127
128 1
		return (int) $result->calculation;
129
	}
130
131
	/**
132
	 * Fetch river items
133
	 *
134
	 * @param int      $limit    Limit
135
	 * @param int      $offset   Offset
136
	 * @param callable $callback Custom callback
137
	 *
138
	 * @return ElggEntity[]
139
	 * @throws \DatabaseException
140
	 */
141 19
	public function get($limit = null, $offset = null, $callback = null) {
142
143 19
		$qb = Select::fromTable('river', 'rv');
144
145 19
		$distinct = $this->options->distinct ? "DISTINCT" : "";
146 19
		$qb->select("$distinct rv.*");
147
148 19
		$this->expandInto($qb, 'rv');
149
150 19
		$qb = $this->buildQuery($qb);
151
152
		// Keeping things backwards compatible
153 18
		$original_order = elgg_extract('order_by', $this->options->__original_options);
154 18
		if (empty($original_order) && $original_order !== false) {
155 11
			$qb->addOrderBy('rv.posted', 'desc');
156
		}
157
158 18
		if ($limit) {
159 18
			$qb->setMaxResults((int) $limit);
160 18
			$qb->setFirstResult((int) $offset);
161
		}
162
163 18
		$callback = $callback ? : $this->options->callback;
164 18
		if (!isset($callback)) {
165 11
			$callback = function ($row) {
166 11
				return new ElggRiverItem($row);
167 11
			};
168
		}
169
170 18
		$items = _elgg_services()->db->getData($qb, $callback);
171
172 18
		if ($items) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $items 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...
173 18
			$preload = array_filter($items, function($e) {
174 18
				return $e instanceof ElggRiverItem;
175 18
			});
176
177 18
			_elgg_services()->entityPreloader->preload($preload, [
178 18
				'subject_guid',
179
				'object_guid',
180
				'target_guid',
181
			]);
182
		}
183
184 18
		return $items;
185
	}
186
187
	/**
188
	 * Execute the query resolving calculation, count and/or batch options
189
	 *
190
	 * @return array|\ElggData[]|ElggEntity[]|false|int
191
	 * @throws \LogicException
192
	 */
193 228
	public function execute() {
194
195 228
		if ($this->options->annotation_calculation) {
196 2
			$clauses = $this->options->annotation_name_value_pairs;
197 2
			if (count($clauses) > 1 && $this->options->annotation_name_value_pairs_operator !== 'OR') {
198 1
				throw new \LogicException("Annotation calculation can not be performed on multiple annotation name value pairs merged with AND");
199
			}
200
201 1
			$clause = array_shift($clauses);
202
203 1
			return $this->calculate($this->options->annotation_calculation, $clause->names, 'annotation');
204 226
		} else if ($this->options->count) {
205 218
			return $this->count();
206 223
		} else if ($this->options->batch) {
207 216
			return $this->batch($this->options->limit, $this->options->offset, $this->options->callback);
208
		} else {
209 19
			return $this->get($this->options->limit, $this->options->offset, $this->options->callback);
210
		}
211
	}
212
213
	/**
214
	 * Build a database query
215
	 *
216
	 * @param QueryBuilder $qb
217
	 *
218
	 * @return QueryBuilder
219
	 */
220 227
	protected function buildQuery(QueryBuilder $qb) {
221
222 227
		$ands = [];
223
224 227
		foreach ($this->options->joins as $join) {
225 1
			$join->prepare($qb, 'rv');
226
		}
227
228 227
		foreach ($this->options->wheres as $where) {
229 1
			$ands[] = $where->prepare($qb, 'rv');
230
		}
231
232 227
		$ands[] = $this->buildRiverClause($qb);
233 225
		$ands[] = $this->buildEntityClauses($qb);
234 225
		$ands[] = $this->buildPairedAnnotationClause($qb, $this->options->annotation_name_value_pairs, $this->options->annotation_name_value_pairs_operator);
235 225
		$ands[] = $this->buildPairedRelationshipClause($qb, $this->options->relationship_pairs);
236
237 225
		$ands = $qb->merge($ands);
238
239 225
		if (!empty($ands)) {
240 225
			$qb->andWhere($ands);
241
		}
242
243 225
		return $qb;
244
	}
245
246
	/**
247
	 * Process river properties
248
	 *
249
	 * @param QueryBuilder $qb Query builder
250
	 *
251
	 * @return CompositeExpression|mixed|null|string
252
	 */
253 227
	protected function buildRiverClause(QueryBuilder $qb) {
254 227
		$where = new RiverWhereClause();
255 227
		$where->ids = $this->options->ids;
256 227
		$where->views = $this->options->views;
257 227
		$where->action_types = $this->options->action_types;
258 227
		$where->subject_guids = $this->options->subject_guids;
259 227
		$where->object_guids = $this->options->object_guids;
260 227
		$where->target_guids = $this->options->target_guids;
261 227
		$where->type_subtype_pairs = $this->options->type_subtype_pairs;
262 227
		$where->created_after = $this->options->created_after;
263 227
		$where->created_before = $this->options->created_before;
264
265 227
		return $where->prepare($qb, 'rv');
266
	}
267
268
	/**
269
	 * Add subject, object and target clauses
270
	 * Make sure all three are accessible by the user
271
	 *
272
	 * @param QueryBuilder $qb Query builder
273
	 *
274
	 * @return CompositeExpression|mixed|null|string
275
	 */
276 225
	public function buildEntityClauses($qb) {
277
278 225
		$use_access_clause = !_elgg_services()->userCapabilities->canBypassPermissionsCheck();
279
280 225
		$ands = [];
281
282 225
		if ($this->options->subject_guids || $use_access_clause) {
283 223
			$qb->joinEntitiesTable('rv', 'subject_guid', 'inner', 'se');
284 223
			$subject = new EntityWhereClause();
285 223
			$subject->guids = $this->options->subject_guids;
286 223
			$ands[] = $subject->prepare($qb, 'se');
287
		}
288
289 225
		if ($this->options->object_guids || $use_access_clause) {
290 223
			$qb->joinEntitiesTable('rv', 'object_guid', 'inner', 'oe');
291 223
			$object = new EntityWhereClause();
292 223
			$object->guids = $this->options->object_guids;
293 223
			$ands[] = $object->prepare($qb, 'oe');
294
		}
295
296 225
		if ($this->options->target_guids || $use_access_clause) {
297 223
			$target_ors = [];
298 223
			$qb->joinEntitiesTable('rv', 'target_guid', 'left', 'te');
299 223
			$target = new EntityWhereClause();
300 223
			$target->guids = $this->options->target_guids;
301 223
			$target_ors[] = $target->prepare($qb, 'te');
302
			// Note the LEFT JOIN
303 223
			$target_ors[] = $qb->compare('te.guid', 'IS NULL');
304 223
			$ands[] = $qb->merge($target_ors, 'OR');
305
		}
306
307 225
		return $qb->merge($ands);
308
	}
309
310
	/**
311
	 * Process annotation name value pairs
312
	 * Joins the annotation table on entity guid in the entities table and applies annotation where clauses
313
	 *
314
	 * @param QueryBuilder            $qb      Query builder
315
	 * @param AnnotationWhereClause[] $clauses Where clauses
316
	 * @param string                  $boolean Merge boolean
317
	 *
318
	 * @return CompositeExpression|string
319
	 */
320 225
	protected function buildPairedAnnotationClause(QueryBuilder $qb, $clauses, $boolean = 'AND') {
321 225
		$parts = [];
322
323 225
		foreach ($clauses as $clause) {
324 33
			if (strtoupper($boolean) === 'OR' || count($clauses) === 1) {
325 32
				$joined_alias = 'n_table';
326
			} else {
327 1
				$joined_alias = $qb->getNextJoinAlias();
328
			}
329 33
			$joins = $qb->getQueryPart('join');
330 33
			$is_joined = false;
331 33
			if (!empty($joins['rv'])) {
332 3
				foreach ($joins['rv'] as $join) {
333 3
					if ($join['joinAlias'] === $joined_alias) {
334 3
						$is_joined = true;
335
					}
336
				}
337
			}
338
339 33
			if (!$is_joined) {
340 32
				$qb->join('rv', 'annotations', $joined_alias, "$joined_alias.id = rv.annotation_id");
341
			}
342
343 33
			$parts[] = $clause->prepare($qb, $joined_alias);
344
		}
345
346 225
		return $qb->merge($parts, $boolean);
347
	}
348
349
	/**
350
	 * Process relationship pairs
351
	 *
352
	 * @param QueryBuilder              $qb      Query builder
353
	 * @param RelationshipWhereClause[] $clauses Where clauses
354
	 * @param string                    $boolean Merge boolean
355
	 *
356
	 * @return CompositeExpression|string
357
	 */
358 225
	protected function buildPairedRelationshipClause(QueryBuilder $qb, $clauses, $boolean = 'AND') {
359 225
		$parts = [];
360
361 225
		foreach ($clauses as $clause) {
362 2
			$join_on = $clause->join_on === 'guid' ? 'subject_guid' : $clause->join_on;
363 2
			if (strtoupper($boolean) == 'OR' || count($clauses) === 1) {
364 1
				$joined_alias = $qb->joinRelationshipTable('rv', $join_on, null, $clause->inverse, 'inner', 'r');
365
			} else {
366 1
				$joined_alias = $qb->joinRelationshipTable('rv', $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

366
				$joined_alias = $qb->joinRelationshipTable('rv', $join_on, /** @scrutinizer ignore-type */ $clause->names, $clause->inverse);
Loading history...
367
			}
368 2
			$parts[] = $clause->prepare($qb, $joined_alias);
369
		}
370
371 225
		return $qb->merge($parts, $boolean);
372
	}
373
}
374