Completed
Push — master ( 87aba0...0e8729 )
by Andreas
25s queued 16s
created

lib/Doctrine/ODM/MongoDB/Query/ReferencePrimer.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB\Query;
21
22
use Doctrine\ODM\MongoDB\DocumentManager;
23
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
24
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
25
use Doctrine\ODM\MongoDB\PersistentCollection;
26
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
27
use Doctrine\ODM\MongoDB\Proxy\Proxy;
28
use Doctrine\ODM\MongoDB\UnitOfWork;
29
30
/**
31
 * The ReferencePrimer is responsible for priming reference relationships.
32
 *
33
 * Priming a field mapped as either reference-one or reference-many will load
34
 * the referenced document(s) eagerly and avoid individual lazy loading through
35
 * proxy object initialization.
36
 *
37
 * Priming can only be used for the owning side side of a relationship, since
38
 * the referenced identifiers are not immediately available on an inverse side.
39
 *
40
 * @since  1.0
41
 */
42
class ReferencePrimer
43
{
44
    /**
45
     * The default primer Closure.
46
     *
47
     * @var \Closure
48
     */
49
    private $defaultPrimer;
50
51
    /**
52
     * The DocumentManager instance.
53
     *
54
     * @var DocumentManager $dm
55
     */
56
    private $dm;
57
58
    /**
59
     * The UnitOfWork instance.
60
     *
61
     * @var UnitOfWork
62
     */
63
    private $uow;
64
65
    /**
66
     * Initializes this instance with the specified document manager and unit of work.
67
     *
68
     * @param DocumentManager $dm Document manager.
69
     * @param UnitOfWork $uow Unit of work.
70
     */
71 24
    public function __construct(DocumentManager $dm, UnitOfWork $uow)
72
    {
73 24
        $this->dm = $dm;
74 24
        $this->uow = $uow;
75
76 14
        $this->defaultPrimer = function(DocumentManager $dm, ClassMetadata $class, array $ids, array $hints) {
77 14
            $qb = $dm->createQueryBuilder($class->name)
78 14
                ->field($class->identifier)->in($ids);
79
80 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...
81
                $qb->slaveOkay(true);
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\ODM\MongoDB\Query\Builder::slaveOkay() has been deprecated with message: in version 1.2 - use setReadPreference instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

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