Completed
Pull Request — master (#1714)
by
unknown
11:18
created

ReferencePrimer   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 240
Duplicated Lines 3.33 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 88.76%

Importance

Changes 0
Metric Value
wmc 38
lcom 1
cbo 7
dl 8
loc 240
ccs 79
cts 89
cp 0.8876
rs 8.3999
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 20 3
C primeReferences() 0 54 16
C parseDotSyntaxForPrimer() 0 63 12
C addManyReferences() 8 27 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
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_SLAVE_OKAY])) {
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\ODM\MongoDB\Query\Query::HINT_SLAVE_OKAY has been deprecated.

This class constant has been deprecated.

Loading history...
65
                $qb->slaveOkay(true);
0 ignored issues
show
Bug introduced by
The method slaveOkay() does not seem to exist on object<Doctrine\ODM\MongoDB\Query\Builder>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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