Completed
Push — master ( bb3219...cc367c )
by Andreas
15:44
created

ODM/MongoDB/Aggregation/Stage/GraphLookup.php (3 issues)

Upgrade to new PHP Analysis Engine

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 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, string $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 9
    public function alias(string $alias) : self
77
    {
78 9
        $this->as = $alias;
79
80 9
        return $this;
81
    }
82
83
    /**
84
     * Field name whose value $graphLookup uses to recursively match against the
85
     * connectToField of other documents in the collection.
86
     *
87
     * Optionally, connectFromField may be an array of field names, each of
88
     * which is individually followed through the traversal process.
89
     */
90 10
    public function connectFromField(string $connectFromField) : self
91
    {
92
        // No targetClass mapping - simply use field name as is
93 10
        if (! $this->targetClass) {
94 4
            $this->connectFromField = $connectFromField;
95 4
            return $this;
96
        }
97
98
        // connectFromField doesn't have to be a reference - in this case, just convert the field name
99 6
        if (! $this->targetClass->hasReference($connectFromField)) {
100 2
            $this->connectFromField = $this->convertTargetFieldName($connectFromField);
101 2
            return $this;
102
        }
103
104
        // connectFromField is a reference - do a sanity check
105 4
        $referenceMapping = $this->targetClass->getFieldMapping($connectFromField);
106 4
        if ($referenceMapping['targetDocument'] !== $this->targetClass->name) {
107 1
            throw MappingException::connectFromFieldMustReferenceSameDocument($connectFromField);
108
        }
109
110 3
        $this->connectFromField = $this->getReferencedFieldName($connectFromField, $referenceMapping);
111 3
        return $this;
112
    }
113
114
    /**
115
     * Field name in other documents against which to match the value of the
116
     * field specified by the connectFromField parameter.
117
     */
118 10
    public function connectToField(string $connectToField) : self
119
    {
120 10
        $this->connectToField = $this->convertTargetFieldName($connectToField);
121 10
        return $this;
122
    }
123
124
    /**
125
     * Name of the field to add to each traversed document in the search path.
126
     *
127
     * The value of this field is the recursion depth for the document,
128
     * represented as a NumberLong. Recursion depth value starts at zero, so the
129
     * first lookup corresponds to zero depth.
130
     */
131 3
    public function depthField(string $depthField) : self
132
    {
133 3
        $this->depthField = $depthField;
134
135 3
        return $this;
136
    }
137
138
    /**
139
     * Target collection for the $graphLookup operation to search, recursively
140
     * matching the connectFromField to the connectToField.
141
     *
142
     * The from collection cannot be sharded and must be in the same database as
143
     * any other collections used in the operation.
144
     */
145 11
    public function from(string $from) : self
146
    {
147
        // $from can either be
148
        // a) a field name indicating a reference to a different document. Currently, only REFERENCE_STORE_AS_ID is supported
149
        // b) a Class name
150
        // c) a collection name
151
        // In cases b) and c) the local and foreign fields need to be filled
152 11
        if ($this->class->hasReference($from)) {
153 6
            return $this->fromReference($from);
154
        }
155
156
        // Check if mapped class with given name exists
157
        try {
158 5
            $this->targetClass = $this->dm->getClassMetadata($from);
159 4
        } catch (BaseMappingException $e) {
0 ignored issues
show
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...
160 4
            $this->from = $from;
161 4
            return $this;
162
        }
163
164 1
        if ($this->targetClass->isSharded()) {
165 1
            throw MappingException::cannotUseShardedCollectionInLookupStages($this->targetClass->name);
166
        }
167
168
        $this->from = $this->targetClass->getCollection();
169
        return $this;
170
    }
171
172
    /**
173
     * {@inheritdoc}
174
     */
175 9
    public function getExpression() : array
176
    {
177 9
        $restrictSearchWithMatch = $this->restrictSearchWithMatch->getExpression() ?: (object) [];
178
179
        $graphLookup = [
180 9
            'from' => $this->from,
181 9
            'startWith' => $this->convertExpression($this->startWith),
182 9
            'connectFromField' => $this->connectFromField,
183 9
            'connectToField' => $this->connectToField,
184 9
            'as' => $this->as,
185 9
            'restrictSearchWithMatch' => $restrictSearchWithMatch,
186 9
            'maxDepth' => $this->maxDepth,
187 9
            'depthField' => $this->depthField,
188
        ];
189
190 9
        foreach (['maxDepth', 'depthField'] as $field) {
191 9
            if ($graphLookup[$field] !== null) {
192 3
                continue;
193
            }
194
195 6
            unset($graphLookup[$field]);
196
        }
197
198 9
        return ['$graphLookup' => $graphLookup];
199
    }
200
201
    /**
202
     * Non-negative integral number specifying the maximum recursion depth.
203
     */
204 3
    public function maxDepth(int $maxDepth) : self
205
    {
206 3
        $this->maxDepth = $maxDepth;
207
208 3
        return $this;
209
    }
210
211
    /**
212
     * A document specifying additional conditions for the recursive search.
213
     */
214 1
    public function restrictSearchWithMatch() : GraphLookup\Match
215
    {
216 1
        return $this->restrictSearchWithMatch;
217
    }
218
219
    /**
220
     * Expression that specifies the value of the connectFromField with which to
221
     * start the recursive search.
222
     *
223
     * Optionally, startWith may be array of values, each of which is
224
     * individually followed through the traversal process.
225
     *
226
     * @param string|array|Expr $expression
227
     */
228 10
    public function startWith($expression) : self
229
    {
230 10
        $this->startWith = $expression;
231
232 10
        return $this;
233
    }
234
235
    /**
236
     * @throws MappingException
237
     */
238 6
    private function fromReference(string $fieldName) : self
239
    {
240 6
        if (! $this->class->hasReference($fieldName)) {
241
            MappingException::referenceMappingNotFound($this->class->name, $fieldName);
242
        }
243
244 6
        $referenceMapping  = $this->class->getFieldMapping($fieldName);
245 6
        $this->targetClass = $this->dm->getClassMetadata($referenceMapping['targetDocument']);
246 6
        if ($this->targetClass->isSharded()) {
247
            throw MappingException::cannotUseShardedCollectionInLookupStages($this->targetClass->name);
248
        }
249
250 6
        $this->from = $this->targetClass->getCollection();
251
252 6
        $referencedFieldName = $this->getReferencedFieldName($fieldName, $referenceMapping);
253
254 6
        if ($referenceMapping['isOwningSide']) {
255
            $this
256 5
                ->startWith('$' . $referencedFieldName)
257 5
                ->connectToField('_id');
258
        } else {
259
            $this
260 1
                ->startWith('$' . $referencedFieldName)
261 1
                ->connectToField('_id');
262
        }
263
264
        // A self-reference indicates that we can also fill the "connectFromField" accordingly
265 6
        if ($this->targetClass->name === $this->class->name) {
266 3
            $this->connectFromField($referencedFieldName);
267
        }
268
269 6
        return $this;
270
    }
271
272 9 View Code Duplication
    private function convertExpression($expression)
0 ignored issues
show
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...
273
    {
274 9
        if (is_array($expression)) {
275
            return array_map([$this, 'convertExpression'], $expression);
276 9
        } elseif (is_string($expression) && substr($expression, 0, 1) === '$') {
277 9
            return '$' . $this->getDocumentPersister($this->class)->prepareFieldName(substr($expression, 1));
278
        }
279
280
        return Type::convertPHPToDatabaseValue(Expr::convertExpression($expression));
281
    }
282
283 10
    private function convertTargetFieldName($fieldName)
284
    {
285 10
        if (is_array($fieldName)) {
286
            return array_map([$this, 'convertTargetFieldName'], $fieldName);
287
        }
288
289 10
        if (! $this->targetClass) {
290 4
            return $fieldName;
291
        }
292
293 6
        return $this->getDocumentPersister($this->targetClass)->prepareFieldName($fieldName);
294
    }
295
296 10
    private function getDocumentPersister(ClassMetadata $class) : DocumentPersister
297
    {
298 10
        return $this->dm->getUnitOfWork()->getDocumentPersister($class->name);
299
    }
300
301 6
    private function getReferencedFieldName(string $fieldName, array $mapping) : string
302
    {
303 6
        if (! $mapping['isOwningSide']) {
304 1
            if (isset($mapping['repositoryMethod']) || ! isset($mapping['mappedBy'])) {
305
                throw MappingException::repositoryMethodLookupNotAllowed($this->class->name, $fieldName);
306
            }
307
308 1
            $mapping = $this->targetClass->getFieldMapping($mapping['mappedBy']);
309
        }
310
311 6
        switch ($mapping['storeAs']) {
312
            case ClassMetadata::REFERENCE_STORE_AS_ID:
313
            case ClassMetadata::REFERENCE_STORE_AS_REF:
314 6
                return ClassMetadata::getReferenceFieldName($mapping['storeAs'], $mapping['name']);
315
                break;
0 ignored issues
show
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...
316
317
            default:
318
                throw MappingException::cannotLookupDbRefReference($this->class->name, $fieldName);
319
        }
320
    }
321
}
322