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