Completed
Pull Request — master (#1263)
by Andreas
14:36
created

ReferencePrimer   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 239
Duplicated Lines 1.26 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 0%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 38
c 3
b 1
f 0
lcom 1
cbo 7
dl 3
loc 239
ccs 0
cts 118
cp 0
rs 8.3999

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 3 20 3
C primeReferences() 0 54 16
C parseDotSyntaxForPrimer() 0 63 12
C addManyReferences() 0 26 7

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\Query;
21
22
use Doctrine\ODM\MongoDB\DocumentManager;
23
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
24
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
25
use Doctrine\ODM\MongoDB\PersistentCollection;
26
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
27
use Doctrine\ODM\MongoDB\Proxy\Proxy;
28
use Doctrine\ODM\MongoDB\UnitOfWork;
29
30
/**
31
 * The ReferencePrimer is responsible for priming reference relationships.
32
 *
33
 * Priming a field mapped as either reference-one or reference-many will load
34
 * the referenced document(s) eagerly and avoid individual lazy loading through
35
 * proxy object initialization.
36
 *
37
 * Priming can only be used for the owning side side of a relationship, since
38
 * the referenced identifiers are not immediately available on an inverse side.
39
 *
40
 * @since  1.0
41
 */
42
class ReferencePrimer
43
{
44
    /**
45
     * The default primer Closure.
46
     *
47
     * @var \Closure
48
     */
49
    private $defaultPrimer;
50
51
    /**
52
     * The DocumentManager instance.
53
     *
54
     * @var DocumentManager $dm
55
     */
56
    private $dm;
57
58
    /**
59
     * The UnitOfWork instance.
60
     *
61
     * @var UnitOfWork
62
     */
63
    private $uow;
64
65
    /**
66
     * Initializes this instance with the specified document manager and unit of work.
67
     *
68
     * @param DocumentManager $dm Document manager.
69
     * @param UnitOfWork $uow Unit of work.
70
     */
71
    public function __construct(DocumentManager $dm, UnitOfWork $uow)
72
    {
73
        $this->dm = $dm;
74
        $this->uow = $uow;
75
76
        $this->defaultPrimer = function(DocumentManager $dm, ClassMetadata $class, array $ids, array $hints) {
77
            $qb = $dm->createQueryBuilder($class->name)
78
                ->field($class->identifier)->in($ids);
79
80
            if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
81
                $qb->slaveOkay(true);
82
            }
83
84 View Code Duplication
            if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
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...
85
                $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
86
            }
87
88
            $qb->getQuery()->execute()->toArray(false);
89
        };
90
    }
91
92
93
    /**
94
     * Prime references within a mapped field of one or more documents.
95
     *
96
     * If a $primer callable is provided, it should have the same signature as
97
     * the default primer defined in the constructor. If $primer is not
98
     * callable, the default primer will be used.
99
     *
100
     * @param ClassMetadata      $class     Class metadata for the document
101
     * @param array|\Traversable $documents Documents containing references to prime
102
     * @param string             $fieldName Field name containing references to prime
103
     * @param array              $hints     UnitOfWork hints for priming queries
104
     * @param callable           $primer    Optional primer callable
105
     * @throws \InvalidArgumentException If the mapped field is not the owning
106
     *                                   side of a reference relationship.
107
     * @throws \InvalidArgumentException If $primer is not callable
108
     * @throws \LogicException If the mapped field is a simple reference and is
109
     *                         missing a target document class.
110
     */
111
    public function primeReferences(ClassMetadata $class, $documents, $fieldName, array $hints = array(), $primer = null)
112
    {
113
        $data = $this->parseDotSyntaxForPrimer($fieldName, $class, $documents);
114
        $mapping = $data['mapping'];
115
        $fieldName = $data['fieldName'];
116
        $class = $data['class'];
117
        $documents = $data['documents'];
118
119
        /* Inverse-side references would need to be populated before we can
120
         * collect references to be primed. This is not supported.
121
         */
122
        if ( ! isset($mapping['reference']) || ! $mapping['isOwningSide']) {
123
            throw new \InvalidArgumentException(sprintf('Field "%s" is not the owning side of a reference relationship in class "%s"', $fieldName, $class->name));
124
        }
125
126
        /* Simple reference require a target document class so we can construct
127
         * the priming query.
128
         */
129
        if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID && empty($mapping['targetDocument'])) {
130
            throw new \LogicException(sprintf('Field "%s" is a simple reference without a target document class in class "%s"', $fieldName, $class->name));
131
        }
132
133
        if ($primer !== null && ! is_callable($primer)) {
134
            throw new \InvalidArgumentException('$primer is not callable');
135
        }
136
137
        $primer = $primer ?: $this->defaultPrimer;
138
        $groupedIds = array();
139
140
        /* @var $document PersistentCollectionInterface */
141
        foreach ($documents as $document) {
142
            $fieldValue = $class->getFieldValue($document, $fieldName);
143
144
            /* The field will need to be either a Proxy (reference-one) or
145
             * PersistentCollection (reference-many) in order to prime anything.
146
             */
147
            if ( ! is_object($fieldValue)) {
148
                continue;
149
            }
150
151
            if ($mapping['type'] === 'one' && $fieldValue instanceof Proxy && ! $fieldValue->__isInitialized()) {
152
                $refClass = $this->dm->getClassMetadata(get_class($fieldValue));
153
                $id = $this->uow->getDocumentIdentifier($fieldValue);
154
                $groupedIds[$refClass->name][serialize($id)] = $id;
155
            } elseif ($mapping['type'] == 'many' && $fieldValue instanceof PersistentCollectionInterface) {
156
                $this->addManyReferences($fieldValue, $groupedIds);
157
            }
158
        }
159
160
        foreach ($groupedIds as $className => $ids) {
161
            $refClass = $this->dm->getClassMetadata($className);
162
            call_user_func($primer, $this->dm, $refClass, array_values($ids), $hints);
163
        }
164
    }
165
166
    /**
167
     * If you are priming references inside an embedded document you'll need to parse the dot syntax.
168
     * This method will traverse through embedded documents to find the reference to prime.
169
     * However this method will not traverse through multiple layers of references.
170
     * I.e. you can prime this: myDocument.embeddedDocument.embeddedDocuments.embeddedDocuments.referencedDocument(s)
171
     * ... but you cannot prime this: myDocument.embeddedDocument.referencedDocuments.referencedDocument(s)
172
     * This addresses Issue #624.
173
     *
174
     * @param string             $fieldName
175
     * @param ClassMetadata      $class
176
     * @param array|\Traversable $documents
177
     * @param array              $mapping
178
     * @return array
179
     */
180
    private function parseDotSyntaxForPrimer($fieldName, $class, $documents, $mapping = null)
181
    {
182
        // Recursion passthrough:
183
        if ($mapping != null) {
184
            return array('fieldName' => $fieldName, 'class' => $class, 'documents' => $documents, 'mapping' => $mapping);
185
        }
186
187
        // Gather mapping data:
188
        $e = explode('.', $fieldName);
189
190
        if ( ! isset($class->fieldMappings[$e[0]])) {
191
            throw new \InvalidArgumentException(sprintf('Field %s cannot be further parsed for priming because it is unmapped.', $fieldName));
192
        }
193
194
        $mapping = $class->fieldMappings[$e[0]];
195
        $e[0] = $mapping['name'];
196
197
        // Case of embedded document(s) to recurse through:
198
        if ( ! isset($mapping['reference'])) {
199
            if (empty($mapping['embedded'])) {
200
                throw new \InvalidArgumentException(sprintf('Field "%s" of fieldName "%s" is not an embedded document, therefore no children can be primed. Aborting. This feature does not support traversing nested referenced documents at this time.', $e[0], $fieldName));
201
            }
202
203
            if ( ! isset($mapping['targetDocument'])) {
204
                throw new \InvalidArgumentException(sprintf('No target document class has been specified for this embedded document. However, targetDocument mapping must be specified in order for prime to work on fieldName "%s" for mapping of field "%s".', $fieldName, $mapping['fieldName']));
205
            }
206
207
            $childDocuments = array();
208
209
            foreach ($documents as $document) {
210
                $fieldValue = $class->getFieldValue($document, $e[0]);
211
212
                if ($fieldValue instanceof PersistentCollectionInterface) {
213
                    foreach ($fieldValue as $elemDocument) {
214
                        array_push($childDocuments, $elemDocument);
215
                    }
216
                } else {
217
                    array_push($childDocuments,$fieldValue);
218
                }
219
            }
220
221
            array_shift($e);
222
223
            $childClass = $this->dm->getClassMetadata($mapping['targetDocument']);
224
225
            if ( ! $childClass->hasField($e[0])) {
226
                throw new \InvalidArgumentException(sprintf('Field to prime must exist in embedded target document. Reference fieldName "%s" for mapping of target document class "%s".', $fieldName, $mapping['targetDocument']));
227
            }
228
229
            $childFieldName = implode('.',$e);
230
231
            return $this->parseDotSyntaxForPrimer($childFieldName, $childClass, $childDocuments);
232
        }
233
234
        // Case of reference(s) to prime:
235
        if ($mapping['reference']) {
236
            if (count($e) > 1) {
237
                throw new \InvalidArgumentException(sprintf('Cannot prime more than one layer deep but field "%s" is a reference and has children in fieldName "%s".', $e[0], $fieldName));
238
            }
239
240
            return array('fieldName' => $fieldName, 'class' => $class, 'documents' => $documents, 'mapping' => $mapping);
241
        }
242
    }
243
244
    /**
245
     * Adds identifiers from a PersistentCollection to $groupedIds.
246
     *
247
     * If the relation contains simple references, the mapping is assumed to
248
     * have a target document class defined. Without that, there is no way to
249
     * infer the class of the referenced documents.
250
     *
251
     * @param PersistentCollectionInterface $persistentCollection
252
     * @param array                $groupedIds
253
     */
254
    private function addManyReferences(PersistentCollectionInterface $persistentCollection, array &$groupedIds)
255
    {
256
        $mapping = $persistentCollection->getMapping();
257
258
        if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
259
            $className = $mapping['targetDocument'];
260
            $class = $this->dm->getClassMetadata($className);
261
        }
262
263
        foreach ($persistentCollection->getMongoData() as $reference) {
264
            if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
265
                $id = $reference;
266
            } else {
267
                $id = $reference['$id'];
268
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
269
                $class = $this->dm->getClassMetadata($className);
270
            }
271
272
            $document = $this->uow->tryGetById($id, $class);
0 ignored issues
show
Bug introduced by
The variable $class does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
273
274
            if ( ! $document || ($document instanceof Proxy && ! $document->__isInitialized())) {
275
                $id = $class->getPHPIdentifierValue($id);
276
                $groupedIds[$className][serialize($id)] = $id;
0 ignored issues
show
Bug introduced by
The variable $className does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
277
            }
278
        }
279
    }
280
}
281