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\Aggregation\Stage; |
||
6 | |||
7 | use Doctrine\Common\Persistence\Mapping\MappingException as BaseMappingException; |
||
8 | use Doctrine\ODM\MongoDB\Aggregation\Builder; |
||
9 | use Doctrine\ODM\MongoDB\Aggregation\Expr; |
||
10 | use Doctrine\ODM\MongoDB\Aggregation\Stage; |
||
11 | use Doctrine\ODM\MongoDB\DocumentManager; |
||
12 | use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; |
||
13 | use Doctrine\ODM\MongoDB\Mapping\MappingException; |
||
14 | use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; |
||
15 | use Doctrine\ODM\MongoDB\Types\Type; |
||
16 | use LogicException; |
||
17 | use function array_map; |
||
18 | use function is_array; |
||
19 | use function is_string; |
||
20 | use function substr; |
||
21 | |||
22 | class GraphLookup extends Stage |
||
23 | { |
||
24 | /** @var string */ |
||
25 | private $from; |
||
26 | |||
27 | /** @var string|Expr|array */ |
||
28 | private $startWith; |
||
29 | |||
30 | /** @var string */ |
||
31 | private $connectFromField; |
||
32 | |||
33 | /** @var string */ |
||
34 | private $connectToField; |
||
35 | |||
36 | /** @var string */ |
||
37 | private $as; |
||
38 | |||
39 | /** @var int */ |
||
40 | private $maxDepth; |
||
41 | |||
42 | /** @var string */ |
||
43 | private $depthField; |
||
44 | |||
45 | /** @var Stage\GraphLookup\Match */ |
||
46 | private $restrictSearchWithMatch; |
||
47 | |||
48 | /** @var DocumentManager */ |
||
49 | private $dm; |
||
50 | |||
51 | /** @var ClassMetadata */ |
||
52 | private $class; |
||
53 | |||
54 | /** @var ClassMetadata|null */ |
||
55 | private $targetClass; |
||
56 | |||
57 | /** |
||
58 | * @param string $from Target collection for the $graphLookup operation to |
||
59 | * search, recursively matching the connectFromField to the connectToField. |
||
60 | */ |
||
61 | 11 | public function __construct(Builder $builder, string $from, DocumentManager $documentManager, ClassMetadata $class) |
|
62 | { |
||
63 | 11 | parent::__construct($builder); |
|
64 | |||
65 | 11 | $this->dm = $documentManager; |
|
66 | 11 | $this->class = $class; |
|
67 | 11 | $this->restrictSearchWithMatch = new GraphLookup\Match($this->builder, $this); |
|
68 | 11 | $this->from($from); |
|
69 | 10 | } |
|
70 | |||
71 | /** |
||
72 | * Name of the array field added to each output document. |
||
73 | * |
||
74 | * Contains the documents traversed in the $graphLookup stage to reach the |
||
75 | * document. |
||
76 | */ |
||
77 | 9 | public function alias(string $alias) : self |
|
78 | { |
||
79 | 9 | $this->as = $alias; |
|
80 | |||
81 | 9 | return $this; |
|
82 | } |
||
83 | |||
84 | /** |
||
85 | * Field name whose value $graphLookup uses to recursively match against the |
||
86 | * connectToField of other documents in the collection. |
||
87 | * |
||
88 | * Optionally, connectFromField may be an array of field names, each of |
||
89 | * which is individually followed through the traversal process. |
||
90 | */ |
||
91 | 10 | public function connectFromField(string $connectFromField) : self |
|
92 | { |
||
93 | // No targetClass mapping - simply use field name as is |
||
94 | 10 | if (! $this->targetClass) { |
|
95 | 4 | $this->connectFromField = $connectFromField; |
|
96 | 4 | return $this; |
|
97 | } |
||
98 | |||
99 | // connectFromField doesn't have to be a reference - in this case, just convert the field name |
||
100 | 6 | if (! $this->targetClass->hasReference($connectFromField)) { |
|
101 | 2 | $this->connectFromField = $this->convertTargetFieldName($connectFromField); |
|
102 | 2 | return $this; |
|
103 | } |
||
104 | |||
105 | // connectFromField is a reference - do a sanity check |
||
106 | 4 | $referenceMapping = $this->targetClass->getFieldMapping($connectFromField); |
|
107 | 4 | if ($referenceMapping['targetDocument'] !== $this->targetClass->name) { |
|
108 | 1 | throw MappingException::connectFromFieldMustReferenceSameDocument($connectFromField); |
|
109 | } |
||
110 | |||
111 | 3 | $this->connectFromField = $this->getReferencedFieldName($connectFromField, $referenceMapping); |
|
112 | 3 | return $this; |
|
113 | } |
||
114 | |||
115 | /** |
||
116 | * Field name in other documents against which to match the value of the |
||
117 | * field specified by the connectFromField parameter. |
||
118 | */ |
||
119 | 10 | public function connectToField(string $connectToField) : self |
|
120 | { |
||
121 | 10 | $this->connectToField = $this->convertTargetFieldName($connectToField); |
|
122 | 10 | return $this; |
|
123 | } |
||
124 | |||
125 | /** |
||
126 | * Name of the field to add to each traversed document in the search path. |
||
127 | * |
||
128 | * The value of this field is the recursion depth for the document, |
||
129 | * represented as a NumberLong. Recursion depth value starts at zero, so the |
||
130 | * first lookup corresponds to zero depth. |
||
131 | */ |
||
132 | 3 | public function depthField(string $depthField) : self |
|
133 | { |
||
134 | 3 | $this->depthField = $depthField; |
|
135 | |||
136 | 3 | return $this; |
|
137 | } |
||
138 | |||
139 | /** |
||
140 | * Target collection for the $graphLookup operation to search, recursively |
||
141 | * matching the connectFromField to the connectToField. |
||
142 | * |
||
143 | * The from collection cannot be sharded and must be in the same database as |
||
144 | * any other collections used in the operation. |
||
145 | */ |
||
146 | 11 | public function from(string $from) : self |
|
147 | { |
||
148 | // $from can either be |
||
149 | // a) a field name indicating a reference to a different document. Currently, only REFERENCE_STORE_AS_ID is supported |
||
150 | // b) a Class name |
||
151 | // c) a collection name |
||
152 | // In cases b) and c) the local and foreign fields need to be filled |
||
153 | 11 | if ($this->class->hasReference($from)) { |
|
154 | 6 | return $this->fromReference($from); |
|
155 | } |
||
156 | |||
157 | // Check if mapped class with given name exists |
||
158 | try { |
||
159 | 5 | $this->targetClass = $this->dm->getClassMetadata($from); |
|
0 ignored issues
–
show
|
|||
160 | 4 | } catch (BaseMappingException $e) { |
|
161 | 4 | $this->from = $from; |
|
162 | 4 | return $this; |
|
163 | } |
||
164 | |||
165 | 1 | if ($this->targetClass->isSharded()) { |
|
166 | 1 | throw MappingException::cannotUseShardedCollectionInLookupStages($this->targetClass->name); |
|
0 ignored issues
–
show
Accessing
name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
If you access a property on an interface, you most likely code against a concrete implementation of the interface. Available Fixes
Loading history...
|
|||
167 | } |
||
168 | |||
169 | $this->from = $this->targetClass->getCollection(); |
||
170 | return $this; |
||
171 | } |
||
172 | |||
173 | /** |
||
174 | * {@inheritdoc} |
||
175 | */ |
||
176 | 9 | public function getExpression() : array |
|
177 | { |
||
178 | 9 | $restrictSearchWithMatch = $this->restrictSearchWithMatch->getExpression() ?: (object) []; |
|
179 | |||
180 | $graphLookup = [ |
||
181 | 9 | 'from' => $this->from, |
|
182 | 9 | 'startWith' => $this->convertExpression($this->startWith), |
|
183 | 9 | 'connectFromField' => $this->connectFromField, |
|
184 | 9 | 'connectToField' => $this->connectToField, |
|
185 | 9 | 'as' => $this->as, |
|
186 | 9 | 'restrictSearchWithMatch' => $restrictSearchWithMatch, |
|
187 | 9 | 'maxDepth' => $this->maxDepth, |
|
188 | 9 | 'depthField' => $this->depthField, |
|
189 | ]; |
||
190 | |||
191 | 9 | foreach (['maxDepth', 'depthField'] as $field) { |
|
192 | 9 | if ($graphLookup[$field] !== null) { |
|
193 | 3 | continue; |
|
194 | } |
||
195 | |||
196 | 6 | unset($graphLookup[$field]); |
|
197 | } |
||
198 | |||
199 | 9 | return ['$graphLookup' => $graphLookup]; |
|
200 | } |
||
201 | |||
202 | /** |
||
203 | * Non-negative integral number specifying the maximum recursion depth. |
||
204 | */ |
||
205 | 3 | public function maxDepth(int $maxDepth) : self |
|
206 | { |
||
207 | 3 | $this->maxDepth = $maxDepth; |
|
208 | |||
209 | 3 | return $this; |
|
210 | } |
||
211 | |||
212 | /** |
||
213 | * A document specifying additional conditions for the recursive search. |
||
214 | */ |
||
215 | 1 | public function restrictSearchWithMatch() : GraphLookup\Match |
|
216 | { |
||
217 | 1 | return $this->restrictSearchWithMatch; |
|
218 | } |
||
219 | |||
220 | /** |
||
221 | * Expression that specifies the value of the connectFromField with which to |
||
222 | * start the recursive search. |
||
223 | * |
||
224 | * Optionally, startWith may be array of values, each of which is |
||
225 | * individually followed through the traversal process. |
||
226 | * |
||
227 | * @param string|array|Expr $expression |
||
228 | */ |
||
229 | 10 | public function startWith($expression) : self |
|
230 | { |
||
231 | 10 | $this->startWith = $expression; |
|
232 | |||
233 | 10 | return $this; |
|
234 | } |
||
235 | |||
236 | /** |
||
237 | * @throws MappingException |
||
238 | */ |
||
239 | 6 | private function fromReference(string $fieldName) : self |
|
240 | { |
||
241 | 6 | if (! $this->class->hasReference($fieldName)) { |
|
242 | MappingException::referenceMappingNotFound($this->class->name, $fieldName); |
||
243 | } |
||
244 | |||
245 | 6 | $referenceMapping = $this->class->getFieldMapping($fieldName); |
|
246 | 6 | $this->targetClass = $this->dm->getClassMetadata($referenceMapping['targetDocument']); |
|
0 ignored issues
–
show
It seems like
$this->dm->getClassMetad...ping['targetDocument']) of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is incompatible with the declared type object<Doctrine\ODM\Mong...ing\ClassMetadata>|null of property $targetClass .
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property. Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..
Loading history...
|
|||
247 | 6 | if ($this->targetClass->isSharded()) { |
|
248 | throw MappingException::cannotUseShardedCollectionInLookupStages($this->targetClass->name); |
||
0 ignored issues
–
show
Accessing
name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
If you access a property on an interface, you most likely code against a concrete implementation of the interface. Available Fixes
Loading history...
|
|||
249 | } |
||
250 | |||
251 | 6 | $this->from = $this->targetClass->getCollection(); |
|
252 | |||
253 | 6 | $referencedFieldName = $this->getReferencedFieldName($fieldName, $referenceMapping); |
|
254 | |||
255 | 6 | if ($referenceMapping['isOwningSide']) { |
|
256 | $this |
||
257 | 5 | ->startWith('$' . $referencedFieldName) |
|
258 | 5 | ->connectToField('_id'); |
|
259 | } else { |
||
260 | $this |
||
261 | 1 | ->startWith('$' . $referencedFieldName) |
|
262 | 1 | ->connectToField('_id'); |
|
263 | } |
||
264 | |||
265 | // A self-reference indicates that we can also fill the "connectFromField" accordingly |
||
266 | 6 | if ($this->targetClass->name === $this->class->name) { |
|
0 ignored issues
–
show
Accessing
name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
If you access a property on an interface, you most likely code against a concrete implementation of the interface. Available Fixes
Loading history...
|
|||
267 | 3 | $this->connectFromField($referencedFieldName); |
|
268 | } |
||
269 | |||
270 | 6 | return $this; |
|
271 | } |
||
272 | |||
273 | 9 | View Code Duplication | private function convertExpression($expression) |
274 | { |
||
275 | 9 | if (is_array($expression)) { |
|
276 | return array_map([$this, 'convertExpression'], $expression); |
||
277 | 9 | } elseif (is_string($expression) && substr($expression, 0, 1) === '$') { |
|
278 | 9 | return '$' . $this->getDocumentPersister($this->class)->prepareFieldName(substr($expression, 1)); |
|
279 | } |
||
280 | |||
281 | return Type::convertPHPToDatabaseValue(Expr::convertExpression($expression)); |
||
282 | } |
||
283 | |||
284 | 10 | private function convertTargetFieldName($fieldName) |
|
285 | { |
||
286 | 10 | if (is_array($fieldName)) { |
|
287 | return array_map([$this, 'convertTargetFieldName'], $fieldName); |
||
288 | } |
||
289 | |||
290 | 10 | if (! $this->targetClass) { |
|
291 | 4 | return $fieldName; |
|
292 | } |
||
293 | |||
294 | 6 | return $this->getDocumentPersister($this->targetClass)->prepareFieldName($fieldName); |
|
295 | } |
||
296 | |||
297 | 10 | private function getDocumentPersister(ClassMetadata $class) : DocumentPersister |
|
298 | { |
||
299 | 10 | return $this->dm->getUnitOfWork()->getDocumentPersister($class->name); |
|
300 | } |
||
301 | |||
302 | 6 | private function getReferencedFieldName(string $fieldName, array $mapping) : string |
|
303 | { |
||
304 | 6 | if (! $this->targetClass) { |
|
305 | throw new LogicException('Cannot use getReferencedFieldName when no target mapping was given.'); |
||
306 | } |
||
307 | |||
308 | 6 | if (! $mapping['isOwningSide']) { |
|
309 | 1 | if (isset($mapping['repositoryMethod']) || ! isset($mapping['mappedBy'])) { |
|
310 | throw MappingException::repositoryMethodLookupNotAllowed($this->class->name, $fieldName); |
||
311 | } |
||
312 | |||
313 | 1 | $mapping = $this->targetClass->getFieldMapping($mapping['mappedBy']); |
|
314 | } |
||
315 | |||
316 | 6 | switch ($mapping['storeAs']) { |
|
317 | case ClassMetadata::REFERENCE_STORE_AS_ID: |
||
318 | case ClassMetadata::REFERENCE_STORE_AS_REF: |
||
319 | 6 | return ClassMetadata::getReferenceFieldName($mapping['storeAs'], $mapping['name']); |
|
320 | break; |
||
321 | |||
322 | default: |
||
323 | throw MappingException::cannotLookupDbRefReference($this->class->name, $fieldName); |
||
324 | } |
||
325 | } |
||
326 | } |
||
327 |
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.
Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..