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 ElggEntity; |
||||
13 | use InvalidArgumentException; |
||||
14 | use InvalidParameterException; |
||||
15 | |||||
16 | /** |
||||
17 | * Entities repository contains methods for fetching entities from database or performing |
||||
18 | * calculations on entity properties. |
||||
19 | * |
||||
20 | * API IN FLUX Do not access the methods directly, use elgg_get_entities() instead |
||||
21 | * |
||||
22 | * @todo At a later stage, this class will contain additional shortcut methods to filter entities |
||||
23 | * by relationship, metdataion, annotation, private settings etc. Until then, such filtering |
||||
24 | * can be done via standard ege* options |
||||
25 | * |
||||
26 | * @todo Resolve table alias collissions when querying for both annotationa and metadata name value pairs |
||||
27 | * Currently, n_table is expected across core and plugins, we need to refactor that code |
||||
28 | * and remove n_table joins here |
||||
29 | * |
||||
30 | * @access private |
||||
31 | */ |
||||
32 | class Entities extends Repository { |
||||
33 | |||||
34 | /** |
||||
35 | * {@inheritdoc} |
||||
36 | */ |
||||
37 | 248 | public function count() { |
|||
38 | 248 | $qb = Select::fromTable('entities', 'e'); |
|||
39 | |||||
40 | 248 | $count_expr = $this->options->distinct ? "DISTINCT e.guid" : "*"; |
|||
41 | 248 | $qb->select("COUNT({$count_expr}) AS total"); |
|||
42 | |||||
43 | 248 | $qb = $this->buildQuery($qb); |
|||
44 | |||||
45 | 247 | $result = _elgg_services()->db->getDataRow($qb); |
|||
46 | |||||
47 | 247 | if (!$result) { |
|||
0 ignored issues
–
show
|
|||||
48 | 2 | return 0; |
|||
49 | } |
||||
50 | |||||
51 | 245 | return (int) $result->total; |
|||
52 | } |
||||
53 | |||||
54 | /** |
||||
55 | * Performs a mathematical calculation on a set of entity properties |
||||
56 | * |
||||
57 | * @param string $function Valid numeric function |
||||
58 | * @param string $property Property name |
||||
59 | * @param string $property_type 'attribute'|'metadata'|'annotation'|'private_setting' |
||||
60 | * |
||||
61 | * @return int|float |
||||
62 | * @throws InvalidParameterException |
||||
63 | */ |
||||
64 | 8 | public function calculate($function, $property, $property_type = null) { |
|||
65 | |||||
66 | 8 | if (!in_array(strtolower($function), QueryBuilder::$calculations)) { |
|||
67 | 1 | throw new InvalidArgumentException("'$function' is not a valid numeric function"); |
|||
68 | } |
||||
69 | |||||
70 | 7 | if (!isset($property_type)) { |
|||
71 | 2 | if (in_array($property, ElggEntity::$primary_attr_names)) { |
|||
72 | 1 | $property_type = 'attribute'; |
|||
73 | } else { |
||||
74 | 1 | $property_type = 'metadata'; |
|||
75 | } |
||||
76 | } |
||||
77 | |||||
78 | 7 | $qb = Select::fromTable('entities', 'e'); |
|||
79 | |||||
80 | 7 | switch ($property_type) { |
|||
81 | case 'attribute': |
||||
82 | 3 | if (!in_array($property, ElggEntity::$primary_attr_names)) { |
|||
83 | 1 | throw new InvalidParameterException("'$property' is not a valid attribute"); |
|||
84 | } |
||||
85 | |||||
86 | 2 | $qb->addSelect("{$function}(e.{$property}) AS calculation"); |
|||
87 | 2 | break; |
|||
88 | |||||
89 | case 'metadata' : |
||||
90 | 2 | $alias = $qb->joinMetadataTable('e', 'guid', $property, 'inner', 'n_table'); |
|||
91 | 2 | $qb->addSelect("{$function}({$alias}.value) AS calculation"); |
|||
92 | 2 | break; |
|||
93 | |||||
94 | case 'annotation' : |
||||
95 | 1 | $alias = $qb->joinAnnotationTable('e', 'guid', $property, 'inner', 'n_table'); |
|||
96 | 1 | $qb->addSelect("{$function}({$alias}.value) AS calculation"); |
|||
97 | 1 | break; |
|||
98 | |||||
99 | case 'private_setting' : |
||||
100 | 1 | $alias = $qb->joinPrivateSettingsTable('e', 'guid', $property, 'inner', 'ps'); |
|||
101 | 1 | $qb->addSelect("{$function}({$alias}.value) AS calculation"); |
|||
102 | 1 | break; |
|||
103 | } |
||||
104 | |||||
105 | 6 | $qb = $this->buildQuery($qb); |
|||
106 | |||||
107 | 6 | $result = _elgg_services()->db->getDataRow($qb); |
|||
108 | |||||
109 | 6 | return (int) $result->calculation; |
|||
110 | } |
||||
111 | |||||
112 | /** |
||||
113 | * Fetch entities |
||||
114 | * |
||||
115 | * @param int $limit Limit |
||||
116 | * @param int $offset Offset |
||||
117 | * @param callable $callback Custom callback |
||||
118 | * |
||||
119 | * @return ElggEntity[] |
||||
120 | */ |
||||
121 | 1172 | public function get($limit = null, $offset = null, $callback = null) { |
|||
122 | |||||
123 | 1172 | $qb = Select::fromTable('entities', 'e'); |
|||
124 | |||||
125 | 1172 | $distinct = $this->options->distinct ? "DISTINCT" : ""; |
|||
126 | 1172 | $qb->select("$distinct e.*"); |
|||
127 | |||||
128 | 1172 | $this->expandInto($qb, 'e'); |
|||
129 | |||||
130 | 1172 | $qb = $this->buildQuery($qb); |
|||
131 | |||||
132 | // Keeping things backwards compatible |
||||
133 | 1171 | $original_order = elgg_extract('order_by', $this->options->__original_options); |
|||
134 | 1171 | if (empty($original_order) && $original_order !== false) { |
|||
135 | 912 | $qb->addOrderBy('e.time_created', 'desc'); |
|||
136 | } |
||||
137 | |||||
138 | 1171 | if ($limit) { |
|||
139 | 1170 | $qb->setMaxResults((int) $limit); |
|||
140 | 1170 | $qb->setFirstResult((int) $offset); |
|||
141 | } |
||||
142 | |||||
143 | 1171 | $options = $this->options->getArrayCopy(); |
|||
144 | |||||
145 | 1171 | $options['limit'] = (int) $limit; |
|||
146 | 1171 | $options['offset'] = (int) $offset; |
|||
147 | 1171 | $options['callback'] = $callback ? : $this->options->callback; |
|||
148 | 1171 | if (!isset($options['callback'])) { |
|||
149 | 1150 | $options['callback'] = 'entity_row_to_elggstar'; |
|||
150 | } |
||||
151 | |||||
152 | 1171 | unset($options['count']); |
|||
153 | |||||
154 | 1171 | return _elgg_services()->entityTable->fetch($qb, $options); |
|||
155 | } |
||||
156 | |||||
157 | /** |
||||
158 | * Execute the query resolving calculation, count and/or batch options |
||||
159 | * |
||||
160 | * @return array|\ElggData[]|ElggEntity[]|false|int |
||||
161 | * @throws \LogicException |
||||
162 | */ |
||||
163 | 1178 | public function execute() { |
|||
164 | |||||
165 | 1178 | if ($this->options->annotation_calculation) { |
|||
166 | 2 | $clauses = $this->options->annotation_name_value_pairs; |
|||
167 | 2 | if (count($clauses) > 1 && $this->options->annotation_name_value_pairs_operator !== 'OR') { |
|||
168 | 1 | throw new \LogicException("Annotation calculation can not be performed on multiple annotation name value pairs merged with AND"); |
|||
169 | } |
||||
170 | |||||
171 | 1 | $clause = array_shift($clauses); |
|||
172 | |||||
173 | 1 | return $this->calculate($this->options->annotation_calculation, $clause->names, 'annotation'); |
|||
174 | 1176 | } else if ($this->options->metadata_calculation) { |
|||
175 | 2 | $clauses = $this->options->metadata_name_value_pairs; |
|||
176 | 2 | if (count($clauses) > 1 && $this->options->metadata_name_value_pairs_operator !== 'OR') { |
|||
177 | 1 | throw new \LogicException("Metadata calculation can not be performed on multiple metadata name value pairs merged with AND"); |
|||
178 | } |
||||
179 | |||||
180 | 1 | $clause = array_shift($clauses); |
|||
181 | |||||
182 | 1 | return $this->calculate($this->options->metadata_calculation, $clause->names, 'metadata'); |
|||
183 | 1174 | } else if ($this->options->count) { |
|||
184 | 248 | return $this->count(); |
|||
185 | 1172 | } else if ($this->options->batch) { |
|||
186 | 66 | return $this->batch($this->options->limit, $this->options->offset, $this->options->callback); |
|||
187 | } else { |
||||
188 | 1172 | return $this->get($this->options->limit, $this->options->offset, $this->options->callback); |
|||
189 | } |
||||
190 | } |
||||
191 | |||||
192 | /** |
||||
193 | * Build a database query |
||||
194 | * |
||||
195 | * @param QueryBuilder $qb |
||||
196 | * |
||||
197 | * @return QueryBuilder |
||||
198 | */ |
||||
199 | 1180 | protected function buildQuery(QueryBuilder $qb) { |
|||
200 | |||||
201 | 1180 | $ands = []; |
|||
202 | |||||
203 | 1180 | foreach ($this->options->joins as $join) { |
|||
204 | 2 | $join->prepare($qb, 'e'); |
|||
205 | } |
||||
206 | |||||
207 | 1180 | foreach ($this->options->wheres as $where) { |
|||
208 | 476 | $ands[] = $where->prepare($qb, 'e'); |
|||
209 | } |
||||
210 | |||||
211 | 1180 | $ands[] = $this->buildEntityClause($qb); |
|||
212 | 1178 | $ands[] = $this->buildPairedMetadataClause($qb, $this->options->metadata_name_value_pairs, $this->options->metadata_name_value_pairs_operator); |
|||
213 | 1178 | $ands[] = $this->buildPairedMetadataClause($qb, $this->options->search_name_value_pairs, 'OR'); |
|||
214 | 1178 | $ands[] = $this->buildPairedAnnotationClause($qb, $this->options->annotation_name_value_pairs, $this->options->annotation_name_value_pairs_operator); |
|||
215 | 1178 | $ands[] = $this->buildPairedPrivateSettingsClause($qb, $this->options->private_setting_name_value_pairs, $this->options->private_setting_name_value_pairs_operator); |
|||
216 | 1178 | $ands[] = $this->buildPairedRelationshipClause($qb, $this->options->relationship_pairs); |
|||
217 | |||||
218 | 1178 | $ands = $qb->merge($ands); |
|||
219 | |||||
220 | 1178 | if (!empty($ands)) { |
|||
221 | 1178 | $qb->andWhere($ands); |
|||
222 | } |
||||
223 | |||||
224 | 1178 | return $qb; |
|||
225 | } |
||||
226 | |||||
227 | /** |
||||
228 | * Process entity attribute wheres |
||||
229 | * Applies entity attribute constrains on the selected entities table |
||||
230 | * |
||||
231 | * @param QueryBuilder $qb Query builder |
||||
232 | * |
||||
233 | * @return Closure|CompositeExpression|mixed|null|string |
||||
234 | */ |
||||
235 | 1180 | protected function buildEntityClause(QueryBuilder $qb) { |
|||
236 | 1180 | return EntityWhereClause::factory($this->options)->prepare($qb, 'e'); |
|||
237 | } |
||||
238 | |||||
239 | /** |
||||
240 | * Process metadata name value pairs |
||||
241 | * Joins the metadata table on entity guid in the entities table and applies metadata where clauses |
||||
242 | * |
||||
243 | * @param QueryBuilder $qb Query builder |
||||
244 | * @param MetadataWhereClause[] $clauses Where clauses |
||||
245 | * @param string $boolean Merge boolean |
||||
246 | * |
||||
247 | * @return CompositeExpression|string |
||||
248 | */ |
||||
249 | 1178 | protected function buildPairedMetadataClause(QueryBuilder $qb, $clauses, $boolean = 'AND') { |
|||
250 | 1178 | $parts = []; |
|||
251 | |||||
252 | |||||
253 | 1178 | foreach ($clauses as $clause) { |
|||
254 | 1146 | if ($clause instanceof MetadataWhereClause) { |
|||
255 | 1146 | if (strtoupper($boolean) === 'OR' || count($clauses) === 1) { |
|||
256 | 1145 | $joined_alias = $qb->joinMetadataTable('e', 'guid', null, 'inner', 'n_table'); |
|||
257 | } else { |
||||
258 | 12 | $joined_alias = $qb->joinMetadataTable('e', 'guid', $clause->names); |
|||
259 | } |
||||
260 | 1146 | $parts[] = $clause->prepare($qb, $joined_alias); |
|||
261 | } |
||||
262 | } |
||||
263 | |||||
264 | 1178 | return $qb->merge($parts, $boolean); |
|||
265 | } |
||||
266 | |||||
267 | /** |
||||
268 | * Process annotation name value pairs |
||||
269 | * Joins the annotation table on entity guid in the entities table and applies annotation where clauses |
||||
270 | * |
||||
271 | * @param QueryBuilder $qb Query builder |
||||
272 | * @param AnnotationWhereClause[] $clauses Where clauses |
||||
273 | * @param string $boolean Merge boolean |
||||
274 | * |
||||
275 | * @return CompositeExpression|string |
||||
276 | */ |
||||
277 | 1178 | protected function buildPairedAnnotationClause(QueryBuilder $qb, $clauses, $boolean = 'AND') { |
|||
278 | 1178 | $parts = []; |
|||
279 | |||||
280 | 1178 | foreach ($clauses as $clause) { |
|||
281 | 14 | if (strtoupper($boolean) === 'OR' || count($clauses) === 1) { |
|||
282 | 13 | $joined_alias = $qb->joinAnnotationTable('e', 'guid', null, 'inner', 'n_table'); |
|||
283 | } else { |
||||
284 | 1 | $joined_alias = $qb->joinAnnotationTable('e', 'guid', $clause->names); |
|||
285 | } |
||||
286 | 14 | $parts[] = $clause->prepare($qb, $joined_alias); |
|||
287 | } |
||||
288 | |||||
289 | 1178 | return $qb->merge($parts, $boolean); |
|||
290 | } |
||||
291 | |||||
292 | /** |
||||
293 | * Process private setting name value pairs |
||||
294 | * Joins the private settings table on entity guid in the entities table and applies private setting where clauses |
||||
295 | * |
||||
296 | * @param QueryBuilder $qb Query builder |
||||
297 | * @param PrivateSettingWhereClause[] $clauses Where clauses |
||||
298 | * @param string $boolean Merge boolean |
||||
299 | * |
||||
300 | * @return CompositeExpression|string |
||||
301 | */ |
||||
302 | 1178 | protected function buildPairedPrivateSettingsClause(QueryBuilder $qb, $clauses, $boolean = 'AND') { |
|||
303 | 1178 | $parts = []; |
|||
304 | |||||
305 | 1178 | foreach ($clauses as $clause) { |
|||
306 | 78 | if (strtoupper($boolean) === 'OR' || count($clauses) === 1) { |
|||
307 | 77 | $joined_alias = $qb->joinPrivateSettingsTable('e', 'guid', null, 'inner', 'ps'); |
|||
308 | } else { |
||||
309 | 10 | $joined_alias = $qb->joinPrivateSettingsTable('e', 'guid', $clause->names); |
|||
310 | } |
||||
311 | 78 | $parts[] = $clause->prepare($qb, $joined_alias); |
|||
312 | } |
||||
313 | |||||
314 | 1178 | return $qb->merge($parts, $boolean); |
|||
315 | } |
||||
316 | |||||
317 | /** |
||||
318 | * Process relationship pairs |
||||
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 | 1178 | protected function buildPairedRelationshipClause(QueryBuilder $qb, $clauses, $boolean = 'AND') { |
|||
327 | 1178 | $parts = []; |
|||
328 | |||||
329 | 1178 | foreach ($clauses as $clause) { |
|||
330 | 26 | if (strtoupper($boolean) == 'OR' || count($clauses) === 1) { |
|||
331 | 25 | $joined_alias = $qb->joinRelationshipTable('e', $clause->join_on, null, $clause->inverse, 'inner', 'r'); |
|||
332 | } else { |
||||
333 | 1 | $joined_alias = $qb->joinRelationshipTable('e', $clause->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
Loading history...
|
|||||
334 | } |
||||
335 | 26 | $parts[] = $clause->prepare($qb, $joined_alias); |
|||
336 | } |
||||
337 | |||||
338 | 1178 | 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.