Completed
Push — 3.1 ( 67429b...cb395f )
by Jeroen
105:07 queued 10s
created

engine/classes/Elgg/Database/River.php (1 issue)

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
 * @internal
21
 */
22
class River extends Repository {
23
24
	/**
25
	 * {@inheritdoc}
26
	 */
27 293
	public function __construct(array $options = []) {
28
		$singulars = [
29 293
			'id',
30
			'subject_guid',
31
			'object_guid',
32
			'target_guid',
33
			'annotation_id',
34
			'action_type',
35
			'type',
36
			'subtype',
37
			'view',
38
		];
39
40 293
		$options = LegacyQueryOptionsAdapter::normalizePluralOptions($options, $singulars);
41
42
		$defaults = [
43 293
			'ids' => null,
44
			'subject_guids' => null,
45
			'object_guids' => null,
46
			'target_guids' => null,
47
			'annotation_ids' => null,
48
			'views' => null,
49
			'action_types' => null,
50
			'posted_time_lower' => null,
51
			'posted_time_upper' => null,
52
			'limit' => 20,
53
			'offset' => 0,
54
		];
55
56 293
		$options = array_merge($defaults, $options);
57 293
		parent::__construct($options);
58 293
	}
59
60
	/**
61
	 * Build and execute a new query from an array of legacy options
62
	 *
63
	 * @param array $options Options
64
	 *
65
	 * @return ElggRiverItem[]|int|mixed
66
	 */
67 293
	public static function find(array $options = []) {
68 293
		return parent::find($options);
69
	}
70
71
	/**
72
	 * {@inheritdoc}
73
	 */
74 283
	public function count() {
75 283
		$qb = Select::fromTable('river', 'rv');
76
77 283
		$count_expr = $this->options->distinct ? "DISTINCT rv.id" : "*";
78 283
		$qb->select("COUNT({$count_expr}) AS total");
79
80 283
		$qb = $this->buildQuery($qb);
81
82 282
		$result = _elgg_services()->db->getDataRow($qb);
83
84 282
		if (empty($result)) {
85 2
			return 0;
86
		}
87
88 280
		return (int) $result->total;
89
	}
90
91
	/**
92
	 * Performs a mathematical calculation on river annotations
93
	 *
94
	 * @param string $function      Valid numeric function
95
	 * @param string $property      Property name
96
	 * @param string $property_type 'annotation'
97
	 *
98
	 * @return int|float
99
	 * @throws InvalidParameterException
100
	 */
101 1
	public function calculate($function, $property, $property_type = 'annotation') {
102
103 1
		if (!in_array(strtolower($function), QueryBuilder::$calculations)) {
104
			throw new InvalidArgumentException("'$function' is not a valid numeric function");
105
		}
106
107 1
		$qb = Select::fromTable('river', 'rv');
108
109 1
		$alias = 'n_table';
110 1
		if (!empty($this->options->annotation_name_value_pairs) && $this->options->annotation_name_value_pairs[0]->names != $property) {
111
			$alias = $qb->getNextJoinAlias();
112
113
			$annotation = new AnnotationWhereClause();
114
			$annotation->names = $property;
115
			$qb->addClause($annotation, $alias);
116
		}
117
118 1
		$qb->join('rv', 'annotations', $alias, "rv.annotation_id = $alias.id");
119 1
		$qb->select("{$function}(n_table.value) AS calculation");
120
121 1
		$qb = $this->buildQuery($qb);
122
123 1
		$result = _elgg_services()->db->getDataRow($qb);
124
125 1
		if (empty($result)) {
126
			return 0;
127
		}
128
129 1
		return (int) $result->calculation;
130
	}
131
132
	/**
133
	 * Fetch river items
134
	 *
135
	 * @param int      $limit    Limit
136
	 * @param int      $offset   Offset
137
	 * @param callable $callback Custom callback
138
	 *
139
	 * @return ElggEntity[]
140
	 * @throws \DatabaseException
141
	 */
142 19
	public function get($limit = null, $offset = null, $callback = null) {
143
144 19
		$qb = Select::fromTable('river', 'rv');
145
146 19
		$distinct = $this->options->distinct ? "DISTINCT" : "";
147 19
		$qb->select("$distinct rv.*");
148
149 19
		$this->expandInto($qb, 'rv');
150
151 19
		$qb = $this->buildQuery($qb);
152
153
		// Keeping things backwards compatible
154 18
		$original_order = elgg_extract('order_by', $this->options->__original_options);
155 18
		if (empty($original_order) && $original_order !== false) {
1 ignored issue
show
The condition $original_order !== false is always false.
Loading history...
156 11
			$qb->addOrderBy('rv.posted', 'desc');
157
		}
158
159 18
		if ($limit > 0) {
160 18
			$qb->setMaxResults((int) $limit);
161 18
			$qb->setFirstResult((int) $offset);
162
		}
163
164 18
		$callback = $callback ? : $this->options->callback;
165 18
		if (!isset($callback)) {
166 11
			$callback = function ($row) {
167 11
				return new ElggRiverItem($row);
168 11
			};
169
		}
170
171 18
		$items = _elgg_services()->db->getData($qb, $callback);
172
173 18
		if (!empty($items)) {
174 18
			$preload = array_filter($items, function($e) {
175 18
				return $e instanceof ElggRiverItem;
176 18
			});
177
178 18
			_elgg_services()->entityPreloader->preload($preload, [
179 18
				'subject_guid',
180
				'object_guid',
181
				'target_guid',
182
			]);
183
		}
184
185 18
		return $items;
186
	}
187
188
	/**
189
	 * Execute the query resolving calculation, count and/or batch options
190
	 *
191
	 * @return array|\ElggData[]|ElggEntity[]|false|int
192
	 * @throws \LogicException
193
	 */
194 293
	public function execute() {
195
196 293
		if ($this->options->annotation_calculation) {
197 2
			$clauses = $this->options->annotation_name_value_pairs;
198 2
			if (count($clauses) > 1 && $this->options->annotation_name_value_pairs_operator !== 'OR') {
199 1
				throw new \LogicException("Annotation calculation can not be performed on multiple annotation name value pairs merged with AND");
200
			}
201
202 1
			$clause = array_shift($clauses);
203
204 1
			return $this->calculate($this->options->annotation_calculation, $clause->names, 'annotation');
205 291
		} else if ($this->options->count) {
206 283
			return $this->count();
207 288
		} else if ($this->options->batch) {
208 281
			return $this->batch($this->options->limit, $this->options->offset, $this->options->callback);
209
		} else {
210 19
			return $this->get($this->options->limit, $this->options->offset, $this->options->callback);
211
		}
212
	}
213
214
	/**
215
	 * Build a database query
216
	 *
217
	 * @param QueryBuilder $qb
218
	 *
219
	 * @return QueryBuilder
220
	 */
221 292
	protected function buildQuery(QueryBuilder $qb) {
222
223 292
		$ands = [];
224
225 292
		foreach ($this->options->joins as $join) {
226 1
			$join->prepare($qb, 'rv');
227
		}
228
229 292
		foreach ($this->options->wheres as $where) {
230 1
			$ands[] = $where->prepare($qb, 'rv');
231
		}
232
233 292
		$ands[] = $this->buildRiverClause($qb);
234 290
		$ands[] = $this->buildEntityClauses($qb);
235 290
		$ands[] = $this->buildPairedAnnotationClause($qb, $this->options->annotation_name_value_pairs, $this->options->annotation_name_value_pairs_operator);
236 290
		$ands[] = $this->buildPairedRelationshipClause($qb, $this->options->relationship_pairs);
237
238 290
		$ands = $qb->merge($ands);
239
240 290
		if (!empty($ands)) {
241 290
			$qb->andWhere($ands);
242
		}
243
244 290
		return $qb;
245
	}
246
247
	/**
248
	 * Process river properties
249
	 *
250
	 * @param QueryBuilder $qb Query builder
251
	 *
252
	 * @return CompositeExpression|mixed|null|string
253
	 */
254 292
	protected function buildRiverClause(QueryBuilder $qb) {
255 292
		$where = new RiverWhereClause();
256 292
		$where->ids = $this->options->ids;
257 292
		$where->views = $this->options->views;
258 292
		$where->action_types = $this->options->action_types;
259 292
		$where->subject_guids = $this->options->subject_guids;
260 292
		$where->object_guids = $this->options->object_guids;
261 292
		$where->target_guids = $this->options->target_guids;
262 292
		$where->created_after = $this->options->created_after;
263 292
		$where->created_before = $this->options->created_before;
264
265 292
		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 290
	public function buildEntityClauses($qb) {
277
278 290
		$use_access_clause = !_elgg_services()->userCapabilities->canBypassPermissionsCheck();
279
280 290
		$ands = [];
281
282 290
		if (!empty($this->options->subject_guids) || $use_access_clause) {
283 287
			$qb->joinEntitiesTable('rv', 'subject_guid', 'inner', 'se');
284 287
			$subject = new EntityWhereClause();
285 287
			$subject->guids = $this->options->subject_guids;
286 287
			$ands[] = $subject->prepare($qb, 'se');
287
		}
288
289 290
		if (!empty($this->options->object_guids) || $use_access_clause || !empty($this->options->type_subtype_pairs)) {
290 287
			$qb->joinEntitiesTable('rv', 'object_guid', 'inner', 'oe');
291 287
			$object = new EntityWhereClause();
292 287
			$object->guids = $this->options->object_guids;
293 287
			$object->type_subtype_pairs = $this->options->type_subtype_pairs;
294 287
			$ands[] = $object->prepare($qb, 'oe');
295
		}
296
297 290
		if (!empty($this->options->target_guids) || $use_access_clause) {
298 287
			$target_ors = [];
299 287
			$qb->joinEntitiesTable('rv', 'target_guid', 'left', 'te');
300 287
			$target = new EntityWhereClause();
301 287
			$target->guids = $this->options->target_guids;
302 287
			$target_ors[] = $target->prepare($qb, 'te');
303
			// Note the LEFT JOIN
304 287
			$target_ors[] = $qb->compare('te.guid', 'IS NULL');
305 287
			$ands[] = $qb->merge($target_ors, 'OR');
306
		}
307
308 290
		return $qb->merge($ands);
309
	}
310
311
	/**
312
	 * Process annotation name value pairs
313
	 * Joins the annotation table on entity guid in the entities table and applies annotation where clauses
314
	 *
315
	 * @param QueryBuilder            $qb      Query builder
316
	 * @param AnnotationWhereClause[] $clauses Where clauses
317
	 * @param string                  $boolean Merge boolean
318
	 *
319
	 * @return CompositeExpression|string
320
	 */
321 290
	protected function buildPairedAnnotationClause(QueryBuilder $qb, $clauses, $boolean = 'AND') {
322 290
		$parts = [];
323
324 290
		foreach ($clauses as $clause) {
325 59
			if (strtoupper($boolean) === 'OR' || count($clauses) === 1) {
326 58
				$joined_alias = 'n_table';
327
			} else {
328 1
				$joined_alias = $qb->getNextJoinAlias();
329
			}
330 59
			$joins = $qb->getQueryPart('join');
331 59
			$is_joined = false;
332 59
			if (!empty($joins['rv'])) {
333 3
				foreach ($joins['rv'] as $join) {
334 3
					if ($join['joinAlias'] === $joined_alias) {
335 3
						$is_joined = true;
336
					}
337
				}
338
			}
339
340 59
			if (!$is_joined) {
341 58
				$qb->join('rv', 'annotations', $joined_alias, "$joined_alias.id = rv.annotation_id");
342
			}
343
344 59
			$parts[] = $clause->prepare($qb, $joined_alias);
345
		}
346
347 290
		return $qb->merge($parts, $boolean);
348
	}
349
350
	/**
351
	 * Process relationship pairs
352
	 *
353
	 * @param QueryBuilder              $qb      Query builder
354
	 * @param RelationshipWhereClause[] $clauses Where clauses
355
	 * @param string                    $boolean Merge boolean
356
	 *
357
	 * @return CompositeExpression|string
358
	 */
359 290
	protected function buildPairedRelationshipClause(QueryBuilder $qb, $clauses, $boolean = 'AND') {
360 290
		$parts = [];
361
362 290
		foreach ($clauses as $clause) {
363 2
			$join_on = $clause->join_on === 'guid' ? 'subject_guid' : $clause->join_on;
364 2
			if (strtoupper($boolean) == 'OR' || count($clauses) === 1) {
365 1
				$joined_alias = $qb->joinRelationshipTable('rv', $join_on, null, $clause->inverse, 'inner', 'r');
366
			} else {
367 1
				$joined_alias = $qb->joinRelationshipTable('rv', $join_on, $clause->names, $clause->inverse);
368
			}
369 2
			$parts[] = $clause->prepare($qb, $joined_alias);
370
		}
371
372 290
		return $qb->merge($parts, $boolean);
373
	}
374
}
375