Completed
Pull Request — master (#1714)
by
unknown
10:42
created

ReferencePrimer   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 236
Duplicated Lines 3.39 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 89.66%

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 7
dl 8
loc 236
ccs 78
cts 87
cp 0.8966
rs 8.6
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 16 2
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_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 10
        $class = null;
238 10
        $className = null;
239
240 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...
241 2
            $className = $mapping['targetDocument'];
242 2
            $class = $this->dm->getClassMetadata($className);
243
        }
244
245 10
        foreach ($persistentCollection->getMongoData() as $reference) {
246 10
            $id = ClassMetadataInfo::getReferenceId($reference, $mapping['storeAs']);
247
248 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...
249 9
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
250 9
                $class = $this->dm->getClassMetadata($className);
251
            }
252
253 10
            $document = $this->uow->tryGetById($id, $class);
254
255 10
            if ( ! $document || ($document instanceof Proxy && ! $document->__isInitialized())) {
256 9
                $id = $class->getPHPIdentifierValue($id);
257 10
                $groupedIds[$className][serialize($id)] = $id;
258
            }
259
        }
260 10
    }
261
}
262