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
|
|||
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; |
||
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
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
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
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
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); |
|
367 | } |
||
368 | 2 | $parts[] = $clause->prepare($qb, $joined_alias); |
|
369 | } |
||
370 | |||
371 | 225 | return $qb->merge($parts, $boolean); |
|
372 | } |
||
373 | } |
||
374 |
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.