Completed
Push — master ( 7b9f4b...1cd743 )
by Andreas
13s queued 10s
created

lib/Doctrine/ODM/MongoDB/Query/ReferencePrimer.php (1 issue)

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

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
224
        }
225
226
        // Case of reference(s) to prime:
227 21
        if ($mapping['reference']) {
228 21
            if (count($e) > 1) {
229
                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));
230
            }
231
232 21
            return ['fieldName' => $fieldName, 'class' => $class, 'documents' => $documents, 'mapping' => $mapping];
233
        }
234
    }
235
236
    /**
237
     * Adds identifiers from a PersistentCollection to $groupedIds.
238
     *
239
     * If the relation contains simple references, the mapping is assumed to
240
     * have a target document class defined. Without that, there is no way to
241
     * infer the class of the referenced documents.
242
     */
243 10
    private function addManyReferences(PersistentCollectionInterface $persistentCollection, array &$groupedIds) : void
244
    {
245 10
        $mapping   = $persistentCollection->getMapping();
246 10
        $class     = null;
247 10
        $className = null;
248
249 10
        if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) {
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
            if ($mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID) {
258 9
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
259 9
                $class     = $this->dm->getClassMetadata($className);
260
            }
261
262 10
            if ($class === null) {
263
                continue;
264
            }
265
266 10
            $document = $this->uow->tryGetById($id, $class);
267
268 10
            if ($document && ! (($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()))) {
269 1
                continue;
270
            }
271
272 9
            $id                                     = $class->getPHPIdentifierValue($id);
273 9
            $groupedIds[$className][serialize($id)] = $id;
274
        }
275 10
    }
276
}
277