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 ElggData; |
||||
13 | use ElggEntity; |
||||
14 | use ElggMetadata; |
||||
15 | use InvalidArgumentException; |
||||
16 | use InvalidParameterException; |
||||
17 | use LogicException; |
||||
18 | |||||
19 | /** |
||||
20 | * Metadata repository contains methods for fetching metadata from database or performing |
||||
21 | * calculations on entity properties. |
||||
22 | * |
||||
23 | * API IN FLUX Do not access the methods directly, use elgg_get_metadata() instead |
||||
24 | * |
||||
25 | * @access private |
||||
26 | */ |
||||
27 | class Metadata extends Repository { |
||||
28 | |||||
29 | /** |
||||
30 | * {@inheritdoc} |
||||
31 | */ |
||||
32 | 273 | public function count() { |
|||
33 | 273 | $qb = Select::fromTable('metadata', 'n_table'); |
|||
34 | |||||
35 | 273 | $count_expr = $this->options->distinct ? "DISTINCT n_table.id" : "*"; |
|||
36 | 273 | $qb->select("COUNT({$count_expr}) AS total"); |
|||
37 | |||||
38 | 273 | $qb = $this->buildQuery($qb); |
|||
39 | |||||
40 | 272 | $result = _elgg_services()->db->getDataRow($qb); |
|||
41 | |||||
42 | 272 | if (!$result) { |
|||
0 ignored issues
–
show
|
|||||
43 | 44 | return 0; |
|||
44 | } |
||||
45 | |||||
46 | 228 | return (int) $result->total; |
|||
47 | } |
||||
48 | |||||
49 | /** |
||||
50 | * Performs a mathematical calculation on metadata or metadata entity's properties |
||||
51 | * |
||||
52 | * @param string $function Valid numeric function |
||||
53 | * @param string $property Property name |
||||
54 | * @param string $property_type 'attribute'|'metadata'|'annotation'|'private_setting' |
||||
55 | * |
||||
56 | * @return int|float |
||||
57 | * @throws InvalidParameterException |
||||
58 | */ |
||||
59 | 8 | public function calculate($function, $property, $property_type = null) { |
|||
60 | |||||
61 | 8 | if (!in_array(strtolower($function), QueryBuilder::$calculations)) { |
|||
62 | 1 | throw new InvalidArgumentException("'$function' is not a valid numeric function"); |
|||
63 | } |
||||
64 | |||||
65 | 7 | if (!isset($property_type)) { |
|||
66 | 1 | $property_type = 'metadata'; |
|||
67 | } |
||||
68 | |||||
69 | 7 | $qb = Select::fromTable('metadata', 'n_table'); |
|||
70 | |||||
71 | 7 | switch ($property_type) { |
|||
72 | case 'attribute': |
||||
73 | 2 | if (!in_array($property, ElggEntity::$primary_attr_names)) { |
|||
74 | 1 | throw new InvalidParameterException("'$property' is not a valid attribute"); |
|||
75 | } |
||||
76 | |||||
77 | /** |
||||
78 | * @todo When no entity constraints are present, do we need to ensure that entity access clause is added? |
||||
79 | */ |
||||
80 | 1 | $alias = $qb->joinEntitiesTable('n_table', 'entity_guid', 'inner', 'e'); |
|||
81 | 1 | $qb->addSelect("{$function}({$alias}.{$property}) AS calculation"); |
|||
82 | 1 | break; |
|||
83 | |||||
84 | case 'metadata' : |
||||
85 | 3 | $alias = 'n_table'; |
|||
86 | 3 | if (!empty($this->options->metadata_name_value_pairs) && $this->options->metadata_name_value_pairs[0]->names != $property) { |
|||
87 | 1 | $alias = $qb->joinMetadataTable('n_table', 'entity_guid', $property); |
|||
88 | } |
||||
89 | 3 | $qb->addSelect("{$function}($alias.value) AS calculation"); |
|||
90 | 3 | break; |
|||
91 | |||||
92 | case 'annotation' : |
||||
93 | 1 | $alias = $qb->joinAnnotationTable('n_table', 'entity_guid', $property); |
|||
94 | 1 | $qb->addSelect("{$function}({$alias}.value) AS calculation"); |
|||
95 | 1 | break; |
|||
96 | |||||
97 | case 'private_setting' : |
||||
98 | 1 | $alias = $qb->joinPrivateSettingsTable('n_table', 'entity_guid', $property); |
|||
99 | 1 | $qb->addSelect("{$function}({$alias}.value) AS calculation"); |
|||
100 | 1 | break; |
|||
101 | } |
||||
102 | |||||
103 | 6 | $qb = $this->buildQuery($qb); |
|||
104 | |||||
105 | 6 | $result = _elgg_services()->db->getDataRow($qb); |
|||
106 | |||||
107 | 6 | 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...
|
|||||
108 | return 0; |
||||
109 | } |
||||
110 | |||||
111 | 6 | return (int) $result->calculation; |
|||
112 | } |
||||
113 | |||||
114 | /** |
||||
115 | * Fetch metadata |
||||
116 | * |
||||
117 | * @param int $limit Limit |
||||
118 | * @param int $offset Offset |
||||
119 | * @param callable $callback Custom callback |
||||
120 | * |
||||
121 | * @return ElggMetadata[] |
||||
122 | */ |
||||
123 | 602 | public function get($limit = null, $offset = null, $callback = null) { |
|||
124 | |||||
125 | 602 | $qb = Select::fromTable('metadata', 'n_table'); |
|||
126 | |||||
127 | 602 | $distinct = $this->options->distinct ? "DISTINCT" : ""; |
|||
128 | 602 | $qb->select("$distinct n_table.*"); |
|||
129 | |||||
130 | 602 | $this->expandInto($qb, 'n_table'); |
|||
131 | |||||
132 | 602 | $qb = $this->buildQuery($qb); |
|||
133 | |||||
134 | // Keeping things backwards compatible |
||||
135 | 601 | $original_order = elgg_extract('order_by', $this->options->__original_options); |
|||
136 | 601 | if (empty($original_order) && $original_order !== false) { |
|||
137 | 476 | $qb->addOrderBy('n_table.time_created', 'asc'); |
|||
138 | 476 | $qb->addOrderBy('n_table.id', 'asc'); |
|||
139 | } |
||||
140 | |||||
141 | 601 | 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...
|
|||||
142 | 211 | $qb->setMaxResults((int) $limit); |
|||
143 | 211 | $qb->setFirstResult((int) $offset); |
|||
144 | } |
||||
145 | |||||
146 | 601 | $callback = $callback ? : $this->options->callback; |
|||
147 | 601 | if (!isset($callback)) { |
|||
148 | 476 | $callback = function ($row) { |
|||
149 | 200 | return new ElggMetadata($row); |
|||
150 | 476 | }; |
|||
151 | } |
||||
152 | |||||
153 | 601 | return _elgg_services()->db->getData($qb, $callback); |
|||
154 | } |
||||
155 | |||||
156 | /** |
||||
157 | * Execute the query resolving calculation, count and/or batch options |
||||
158 | * |
||||
159 | * @return array|ElggData[]|ElggMetadata[]|false|int |
||||
160 | * @throws LogicException |
||||
161 | */ |
||||
162 | 652 | public function execute() { |
|||
163 | |||||
164 | 652 | if ($this->options->annotation_calculation) { |
|||
165 | 2 | $clauses = $this->options->annotation_name_value_pairs; |
|||
166 | 2 | if (count($clauses) > 1 && $this->options->annotation_name_value_pairs_operator !== 'OR') { |
|||
167 | 1 | throw new LogicException("Annotation calculation can not be performed on multiple annotation name value pairs merged with AND"); |
|||
168 | } |
||||
169 | |||||
170 | 1 | $clause = array_shift($clauses); |
|||
171 | |||||
172 | 1 | return $this->calculate($this->options->annotation_calculation, $clause->names, 'annotation'); |
|||
173 | 650 | } else if ($this->options->metadata_calculation) { |
|||
174 | 2 | $clauses = $this->options->metadata_name_value_pairs; |
|||
175 | 2 | if (count($clauses) > 1 && $this->options->metadata_name_value_pairs_operator !== 'OR') { |
|||
176 | 1 | 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 | 648 | } else if ($this->options->count) { |
|||
183 | 273 | return $this->count(); |
|||
184 | 646 | } else if ($this->options->batch) { |
|||
185 | 272 | return $this->batch($this->options->limit, $this->options->offset, $this->options->callback); |
|||
186 | } else { |
||||
187 | 602 | 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 | 654 | protected function buildQuery(QueryBuilder $qb) { |
|||
199 | |||||
200 | 654 | $ands = []; |
|||
201 | |||||
202 | 654 | foreach ($this->options->joins as $join) { |
|||
203 | 1 | $join->prepare($qb, 'n_table'); |
|||
204 | } |
||||
205 | |||||
206 | 654 | foreach ($this->options->wheres as $where) { |
|||
207 | 2 | $ands[] = $where->prepare($qb, 'n_table'); |
|||
208 | } |
||||
209 | |||||
210 | 654 | $ands[] = $this->buildPairedMetadataClause($qb, $this->options->metadata_name_value_pairs, $this->options->metadata_name_value_pairs_operator); |
|||
211 | 654 | $ands[] = $this->buildEntityWhereClause($qb); |
|||
212 | 652 | $ands[] = $this->buildPairedMetadataClause($qb, $this->options->search_name_value_pairs, 'OR'); |
|||
213 | 652 | $ands[] = $this->buildPairedAnnotationClause($qb, $this->options->annotation_name_value_pairs, $this->options->annotation_name_value_pairs_operator); |
|||
214 | 652 | $ands[] = $this->buildPairedPrivateSettingsClause($qb, $this->options->private_setting_name_value_pairs, $this->options->private_setting_name_value_pairs_operator); |
|||
215 | 652 | $ands[] = $this->buildPairedRelationshipClause($qb, $this->options->relationship_pairs); |
|||
216 | |||||
217 | 652 | $ands = $qb->merge($ands); |
|||
218 | |||||
219 | 652 | if (!empty($ands)) { |
|||
220 | 652 | $qb->andWhere($ands); |
|||
221 | } |
||||
222 | |||||
223 | 652 | return $qb; |
|||
224 | } |
||||
225 | |||||
226 | /** |
||||
227 | * Process entity attribute wheres |
||||
228 | * Joins entities table on entity guid in metadata table and applies where clauses |
||||
229 | * |
||||
230 | * @param QueryBuilder $qb Query builder |
||||
231 | * |
||||
232 | * @return Closure|CompositeExpression|mixed|null|string |
||||
233 | */ |
||||
234 | 654 | protected function buildEntityWhereClause(QueryBuilder $qb) { |
|||
235 | |||||
236 | // Even if all of these properties are empty, we want to add this clause regardless, |
||||
237 | // to ensure that entity access clauses are appended to the query |
||||
238 | |||||
239 | 654 | $joined_alias = $qb->joinEntitiesTable('n_table', 'entity_guid', 'inner', 'e'); |
|||
240 | 654 | return EntityWhereClause::factory($this->options)->prepare($qb, $joined_alias); |
|||
241 | } |
||||
242 | |||||
243 | /** |
||||
244 | * Process metadata name value pairs |
||||
245 | * Applies where clauses to the selected metadata table |
||||
246 | * |
||||
247 | * @param QueryBuilder $qb Query builder |
||||
248 | * @param MetadataWhereClause[] $clauses Where clauses |
||||
249 | * @param string $boolean Merge boolean |
||||
250 | * |
||||
251 | * @return CompositeExpression|string |
||||
252 | */ |
||||
253 | 654 | protected function buildPairedMetadataClause(QueryBuilder $qb, $clauses, $boolean = 'AND') { |
|||
254 | 654 | $parts = []; |
|||
255 | |||||
256 | |||||
257 | 654 | foreach ($clauses as $clause) { |
|||
258 | 523 | $parts[] = $clause->prepare($qb, 'n_table'); |
|||
259 | } |
||||
260 | |||||
261 | 654 | return $qb->merge($parts, $boolean); |
|||
262 | } |
||||
263 | |||||
264 | /** |
||||
265 | * Process annotation name value pairs |
||||
266 | * Joins annotation table on entity_guid in the metadata table and applies where clauses |
||||
267 | * |
||||
268 | * @param QueryBuilder $qb Query builder |
||||
269 | * @param AnnotationWhereClause[] $clauses Where clauses |
||||
270 | * @param string $boolean Merge boolean |
||||
271 | * |
||||
272 | * @return CompositeExpression|string |
||||
273 | */ |
||||
274 | 652 | protected function buildPairedAnnotationClause(QueryBuilder $qb, $clauses, $boolean = 'AND') { |
|||
275 | 652 | $parts = []; |
|||
276 | |||||
277 | 652 | foreach ($clauses as $clause) { |
|||
278 | 3 | if (strtoupper($boolean) === 'OR' || count($clauses) > 1) { |
|||
279 | 2 | $joined_alias = $qb->joinAnnotationTable('n_table', 'entity_guid'); |
|||
280 | } else { |
||||
281 | 1 | $joined_alias = $qb->joinAnnotationTable('n_table', 'entity_guid', $clause->names); |
|||
282 | } |
||||
283 | 3 | $parts[] = $clause->prepare($qb, $joined_alias); |
|||
284 | } |
||||
285 | |||||
286 | 652 | 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 metadata 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 | 652 | protected function buildPairedPrivateSettingsClause(QueryBuilder $qb, $clauses, $boolean = 'AND') { |
|||
300 | 652 | $parts = []; |
|||
301 | |||||
302 | 652 | 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 | 652 | return $qb->merge($parts, $boolean); |
|||
312 | } |
||||
313 | |||||
314 | /** |
||||
315 | * Process relationship name value pairs |
||||
316 | * Joins relationship table on entity_guid in the metadata table and applies where clauses |
||||
317 | * |
||||
318 | * @note Note that $relationship_join_on does not apply here, as there is only one guid column in the metadata table |
||||
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 | 652 | protected function buildPairedRelationshipClause(QueryBuilder $qb, $clauses, $boolean = 'AND') { |
|||
327 | 652 | $parts = []; |
|||
328 | |||||
329 | 652 | foreach ($clauses as $clause) { |
|||
330 | 2 | if (strtoupper($boolean) == 'OR' || count($clauses) > 1) { |
|||
331 | 1 | $joined_alias = $qb->joinRelationshipTable('n_table', 'entity_guid', null, $clause->inverse); |
|||
332 | } else { |
||||
333 | 1 | $joined_alias = $qb->joinRelationshipTable('n_table', 'entity_guid', $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
Loading history...
|
|||||
334 | } |
||||
335 | 2 | $parts[] = $clause->prepare($qb, $joined_alias); |
|||
336 | } |
||||
337 | |||||
338 | 652 | return $qb->merge($parts, $boolean); |
|||
339 | } |
||||
340 | } |
||||
341 |
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.