Completed
Push — master ( a8fe50...bce26f )
by Maciej
13s
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
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);
0 ignored issues
show
It seems like $class defined by null on line 234 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...
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