Completed
Pull Request — master (#1787)
by Stefano
21:31
created

ReferencePrimer::addManyReferences()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 29

Duplication

Lines 8
Ratio 27.59 %

Code Coverage

Tests 18
CRAP Score 7

Importance

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