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(); |
|
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; |
|
0 ignored issues
–
show
|
|||
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(); |
|
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...
|
|||
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 |
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: