Completed
Push — master ( 7b9f4b...1cd743 )
by Andreas
13s queued 10s
created

ODM/MongoDB/Aggregation/Stage/GraphLookup.php (1 issue)

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 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);
160 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...
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);
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']);
247 6
        if ($this->targetClass->isSharded()) {
248
            throw MappingException::cannotUseShardedCollectionInLookupStages($this->targetClass->name);
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) {
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