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

ODM/MongoDB/Aggregation/Stage/GraphLookup.php (5 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 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
Documentation Bug introduced by
It seems like $this->dm->getClassMetadata($from) 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...
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

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
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
Documentation Bug introduced by
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

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
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

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
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