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