Completed
Push — master ( 11f27c...4713a5 )
by Andreas
08:33
created

GraphLookup   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 356
Duplicated Lines 13.48 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 91%

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 10
dl 48
loc 356
ccs 91
cts 100
cp 0.91
rs 8.6
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A alias() 0 6 1
B connectFromField() 0 23 4
A connectToField() 0 5 1
A depthField() 0 6 1
B from() 26 26 4
A getExpression() 0 21 3
A maxDepth() 0 6 1
A restrictSearchWithMatch() 0 4 1
A startWith() 0 6 1
B fromReference() 0 33 5
A convertExpression() 10 10 4
A convertTargetFieldName() 0 12 3
A getDocumentPersister() 0 4 1
B getReferencedFieldName() 12 20 6

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB\Aggregation\Stage;
21
22
use Doctrine\Common\Persistence\Mapping\MappingException as BaseMappingException;
23
use Doctrine\ODM\MongoDB\Aggregation\Builder;
24
use Doctrine\ODM\MongoDB\Aggregation\Expr;
25
use Doctrine\ODM\MongoDB\Aggregation\Stage;
26
use Doctrine\ODM\MongoDB\DocumentManager;
27
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
28
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
29
use Doctrine\ODM\MongoDB\Mapping\MappingException;
30
use Doctrine\ODM\MongoDB\Types\Type;
31
32
class GraphLookup extends Stage
33
{
34
    /**
35
     * @var string
36
     */
37
    private $from;
38
39
    /**
40
     * @var string|Expr|array
41
     */
42
    private $startWith;
43
44
    /**
45
     * @var string
46
     */
47
    private $connectFromField;
48
49
    /**
50
     * @var string
51
     */
52
    private $connectToField;
53
54
    /**
55
     * @var string
56
     */
57
    private $as;
58
59
    /**
60
     * @var int
61
     */
62
    private $maxDepth;
63
64
    /**
65
     * @var string
66
     */
67
    private $depthField;
68
69
    /**
70
     * @var Stage\GraphLookup\Match
71
     */
72
    private $restrictSearchWithMatch;
73
74
    /**
75
     * @var DocumentManager
76
     */
77
    private $dm;
78
79
    /**
80
     * @var ClassMetadata
81
     */
82
    private $class;
83
84
    /**
85
     * @var ClassMetadata
86
     */
87
    private $targetClass;
88
89
    /**
90
     * @param Builder $builder
91
     * @param string $from Target collection for the $graphLookup operation to
92
     * search, recursively matching the connectFromField to the connectToField.
93
     * @param DocumentManager $documentManager
94
     * @param ClassMetadata $class
95
     */
96 11
    public function __construct(Builder $builder, $from, DocumentManager $documentManager, ClassMetadata $class)
97
    {
98 11
        parent::__construct($builder);
99
100 11
        $this->dm = $documentManager;
101 11
        $this->class = $class;
102 11
        $this->restrictSearchWithMatch = new GraphLookup\Match($this->builder, $this);
103 11
        $this->from($from);
104 10
    }
105
106
    /**
107
     * Name of the array field added to each output document.
108
     *
109
     * Contains the documents traversed in the $graphLookup stage to reach the
110
     * document.
111
     *
112
     * @param string $alias
113
     *
114
     * @return $this
115
     */
116 9
    public function alias($alias)
117
    {
118 9
        $this->as = $alias;
119
120 9
        return $this;
121
    }
122
123
    /**
124
     * Field name whose value $graphLookup uses to recursively match against the
125
     * connectToField of other documents in the collection.
126
     *
127
     * Optionally, connectFromField may be an array of field names, each of
128
     * which is individually followed through the traversal process.
129
     *
130
     * @param string $connectFromField
131
     *
132
     * @return $this
133
     */
134 10
    public function connectFromField($connectFromField)
135
    {
136
        // No targetClass mapping - simply use field name as is
137 10
        if ( ! $this->targetClass) {
138 4
            $this->connectFromField = $connectFromField;
139 4
            return $this;
140
        }
141
142
        // connectFromField doesn't have to be a reference - in this case, just convert the field name
143 6
        if ( ! $this->targetClass->hasReference($connectFromField)) {
144 2
            $this->connectFromField = $this->convertTargetFieldName($connectFromField);
145 2
            return $this;
146
        }
147
148
        // connectFromField is a reference - do a sanity check
149 4
        $referenceMapping = $this->targetClass->getFieldMapping($connectFromField);
150 4
        if ($referenceMapping['targetDocument'] !== $this->targetClass->name) {
151 1
            throw MappingException::connectFromFieldMustReferenceSameDocument($connectFromField);
152
        }
153
154 3
        $this->connectFromField = $this->getReferencedFieldName($connectFromField, $referenceMapping);
155 3
        return $this;
156
    }
157
158
    /**
159
     * Field name in other documents against which to match the value of the
160
     * field specified by the connectFromField parameter.
161
     *
162
     * @param string $connectToField
163
     *
164
     * @return $this
165
     */
166 10
    public function connectToField($connectToField)
167
    {
168 10
        $this->connectToField = $this->convertTargetFieldName($connectToField);
169 10
        return $this;
170
    }
171
172
    /**
173
     * Name of the field to add to each traversed document in the search path.
174
     *
175
     * The value of this field is the recursion depth for the document,
176
     * represented as a NumberLong. Recursion depth value starts at zero, so the
177
     * first lookup corresponds to zero depth.
178
     *
179
     * @param string $depthField
180
     *
181
     * @return $this
182
     */
183 3
    public function depthField($depthField)
184
    {
185 3
        $this->depthField = $depthField;
186
187 3
        return $this;
188
    }
189
190
    /**
191
     * Target collection for the $graphLookup operation to search, recursively
192
     * matching the connectFromField to the connectToField.
193
     *
194
     * The from collection cannot be sharded and must be in the same database as
195
     * any other collections used in the operation.
196
     *
197
     * @param string $from
198
     *
199
     * @return $this
200
     */
201 11 View Code Duplication
    public function from($from)
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...
202
    {
203
        // $from can either be
204
        // a) a field name indicating a reference to a different document. Currently, only REFERENCE_STORE_AS_ID is supported
205
        // b) a Class name
206
        // c) a collection name
207
        // In cases b) and c) the local and foreign fields need to be filled
208 11
        if ($this->class->hasReference($from)) {
209 6
            return $this->fromReference($from);
210
        }
211
212
        // Check if mapped class with given name exists
213
        try {
214 5
            $this->targetClass = $this->dm->getClassMetadata($from);
215 4
        } catch (BaseMappingException $e) {
216 4
            $this->from = $from;
217 4
            return $this;
218
        }
219
220 1
        if ($this->targetClass->isSharded()) {
221 1
            throw MappingException::cannotUseShardedCollectionInLookupStages($this->targetClass->name);
222
        }
223
224
        $this->from = $this->targetClass->getCollection();
225
        return $this;
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231 9
    public function getExpression()
232
    {
233
        $graphLookup = [
234 9
            'from' => $this->from,
235 9
            'startWith' => $this->convertExpression($this->startWith),
236 9
            'connectFromField' => $this->connectFromField,
237 9
            'connectToField' => $this->connectToField,
238 9
            'as' => $this->as,
239 9
            'restrictSearchWithMatch' => $this->restrictSearchWithMatch->getExpression(),
240
        ];
241
242 9
        foreach (['maxDepth', 'depthField'] as $field) {
243 9
            if ($this->$field === null) {
244 6
                continue;
245
            }
246
247 3
            $graphLookup[$field] = $this->$field;
248
        }
249
250 9
        return ['$graphLookup' => $graphLookup];
251
    }
252
253
    /**
254
     * Non-negative integral number specifying the maximum recursion depth.
255
     *
256
     * @param int $maxDepth
257
     *
258
     * @return $this
259
     */
260 3
    public function maxDepth($maxDepth)
261
    {
262 3
        $this->maxDepth = $maxDepth;
263
264 3
        return $this;
265
    }
266
267
    /**
268
     * A document specifying additional conditions for the recursive search.
269
     *
270
     * @return GraphLookup\Match
271
     */
272 1
    public function restrictSearchWithMatch()
273
    {
274 1
        return $this->restrictSearchWithMatch;
275
    }
276
277
    /**
278
     * Expression that specifies the value of the connectFromField with which to
279
     * start the recursive search.
280
     *
281
     * Optionally, startWith may be array of values, each of which is
282
     * individually followed through the traversal process.
283
     *
284
     * @param string|array|Expr $expression
285
     *
286
     * @return $this
287
     */
288 10
    public function startWith($expression)
289
    {
290 10
        $this->startWith = $expression;
291
292 10
        return $this;
293
    }
294
295
    /**
296
     * @param string $fieldName
297
     * @return $this
298
     * @throws MappingException
299
     */
300 6
    private function fromReference($fieldName)
301
    {
302 6
        if ( ! $this->class->hasReference($fieldName)) {
303
            MappingException::referenceMappingNotFound($this->class->name, $fieldName);
304
        }
305
306 6
        $referenceMapping = $this->class->getFieldMapping($fieldName);
307 6
        $this->targetClass = $this->dm->getClassMetadata($referenceMapping['targetDocument']);
308 6
        if ($this->targetClass->isSharded()) {
309
            throw MappingException::cannotUseShardedCollectionInLookupStages($this->targetClass->name);
310
        }
311
312 6
        $this->from = $this->targetClass->getCollection();
313
314 6
        $referencedFieldName = $this->getReferencedFieldName($fieldName, $referenceMapping);
315
316 6
        if ($referenceMapping['isOwningSide']) {
317
            $this
318 5
                ->startWith('$' . $referencedFieldName)
319 5
                ->connectToField('_id');
320
        } else {
321
            $this
322 1
                ->startWith('$' . $referencedFieldName)
323 1
                ->connectToField('_id');
324
        }
325
326
        // A self-reference indicates that we can also fill the "connectFromField" accordingly
327 6
        if ($this->targetClass->name === $this->class->name) {
328 3
            $this->connectFromField($referencedFieldName);
329
        }
330
331 6
        return $this;
332
    }
333
334 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...
335
    {
336 9
        if (is_array($expression)) {
337
            return array_map([$this, 'convertExpression'], $expression);
338 9
        } elseif (is_string($expression) && substr($expression, 0, 1) === '$') {
339 9
            return '$' . $this->getDocumentPersister($this->class)->prepareFieldName(substr($expression, 1));
340
        } else {
341
            return Type::convertPHPToDatabaseValue(Expr::convertExpression($expression));
342
        }
343
    }
344
345 10
    private function convertTargetFieldName($fieldName)
346
    {
347 10
        if (is_array($fieldName)) {
348
            return array_map([$this, 'convertTargetFieldName'], $fieldName);
349
        }
350
351 10
        if ( ! $this->targetClass) {
352 4
            return $fieldName;
353
        }
354
355 6
        return $this->getDocumentPersister($this->targetClass)->prepareFieldName($fieldName);
356
    }
357
358
    /**
359
     * @param ClassMetadata $class
360
     * @return \Doctrine\ODM\MongoDB\Persisters\DocumentPersister
361
     */
362 10
    private function getDocumentPersister(ClassMetadata $class)
363
    {
364 10
        return $this->dm->getUnitOfWork()->getDocumentPersister($class->name);
365
    }
366
367 6
    private function getReferencedFieldName($fieldName, array $mapping)
368
    {
369 6
        if ( ! $mapping['isOwningSide']) {
370 1 View Code Duplication
            if (isset($mapping['repositoryMethod']) || ! isset($mapping['mappedBy'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
371
                throw MappingException::repositoryMethodLookupNotAllowed($this->class->name, $fieldName);
372
            }
373
374 1
            $mapping = $this->targetClass->getFieldMapping($mapping['mappedBy']);
375
        }
376
377 6 View Code Duplication
        switch ($mapping['storeAs']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
378 6
            case ClassMetadataInfo::REFERENCE_STORE_AS_ID:
379 3
            case ClassMetadataInfo::REFERENCE_STORE_AS_REF:
380 6
                return ClassMetadataInfo::getReferenceFieldName($mapping['storeAs'], $mapping['name']);
381
                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...
382
383
            default:
384
                throw MappingException::cannotLookupDbRefReference($this->class->name, $fieldName);
385
        }
386
    }
387
}
388