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
|
|||
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
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...
|
|||
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
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 For 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
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
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 |
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.