Completed
Pull Request — master (#1716)
by Maciej
10:47
created

ReferencePrimer::primeReferences()   C

Complexity

Conditions 16
Paths 23

Size

Total Lines 54
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 16.0931

Importance

Changes 0
Metric Value
dl 0
loc 54
ccs 26
cts 28
cp 0.9286
rs 6.6856
c 0
b 0
f 0
cc 16
eloc 27
nc 23
nop 5
crap 16.0931

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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