Completed
Push — master ( a8fe50...bce26f )
by Maciej
13s
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 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 22
    public function __construct(DocumentManager $dm, UnitOfWork $uow)
60
    {
61 22
        $this->dm = $dm;
62 22
        $this->uow = $uow;
63
64
        $this->defaultPrimer = function (DocumentManager $dm, ClassMetadata $class, array $ids, array $hints): void {
65 14
            $qb = $dm->createQueryBuilder($class->name)
66 14
                ->field($class->identifier)->in($ids);
67
68 14
            if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
69
                $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
70
            }
71
72 14
            $qb->getQuery()->execute()->toArray(false);
73 14
        };
74 22
    }
75
76
77
    /**
78
     * Prime references within a mapped field of one or more documents.
79
     *
80
     * If a $primer callable is provided, it should have the same signature as
81
     * the default primer defined in the constructor. If $primer is not
82
     * callable, the default primer will be used.
83
     *
84
     * @param ClassMetadata      $class     Class metadata for the document
85
     * @param array|\Traversable $documents Documents containing references to prime
86
     * @param string             $fieldName Field name containing references to prime
87
     * @param array              $hints     UnitOfWork hints for priming queries
88
     * @param callable           $primer    Optional primer callable
89
     * @throws \InvalidArgumentException If the mapped field is not the owning
90
     *                                   side of a reference relationship.
91
     * @throws \InvalidArgumentException If $primer is not callable.
92
     * @throws \LogicException If the mapped field is a simple reference and is
93
     *                         missing a target document class.
94
     */
95 22
    public function primeReferences(ClassMetadata $class, $documents, string $fieldName, array $hints = [], ?callable $primer = null): void
96
    {
97 22
        $data = $this->parseDotSyntaxForPrimer($fieldName, $class, $documents);
98 21
        $mapping = $data['mapping'];
99 21
        $fieldName = $data['fieldName'];
100 21
        $class = $data['class'];
101 21
        $documents = $data['documents'];
102
103
        /* Inverse-side references would need to be populated before we can
104
         * collect references to be primed. This is not supported.
105
         */
106 21
        if (! isset($mapping['reference']) || ! $mapping['isOwningSide']) {
107 1
            throw new \InvalidArgumentException(sprintf('Field "%s" is not the owning side of a reference relationship in class "%s"', $fieldName, $class->name));
108
        }
109
110
        /* Simple reference require a target document class so we can construct
111
         * the priming query.
112
         */
113 20
        if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID && empty($mapping['targetDocument'])) {
114
            throw new \LogicException(sprintf('Field "%s" is a simple reference without a target document class in class "%s"', $fieldName, $class->name));
115
        }
116
117 20
        if ($primer !== null && ! is_callable($primer)) {
118
            throw new \InvalidArgumentException('$primer is not callable');
119
        }
120
121 20
        $primer = $primer ?: $this->defaultPrimer;
122 20
        $groupedIds = [];
123
124
        /** @var PersistentCollectionInterface $document */
125 20
        foreach ($documents as $document) {
126 20
            $fieldValue = $class->getFieldValue($document, $fieldName);
127
128
            /* The field will need to be either a Proxy (reference-one) or
129
             * PersistentCollection (reference-many) in order to prime anything.
130
             */
131 20
            if (! is_object($fieldValue)) {
132 3
                continue;
133
            }
134
135 17
            if ($mapping['type'] === 'one' && $fieldValue instanceof Proxy && ! $fieldValue->__isInitialized()) {
136 12
                $refClass = $this->dm->getClassMetadata(get_class($fieldValue));
137 12
                $id = $this->uow->getDocumentIdentifier($fieldValue);
0 ignored issues
show
$fieldValue is of type object<Doctrine\ODM\MongoDB\Proxy\Proxy>, but the function expects a object<Doctrine\ODM\MongoDB\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
138 12
                $groupedIds[$refClass->name][serialize($id)] = $id;
139 10
            } elseif ($mapping['type'] === 'many' && $fieldValue instanceof PersistentCollectionInterface) {
140 17
                $this->addManyReferences($fieldValue, $groupedIds);
141
            }
142
        }
143
144 20
        foreach ($groupedIds as $className => $ids) {
145 16
            $refClass = $this->dm->getClassMetadata($className);
146 16
            call_user_func($primer, $this->dm, $refClass, array_values($ids), $hints);
147
        }
148 20
    }
149
150
    /**
151
     * If you are priming references inside an embedded document you'll need to parse the dot syntax.
152
     * This method will traverse through embedded documents to find the reference to prime.
153
     * However this method will not traverse through multiple layers of references.
154
     * I.e. you can prime this: myDocument.embeddedDocument.embeddedDocuments.embeddedDocuments.referencedDocument(s)
155
     * ... but you cannot prime this: myDocument.embeddedDocument.referencedDocuments.referencedDocument(s)
156
     * This addresses Issue #624.
157
     *
158
     * @param array|\Traversable $documents
159
     */
160 22
    private function parseDotSyntaxForPrimer(string $fieldName, ClassMetadata $class, $documents, ?array $mapping = null): array
161
    {
162
        // Recursion passthrough:
163 22
        if ($mapping !== null) {
164
            return ['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 = [];
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 ['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 10
    private function addManyReferences(PersistentCollectionInterface $persistentCollection, array &$groupedIds): void
232
    {
233 10
        $mapping = $persistentCollection->getMapping();
234 10
        $class = null;
235 10
        $className = null;
236
237 10
        if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) {
238 2
            $className = $mapping['targetDocument'];
239 2
            $class = $this->dm->getClassMetadata($className);
240
        }
241
242 10
        foreach ($persistentCollection->getMongoData() as $reference) {
243 10
            $id = ClassMetadata::getReferenceId($reference, $mapping['storeAs']);
244
245 10
            if ($mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID) {
246 9
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
247 9
                $class = $this->dm->getClassMetadata($className);
248
            }
249
250 10
            $document = $this->uow->tryGetById($id, $class);
251
252 10
            if ($document && ! (($document instanceof Proxy && ! $document->__isInitialized()))) {
253 1
                continue;
254
            }
255
256 9
            $id = $class->getPHPIdentifierValue($id);
257 9
            $groupedIds[$className][serialize($id)] = $id;
258
        }
259 10
    }
260
}
261