These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Doctrine\ODM\MongoDB\Query; |
||
6 | |||
7 | use Doctrine\ODM\MongoDB\DocumentManager; |
||
8 | use Doctrine\ODM\MongoDB\Iterator\CachingIterator; |
||
9 | use Doctrine\ODM\MongoDB\Iterator\HydratingIterator; |
||
10 | use Doctrine\ODM\MongoDB\Iterator\Iterator; |
||
11 | use Doctrine\ODM\MongoDB\Iterator\PrimingIterator; |
||
12 | use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; |
||
13 | use Doctrine\ODM\MongoDB\MongoDBException; |
||
14 | use MongoDB\Collection; |
||
15 | use MongoDB\Driver\Cursor; |
||
16 | use MongoDB\Operation\FindOneAndUpdate; |
||
17 | use function array_combine; |
||
18 | use function array_filter; |
||
19 | use function array_flip; |
||
20 | use function array_intersect_key; |
||
21 | use function array_keys; |
||
22 | use function array_map; |
||
23 | use function array_merge; |
||
24 | use function array_values; |
||
25 | use function is_array; |
||
26 | use function is_callable; |
||
27 | |||
28 | /** |
||
29 | * ODM Query wraps the raw Doctrine MongoDB queries to add additional functionality |
||
30 | * and to hydrate the raw arrays of data to Doctrine document objects. |
||
31 | * |
||
32 | */ |
||
33 | class Query implements \IteratorAggregate |
||
34 | { |
||
35 | public const TYPE_FIND = 1; |
||
36 | public const TYPE_FIND_AND_UPDATE = 2; |
||
37 | public const TYPE_FIND_AND_REMOVE = 3; |
||
38 | public const TYPE_INSERT = 4; |
||
39 | public const TYPE_UPDATE = 5; |
||
40 | public const TYPE_REMOVE = 6; |
||
41 | public const TYPE_GROUP = 7; |
||
42 | public const TYPE_MAP_REDUCE = 8; |
||
43 | public const TYPE_DISTINCT = 9; |
||
44 | public const TYPE_COUNT = 11; |
||
45 | |||
46 | /** |
||
47 | * @deprecated 1.1 Will be removed for 2.0 |
||
48 | */ |
||
49 | public const TYPE_GEO_LOCATION = 10; |
||
50 | |||
51 | public const HINT_REFRESH = 1; |
||
52 | // 2 was used for HINT_SLAVE_OKAY, which was removed in 2.0 |
||
53 | public const HINT_READ_PREFERENCE = 3; |
||
54 | public const HINT_READ_ONLY = 5; |
||
55 | |||
56 | /** |
||
57 | * The DocumentManager instance. |
||
58 | * |
||
59 | * @var DocumentManager |
||
60 | */ |
||
61 | private $dm; |
||
62 | |||
63 | /** |
||
64 | * The ClassMetadata instance. |
||
65 | * |
||
66 | * @var ClassMetadata |
||
67 | */ |
||
68 | private $class; |
||
69 | |||
70 | /** |
||
71 | * Whether to hydrate results as document class instances. |
||
72 | * |
||
73 | * @var bool |
||
74 | */ |
||
75 | private $hydrate = true; |
||
76 | |||
77 | /** |
||
78 | * Array of primer Closure instances. |
||
79 | * |
||
80 | * @var array |
||
81 | */ |
||
82 | private $primers = []; |
||
83 | |||
84 | /** |
||
85 | * Hints for UnitOfWork behavior. |
||
86 | * |
||
87 | * @var array |
||
88 | */ |
||
89 | private $unitOfWorkHints = []; |
||
90 | |||
91 | /** |
||
92 | * The Collection instance. |
||
93 | * |
||
94 | * @var Collection |
||
95 | */ |
||
96 | protected $collection; |
||
97 | |||
98 | /** |
||
99 | * Query structure generated by the Builder class. |
||
100 | * |
||
101 | * @var array |
||
102 | */ |
||
103 | private $query; |
||
104 | |||
105 | /** @var Iterator */ |
||
106 | private $iterator; |
||
107 | |||
108 | /** |
||
109 | * Query options |
||
110 | * |
||
111 | * @var array |
||
112 | */ |
||
113 | private $options; |
||
114 | |||
115 | /** |
||
116 | * |
||
117 | * |
||
118 | * Please note that $requireIndexes was deprecated in 1.2 and will be removed in 2.0 |
||
119 | */ |
||
120 | 163 | public function __construct(DocumentManager $dm, ClassMetadata $class, Collection $collection, array $query = [], array $options = [], bool $hydrate = true, bool $refresh = false, array $primers = [], bool $readOnly = false) |
|
121 | { |
||
122 | 163 | $primers = array_filter($primers); |
|
123 | |||
124 | 163 | if (! empty($primers)) { |
|
125 | 22 | $query['eagerCursor'] = true; |
|
126 | } |
||
127 | |||
128 | 163 | if (! empty($query['eagerCursor'])) { |
|
129 | 23 | $query['useIdentifierKeys'] = false; |
|
130 | } |
||
131 | |||
132 | 163 | switch ($query['type']) { |
|
133 | 163 | case self::TYPE_FIND: |
|
134 | 38 | case self::TYPE_FIND_AND_UPDATE: |
|
135 | 26 | case self::TYPE_FIND_AND_REMOVE: |
|
136 | 23 | case self::TYPE_INSERT: |
|
137 | 22 | case self::TYPE_UPDATE: |
|
138 | 8 | case self::TYPE_REMOVE: |
|
139 | 6 | case self::TYPE_GROUP: |
|
140 | 6 | case self::TYPE_MAP_REDUCE: |
|
141 | 6 | case self::TYPE_DISTINCT: |
|
142 | 4 | case self::TYPE_COUNT: |
|
143 | 162 | break; |
|
144 | |||
145 | default: |
||
146 | 1 | throw new \InvalidArgumentException('Invalid query type: ' . $query['type']); |
|
147 | } |
||
148 | |||
149 | 162 | $this->collection = $collection; |
|
150 | 162 | $this->query = $query; |
|
151 | 162 | $this->options = $options; |
|
152 | 162 | $this->dm = $dm; |
|
153 | 162 | $this->class = $class; |
|
154 | 162 | $this->hydrate = $hydrate; |
|
155 | 162 | $this->primers = $primers; |
|
156 | |||
157 | 162 | $this->setReadOnly($readOnly); |
|
158 | 162 | $this->setRefresh($refresh); |
|
159 | |||
160 | 162 | if (! isset($query['readPreference'])) { |
|
161 | 156 | return; |
|
162 | } |
||
163 | |||
164 | 6 | $this->unitOfWorkHints[self::HINT_READ_PREFERENCE] = $query['readPreference']; |
|
165 | 6 | } |
|
166 | |||
167 | 64 | public function __clone() |
|
168 | { |
||
169 | 64 | $this->iterator = null; |
|
170 | 64 | } |
|
171 | |||
172 | /** |
||
173 | * Return an array of information about the query structure for debugging. |
||
174 | * |
||
175 | * The $name parameter may be used to return a specific key from the |
||
176 | * internal $query array property. If omitted, the entire array will be |
||
177 | * returned. |
||
178 | */ |
||
179 | 27 | public function debug(?string $name = null) |
|
180 | { |
||
181 | 27 | return $name !== null ? $this->query[$name] : $this->query; |
|
182 | } |
||
183 | |||
184 | /** |
||
185 | * Execute the query and returns the results. |
||
186 | * |
||
187 | * @throws MongoDBException |
||
188 | * @return Iterator|int|string|array |
||
189 | */ |
||
190 | 122 | public function execute() |
|
191 | { |
||
192 | 122 | $results = $this->runQuery(); |
|
193 | |||
194 | 122 | if (! $this->hydrate) { |
|
195 | 9 | return $results; |
|
196 | } |
||
197 | |||
198 | 116 | if ($results instanceof Cursor) { |
|
199 | $results = $this->makeIterator($results); |
||
200 | } |
||
201 | |||
202 | 116 | $uow = $this->dm->getUnitOfWork(); |
|
203 | |||
204 | /* If a single document is returned from a findAndModify command and it |
||
205 | * includes the identifier field, attempt hydration. |
||
206 | */ |
||
207 | 116 | if (($this->query['type'] === self::TYPE_FIND_AND_UPDATE || |
|
208 | 116 | $this->query['type'] === self::TYPE_FIND_AND_REMOVE) && |
|
209 | 116 | is_array($results) && isset($results['_id'])) { |
|
210 | 5 | $results = $uow->getOrCreateDocument($this->class->name, $results, $this->unitOfWorkHints); |
|
211 | |||
212 | 5 | if (! empty($this->primers)) { |
|
213 | 1 | $referencePrimer = new ReferencePrimer($this->dm, $uow); |
|
214 | |||
215 | 1 | foreach ($this->primers as $fieldName => $primer) { |
|
216 | 1 | $primer = is_callable($primer) ? $primer : null; |
|
217 | 1 | $referencePrimer->primeReferences($this->class, [$results], $fieldName, $this->unitOfWorkHints, $primer); |
|
218 | } |
||
219 | } |
||
220 | } |
||
221 | |||
222 | 116 | return $results; |
|
223 | } |
||
224 | |||
225 | /** |
||
226 | * Gets the ClassMetadata instance. |
||
227 | */ |
||
228 | public function getClass(): ClassMetadata |
||
229 | { |
||
230 | return $this->class; |
||
231 | } |
||
232 | |||
233 | public function getDocumentManager(): DocumentManager |
||
234 | { |
||
235 | return $this->dm; |
||
236 | } |
||
237 | |||
238 | /** |
||
239 | * Execute the query and return its result, which must be an Iterator. |
||
240 | * |
||
241 | * If the query type is not expected to return an Iterator, |
||
242 | * BadMethodCallException will be thrown before executing the query. |
||
243 | * Otherwise, the query will be executed and UnexpectedValueException will |
||
244 | * be thrown if {@link Query::execute()} does not return an Iterator. |
||
245 | * |
||
246 | * @see http://php.net/manual/en/iteratoraggregate.getiterator.php |
||
247 | * @throws \BadMethodCallException If the query type would not return an Iterator. |
||
248 | * @throws \UnexpectedValueException If the query did not return an Iterator. |
||
249 | * @throws MongoDBException |
||
250 | */ |
||
251 | 84 | public function getIterator(): Iterator |
|
252 | { |
||
253 | 84 | switch ($this->query['type']) { |
|
254 | 84 | case self::TYPE_FIND: |
|
255 | 6 | case self::TYPE_GROUP: |
|
256 | 6 | case self::TYPE_MAP_REDUCE: |
|
257 | 6 | case self::TYPE_DISTINCT: |
|
258 | 78 | break; |
|
259 | |||
260 | default: |
||
261 | 6 | throw new \BadMethodCallException('Iterator would not be returned for query type: ' . $this->query['type']); |
|
262 | } |
||
263 | |||
264 | 78 | if ($this->iterator === null) { |
|
265 | 78 | $this->iterator = $this->execute(); |
|
0 ignored issues
–
show
|
|||
266 | } |
||
267 | |||
268 | 78 | return $this->iterator; |
|
269 | } |
||
270 | |||
271 | /** |
||
272 | * Return the query structure. |
||
273 | */ |
||
274 | 14 | public function getQuery(): array |
|
275 | { |
||
276 | 14 | return $this->query; |
|
277 | } |
||
278 | |||
279 | /** |
||
280 | * Execute the query and return the first result. |
||
281 | * |
||
282 | * @return array|object|null |
||
283 | */ |
||
284 | 64 | public function getSingleResult() |
|
285 | { |
||
286 | 64 | $clonedQuery = clone $this; |
|
287 | 64 | $clonedQuery->query['limit'] = 1; |
|
288 | 64 | return $clonedQuery->getIterator()->current() ?: null; |
|
289 | } |
||
290 | |||
291 | /** |
||
292 | * Return the query type. |
||
293 | */ |
||
294 | public function getType(): int |
||
295 | { |
||
296 | return $this->query['type']; |
||
297 | } |
||
298 | |||
299 | /** |
||
300 | * Sets whether or not to hydrate the documents to objects. |
||
301 | */ |
||
302 | public function setHydrate(bool $hydrate): void |
||
303 | { |
||
304 | $this->hydrate = $hydrate; |
||
305 | } |
||
306 | |||
307 | /** |
||
308 | * Set whether documents should be registered in UnitOfWork. If document would |
||
309 | * already be managed it will be left intact and new instance returned. |
||
310 | * |
||
311 | * This option has no effect if hydration is disabled. |
||
312 | */ |
||
313 | 162 | public function setReadOnly(bool $readOnly): void |
|
314 | { |
||
315 | 162 | $this->unitOfWorkHints[self::HINT_READ_ONLY] = $readOnly; |
|
316 | 162 | } |
|
317 | |||
318 | /** |
||
319 | * Set whether to refresh hydrated documents that are already in the |
||
320 | * identity map. |
||
321 | * |
||
322 | * This option has no effect if hydration is disabled. |
||
323 | */ |
||
324 | 162 | public function setRefresh(bool $refresh): void |
|
325 | { |
||
326 | 162 | $this->unitOfWorkHints[self::HINT_REFRESH] = (bool) $refresh; |
|
327 | 162 | } |
|
328 | |||
329 | /** |
||
330 | * Execute the query and return its results as an array. |
||
331 | * |
||
332 | * @see IteratorAggregate::toArray() |
||
333 | */ |
||
334 | 11 | public function toArray(): array |
|
335 | { |
||
336 | 11 | return $this->getIterator()->toArray(); |
|
337 | } |
||
338 | |||
339 | /** |
||
340 | * Returns an array containing the specified keys and their values from the |
||
341 | * query array, provided they exist and are not null. |
||
342 | */ |
||
343 | 121 | private function getQueryOptions(string ...$keys): array |
|
344 | { |
||
345 | 121 | return array_filter( |
|
346 | 121 | array_intersect_key($this->query, array_flip($keys)), |
|
347 | function ($value) { |
||
348 | 88 | return $value !== null; |
|
349 | 121 | } |
|
350 | ); |
||
351 | } |
||
352 | |||
353 | 106 | private function makeIterator(Cursor $cursor): Iterator |
|
354 | { |
||
355 | 106 | if ($this->hydrate && $this->class) { |
|
356 | 98 | $cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $this->class, $this->unitOfWorkHints); |
|
357 | } |
||
358 | |||
359 | 106 | $cursor = new CachingIterator($cursor); |
|
360 | |||
361 | 106 | if (! empty($this->primers)) { |
|
362 | 20 | $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork()); |
|
363 | 20 | $cursor = new PrimingIterator($cursor, $this->class, $referencePrimer, $this->primers, $this->unitOfWorkHints); |
|
364 | } |
||
365 | |||
366 | 106 | return $cursor; |
|
367 | } |
||
368 | |||
369 | /** |
||
370 | * Returns an array with its keys renamed based on the translation map. |
||
371 | * |
||
372 | * @return array $rename Translation map (from => to) for renaming keys |
||
373 | */ |
||
374 | 111 | private function renameQueryOptions(array $options, array $rename): array |
|
375 | { |
||
376 | 111 | if (empty($options)) { |
|
377 | 42 | return $options; |
|
378 | } |
||
379 | |||
380 | 86 | return array_combine( |
|
381 | 86 | array_map( |
|
382 | function ($key) use ($rename) { |
||
383 | 86 | return $rename[$key] ?? $key; |
|
384 | 86 | }, |
|
385 | 86 | array_keys($options) |
|
386 | ), |
||
387 | 86 | array_values($options) |
|
388 | ); |
||
389 | } |
||
390 | |||
391 | /** |
||
392 | * Execute the query and return its result. |
||
393 | * |
||
394 | * The return value will vary based on the query type. Commands with results |
||
395 | * (e.g. aggregate, inline mapReduce) may return an ArrayIterator. Other |
||
396 | * commands and operations may return a status array or a boolean, depending |
||
397 | * on the driver's write concern. Queries and some mapReduce commands will |
||
398 | * return an Iterator. |
||
399 | * |
||
400 | * @return Iterator|string|int|array |
||
401 | */ |
||
402 | 122 | public function runQuery() |
|
403 | { |
||
404 | 122 | $options = $this->options; |
|
405 | |||
406 | 122 | switch ($this->query['type']) { |
|
407 | 122 | case self::TYPE_FIND: |
|
408 | 106 | $queryOptions = $this->getQueryOptions('select', 'sort', 'skip', 'limit', 'readPreference'); |
|
409 | 106 | $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']); |
|
410 | |||
411 | 106 | $cursor = $this->collection->find( |
|
412 | 106 | $this->query['query'], |
|
413 | 106 | $queryOptions |
|
414 | ); |
||
415 | |||
416 | 106 | return $this->makeIterator($cursor); |
|
417 | |||
418 | 24 | case self::TYPE_FIND_AND_UPDATE: |
|
419 | 6 | $queryOptions = $this->getQueryOptions('select', 'sort', 'upsert'); |
|
420 | 6 | $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']); |
|
421 | 6 | $queryOptions['returnDocument'] = ($this->query['new'] ?? false) ? FindOneAndUpdate::RETURN_DOCUMENT_AFTER : FindOneAndUpdate::RETURN_DOCUMENT_BEFORE; |
|
422 | |||
423 | 6 | return $this->collection->findOneAndUpdate( |
|
424 | 6 | $this->query['query'], |
|
425 | 6 | $this->query['newObj'], |
|
426 | 6 | array_merge($options, $queryOptions) |
|
427 | ); |
||
428 | |||
429 | 19 | case self::TYPE_FIND_AND_REMOVE: |
|
430 | 2 | $queryOptions = $this->getQueryOptions('select', 'sort'); |
|
431 | 2 | $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']); |
|
432 | |||
433 | 2 | return $this->collection->findOneAndDelete( |
|
434 | 2 | $this->query['query'], |
|
435 | 2 | array_merge($options, $queryOptions) |
|
436 | ); |
||
437 | |||
438 | 17 | case self::TYPE_INSERT: |
|
439 | return $this->collection->insertOne($this->query['newObj'], $options); |
||
440 | |||
441 | 17 | case self::TYPE_UPDATE: |
|
442 | 12 | if ($this->query['multiple'] ?? false) { |
|
443 | 2 | return $this->collection->updateMany( |
|
444 | 2 | $this->query['query'], |
|
445 | 2 | $this->query['newObj'], |
|
446 | 2 | array_merge($options, $this->getQueryOptions('upsert')) |
|
447 | ); |
||
448 | } |
||
449 | |||
450 | 10 | return $this->collection->updateOne( |
|
451 | 10 | $this->query['query'], |
|
452 | 10 | $this->query['newObj'], |
|
453 | 10 | array_merge($options, $this->getQueryOptions('upsert')) |
|
454 | ); |
||
455 | |||
456 | 5 | case self::TYPE_REMOVE: |
|
457 | 1 | return $this->collection->deleteMany($this->query['query'], $options); |
|
458 | |||
459 | 4 | case self::TYPE_DISTINCT: |
|
460 | 2 | $collection = $this->collection; |
|
461 | 2 | $query = $this->query; |
|
462 | |||
463 | 2 | return $collection->distinct( |
|
464 | 2 | $query['distinct'], |
|
465 | 2 | $query['query'], |
|
466 | 2 | array_merge($options, $this->getQueryOptions('readPreference')) |
|
467 | ); |
||
468 | |||
469 | 2 | case self::TYPE_COUNT: |
|
470 | 2 | $collection = $this->collection; |
|
471 | 2 | $query = $this->query; |
|
472 | |||
473 | 2 | return $collection->count( |
|
474 | 2 | $query['query'], |
|
475 | 2 | array_merge($options, $this->getQueryOptions('hint', 'limit', 'skip', 'readPreference')) |
|
476 | ); |
||
477 | } |
||
478 | } |
||
479 | } |
||
480 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.