Completed
Pull Request — master (#1714)
by Gabriel
09:27
created

ReferencePrimer::addManyReferences()   C

Complexity

Conditions 7
Paths 10

Size

Total Lines 27
Code Lines 16

Duplication

Lines 8
Ratio 29.63 %

Code Coverage

Tests 17
CRAP Score 7

Importance

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

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
253
254 10
            if ( ! $document || ($document instanceof Proxy && ! $document->__isInitialized())) {
255 9
                $id = $class->getPHPIdentifierValue($id);
256 10
                $groupedIds[$className][serialize($id)] = $id;
257
            }
258
        }
259 10
    }
260
}
261