Completed
Pull Request — master (#1787)
by Stefano
21:31
created

GraphLookup::startWith()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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 function array_map;
17
use function is_array;
18
use function is_string;
19
use function substr;
20
21
class GraphLookup extends Stage
22
{
23
    /** @var string */
24
    private $from;
25
26
    /** @var string|Expr|array */
27
    private $startWith;
28
29
    /** @var string */
30
    private $connectFromField;
31
32
    /** @var string */
33
    private $connectToField;
34
35
    /** @var string */
36
    private $as;
37
38
    /** @var int */
39
    private $maxDepth;
40
41
    /** @var string */
42
    private $depthField;
43
44
    /** @var Stage\GraphLookup\Match */
45
    private $restrictSearchWithMatch;
46
47
    /** @var DocumentManager */
48
    private $dm;
49
50
    /** @var ClassMetadata */
51
    private $class;
52
53
    /** @var ClassMetadata */
54
    private $targetClass;
55
56
    /**
57
     * @param string $from Target collection for the $graphLookup operation to
58
     * search, recursively matching the connectFromField to the connectToField.
59
     */
60 11
    public function __construct(Builder $builder, $from, DocumentManager $documentManager, ClassMetadata $class)
61
    {
62 11
        parent::__construct($builder);
63
64 11
        $this->dm = $documentManager;
65 11
        $this->class = $class;
66 11
        $this->restrictSearchWithMatch = new GraphLookup\Match($this->builder, $this);
67 11
        $this->from($from);
68 10
    }
69
70
    /**
71
     * Name of the array field added to each output document.
72
     *
73
     * Contains the documents traversed in the $graphLookup stage to reach the
74
     * document.
75
     *
76
     * @param string $alias
77
     *
78
     * @return $this
79
     */
80 9
    public function alias($alias)
81
    {
82 9
        $this->as = $alias;
83
84 9
        return $this;
85
    }
86
87
    /**
88
     * Field name whose value $graphLookup uses to recursively match against the
89
     * connectToField of other documents in the collection.
90
     *
91
     * Optionally, connectFromField may be an array of field names, each of
92
     * which is individually followed through the traversal process.
93
     *
94
     * @param string $connectFromField
95
     *
96
     * @return $this
97
     */
98 10
    public function connectFromField($connectFromField)
99
    {
100
        // No targetClass mapping - simply use field name as is
101 10
        if (! $this->targetClass) {
102 4
            $this->connectFromField = $connectFromField;
103 4
            return $this;
104
        }
105
106
        // connectFromField doesn't have to be a reference - in this case, just convert the field name
107 6
        if (! $this->targetClass->hasReference($connectFromField)) {
108 2
            $this->connectFromField = $this->convertTargetFieldName($connectFromField);
109 2
            return $this;
110
        }
111
112
        // connectFromField is a reference - do a sanity check
113 4
        $referenceMapping = $this->targetClass->getFieldMapping($connectFromField);
114 4
        if ($referenceMapping['targetDocument'] !== $this->targetClass->name) {
115 1
            throw MappingException::connectFromFieldMustReferenceSameDocument($connectFromField);
116
        }
117
118 3
        $this->connectFromField = $this->getReferencedFieldName($connectFromField, $referenceMapping);
119 3
        return $this;
120
    }
121
122
    /**
123
     * Field name in other documents against which to match the value of the
124
     * field specified by the connectFromField parameter.
125
     *
126
     * @param string $connectToField
127
     *
128
     * @return $this
129
     */
130 10
    public function connectToField($connectToField)
131
    {
132 10
        $this->connectToField = $this->convertTargetFieldName($connectToField);
133 10
        return $this;
134
    }
135
136
    /**
137
     * Name of the field to add to each traversed document in the search path.
138
     *
139
     * The value of this field is the recursion depth for the document,
140
     * represented as a NumberLong. Recursion depth value starts at zero, so the
141
     * first lookup corresponds to zero depth.
142
     *
143
     * @param string $depthField
144
     *
145
     * @return $this
146
     */
147 3
    public function depthField($depthField)
148
    {
149 3
        $this->depthField = $depthField;
150
151 3
        return $this;
152
    }
153
154
    /**
155
     * Target collection for the $graphLookup operation to search, recursively
156
     * matching the connectFromField to the connectToField.
157
     *
158
     * The from collection cannot be sharded and must be in the same database as
159
     * any other collections used in the operation.
160
     *
161
     * @param string $from
162
     *
163
     * @return $this
164
     */
165 11
    public function from($from)
166
    {
167
        // $from can either be
168
        // a) a field name indicating a reference to a different document. Currently, only REFERENCE_STORE_AS_ID is supported
169
        // b) a Class name
170
        // c) a collection name
171
        // In cases b) and c) the local and foreign fields need to be filled
172 11
        if ($this->class->hasReference($from)) {
173 6
            return $this->fromReference($from);
174
        }
175
176
        // Check if mapped class with given name exists
177
        try {
178 5
            $this->targetClass = $this->dm->getClassMetadata($from);
179 4
        } catch (BaseMappingException $e) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Persiste...apping\MappingException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
180 4
            $this->from = $from;
181 4
            return $this;
182
        }
183
184 1
        if ($this->targetClass->isSharded()) {
185 1
            throw MappingException::cannotUseShardedCollectionInLookupStages($this->targetClass->name);
186
        }
187
188
        $this->from = $this->targetClass->getCollection();
189
        return $this;
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195 9
    public function getExpression()
196
    {
197
        $graphLookup = [
198 9
            'from' => $this->from,
199 9
            'startWith' => $this->convertExpression($this->startWith),
200 9
            'connectFromField' => $this->connectFromField,
201 9
            'connectToField' => $this->connectToField,
202 9
            'as' => $this->as,
203 9
            'restrictSearchWithMatch' => $this->restrictSearchWithMatch->getExpression(),
204 9
            'maxDepth' => $this->maxDepth,
205 9
            'depthField' => $this->depthField,
206
        ];
207
208 9
        foreach (['maxDepth', 'depthField'] as $field) {
209 9
            if ($graphLookup[$field] !== null) {
210 3
                continue;
211
            }
212
213 6
            unset($graphLookup[$field]);
214
        }
215
216 9
        return ['$graphLookup' => $graphLookup];
217
    }
218
219
    /**
220
     * Non-negative integral number specifying the maximum recursion depth.
221
     *
222
     * @param int $maxDepth
223
     *
224
     * @return $this
225
     */
226 3
    public function maxDepth($maxDepth)
227
    {
228 3
        $this->maxDepth = $maxDepth;
229
230 3
        return $this;
231
    }
232
233
    /**
234
     * A document specifying additional conditions for the recursive search.
235
     *
236
     * @return GraphLookup\Match
237
     */
238 1
    public function restrictSearchWithMatch()
239
    {
240 1
        return $this->restrictSearchWithMatch;
241
    }
242
243
    /**
244
     * Expression that specifies the value of the connectFromField with which to
245
     * start the recursive search.
246
     *
247
     * Optionally, startWith may be array of values, each of which is
248
     * individually followed through the traversal process.
249
     *
250
     * @param string|array|Expr $expression
251
     *
252
     * @return $this
253
     */
254 10
    public function startWith($expression)
255
    {
256 10
        $this->startWith = $expression;
257
258 10
        return $this;
259
    }
260
261
    /**
262
     * @param string $fieldName
263
     * @return $this
264
     * @throws MappingException
265
     */
266 6
    private function fromReference($fieldName)
267
    {
268 6
        if (! $this->class->hasReference($fieldName)) {
269
            MappingException::referenceMappingNotFound($this->class->name, $fieldName);
270
        }
271
272 6
        $referenceMapping = $this->class->getFieldMapping($fieldName);
273 6
        $this->targetClass = $this->dm->getClassMetadata($referenceMapping['targetDocument']);
274 6
        if ($this->targetClass->isSharded()) {
275
            throw MappingException::cannotUseShardedCollectionInLookupStages($this->targetClass->name);
276
        }
277
278 6
        $this->from = $this->targetClass->getCollection();
279
280 6
        $referencedFieldName = $this->getReferencedFieldName($fieldName, $referenceMapping);
281
282 6
        if ($referenceMapping['isOwningSide']) {
283
            $this
284 5
                ->startWith('$' . $referencedFieldName)
285 5
                ->connectToField('_id');
286
        } else {
287
            $this
288 1
                ->startWith('$' . $referencedFieldName)
289 1
                ->connectToField('_id');
290
        }
291
292
        // A self-reference indicates that we can also fill the "connectFromField" accordingly
293 6
        if ($this->targetClass->name === $this->class->name) {
294 3
            $this->connectFromField($referencedFieldName);
295
        }
296
297 6
        return $this;
298
    }
299
300 9 View Code Duplication
    private function convertExpression($expression)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
301
    {
302 9
        if (is_array($expression)) {
303
            return array_map([$this, 'convertExpression'], $expression);
304 9
        } elseif (is_string($expression) && substr($expression, 0, 1) === '$') {
305 9
            return '$' . $this->getDocumentPersister($this->class)->prepareFieldName(substr($expression, 1));
306
        }
307
308
        return Type::convertPHPToDatabaseValue(Expr::convertExpression($expression));
309
    }
310
311 10
    private function convertTargetFieldName($fieldName)
312
    {
313 10
        if (is_array($fieldName)) {
314
            return array_map([$this, 'convertTargetFieldName'], $fieldName);
315
        }
316
317 10
        if (! $this->targetClass) {
318 4
            return $fieldName;
319
        }
320
321 6
        return $this->getDocumentPersister($this->targetClass)->prepareFieldName($fieldName);
322
    }
323
324
    /**
325
     * @return DocumentPersister
326
     */
327 10
    private function getDocumentPersister(ClassMetadata $class)
328
    {
329 10
        return $this->dm->getUnitOfWork()->getDocumentPersister($class->name);
330
    }
331
332 6
    private function getReferencedFieldName($fieldName, array $mapping)
333
    {
334 6
        if (! $mapping['isOwningSide']) {
335 1
            if (isset($mapping['repositoryMethod']) || ! isset($mapping['mappedBy'])) {
336
                throw MappingException::repositoryMethodLookupNotAllowed($this->class->name, $fieldName);
337
            }
338
339 1
            $mapping = $this->targetClass->getFieldMapping($mapping['mappedBy']);
340
        }
341
342 6
        switch ($mapping['storeAs']) {
343
            case ClassMetadata::REFERENCE_STORE_AS_ID:
344
            case ClassMetadata::REFERENCE_STORE_AS_REF:
345 6
                return ClassMetadata::getReferenceFieldName($mapping['storeAs'], $mapping['name']);
346
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
347
348
            default:
349
                throw MappingException::cannotLookupDbRefReference($this->class->name, $fieldName);
350
        }
351
    }
352
}
353