Completed
Pull Request — master (#1803)
by Andreas
15:44
created

ReferencePrimer::__construct()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2.0023

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 11
cts 12
cp 0.9167
rs 9.6666
c 0
b 0
f 0
cc 2
nc 1
nop 2
crap 2.0023
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
            $qb = $dm->createQueryBuilder($class->name)
71 14
                ->field($class->identifier)->in($ids);
72
73 14
            if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
74
                $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
75
            }
76
77 14
            $iterator = $qb->getQuery()->execute();
78 14
            assert($iterator instanceof Iterator);
79 14
            $iterator->toArray();
80 14
        };
81 22
    }
82
83
84
    /**
85
     * Prime references within a mapped field of one or more documents.
86
     *
87
     * If a $primer callable is provided, it should have the same signature as
88
     * the default primer defined in the constructor. If $primer is not
89
     * callable, the default primer will be used.
90
     *
91
     * @param ClassMetadata     $class     Class metadata for the document
92
     * @param array|Traversable $documents Documents containing references to prime
93
     * @param string            $fieldName Field name containing references to prime
94
     * @param array             $hints     UnitOfWork hints for priming queries
95
     * @param callable          $primer    Optional primer callable
96
     *
97
     * @throws InvalidArgumentException If the mapped field is not the owning
98
     *                                   side of a reference relationship.
99
     * @throws InvalidArgumentException If $primer is not callable.
100
     * @throws LogicException If the mapped field is a simple reference and is
101
     *                         missing a target document class.
102
     */
103 22
    public function primeReferences(ClassMetadata $class, $documents, string $fieldName, array $hints = [], ?callable $primer = null) : void
104
    {
105 22
        $data      = $this->parseDotSyntaxForPrimer($fieldName, $class, $documents);
106 21
        $mapping   = $data['mapping'];
107 21
        $fieldName = $data['fieldName'];
108 21
        $class     = $data['class'];
109 21
        $documents = $data['documents'];
110
111
        /* Inverse-side references would need to be populated before we can
112
         * collect references to be primed. This is not supported.
113
         */
114 21
        if (! isset($mapping['reference']) || ! $mapping['isOwningSide']) {
115 1
            throw new InvalidArgumentException(sprintf('Field "%s" is not the owning side of a reference relationship in class "%s"', $fieldName, $class->name));
116
        }
117
118
        /* Simple reference require a target document class so we can construct
119
         * the priming query.
120
         */
121 20
        if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID && empty($mapping['targetDocument'])) {
122
            throw new LogicException(sprintf('Field "%s" is an identifier reference without a target document class in class "%s"', $fieldName, $class->name));
123
        }
124
125 20
        if ($primer !== null && ! is_callable($primer)) {
126
            throw new InvalidArgumentException('$primer is not callable');
127
        }
128
129 20
        $primer     = $primer ?: $this->defaultPrimer;
130 20
        $groupedIds = [];
131
132
        /** @var PersistentCollectionInterface $document */
133 20
        foreach ($documents as $document) {
134 20
            $fieldValue = $class->getFieldValue($document, $fieldName);
135
136
            /* The field will need to be either a Proxy (reference-one) or
137
             * PersistentCollection (reference-many) in order to prime anything.
138
             */
139 20
            if (! is_object($fieldValue)) {
140 3
                continue;
141
            }
142
143 17
            if ($mapping['type'] === 'one' && $fieldValue instanceof GhostObjectInterface && ! $fieldValue->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
144 12
                $refClass                                    = $this->dm->getClassMetadata(get_class($fieldValue));
145 12
                $id                                          = $this->uow->getDocumentIdentifier($fieldValue);
146 12
                $groupedIds[$refClass->name][serialize($id)] = $id;
147 10
            } elseif ($mapping['type'] === 'many' && $fieldValue instanceof PersistentCollectionInterface) {
148 17
                $this->addManyReferences($fieldValue, $groupedIds);
149
            }
150
        }
151
152 20
        foreach ($groupedIds as $className => $ids) {
153 16
            $refClass = $this->dm->getClassMetadata($className);
154 16
            call_user_func($primer, $this->dm, $refClass, array_values($ids), $hints);
155
        }
156 20
    }
157
158
    /**
159
     * If you are priming references inside an embedded document you'll need to parse the dot syntax.
160
     * This method will traverse through embedded documents to find the reference to prime.
161
     * However this method will not traverse through multiple layers of references.
162
     * I.e. you can prime this: myDocument.embeddedDocument.embeddedDocuments.embeddedDocuments.referencedDocument(s)
163
     * ... but you cannot prime this: myDocument.embeddedDocument.referencedDocuments.referencedDocument(s)
164
     * This addresses Issue #624.
165
     *
166
     * @param array|Traversable $documents
167
     */
168 22
    private function parseDotSyntaxForPrimer(string $fieldName, ClassMetadata $class, $documents, ?array $mapping = null) : array
169
    {
170
        // Recursion passthrough:
171 22
        if ($mapping !== null) {
172
            return ['fieldName' => $fieldName, 'class' => $class, 'documents' => $documents, 'mapping' => $mapping];
173
        }
174
175
        // Gather mapping data:
176 22
        $e = explode('.', $fieldName);
177
178 22
        if (! isset($class->fieldMappings[$e[0]])) {
179
            throw new InvalidArgumentException(sprintf('Field %s cannot be further parsed for priming because it is unmapped.', $fieldName));
180
        }
181
182 22
        $mapping = $class->fieldMappings[$e[0]];
183 22
        $e[0]    = $mapping['fieldName'];
184
185
        // Case of embedded document(s) to recurse through:
186 22
        if (! isset($mapping['reference'])) {
187 4
            if (empty($mapping['embedded'])) {
188 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));
189
            }
190
191 3
            if (! isset($mapping['targetDocument'])) {
192
                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']));
193
            }
194
195 3
            $childDocuments = [];
196
197 3
            foreach ($documents as $document) {
198 3
                $fieldValue = $class->getFieldValue($document, $e[0]);
199
200 3
                if ($fieldValue instanceof PersistentCollectionInterface) {
201 3
                    foreach ($fieldValue as $elemDocument) {
202 3
                        array_push($childDocuments, $elemDocument);
203
                    }
204
                } else {
205 3
                    array_push($childDocuments, $fieldValue);
206
                }
207
            }
208
209 3
            array_shift($e);
210
211 3
            $childClass = $this->dm->getClassMetadata($mapping['targetDocument']);
212
213 3
            if (! $childClass->hasField($e[0])) {
214
                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']));
215
            }
216
217 3
            $childFieldName = implode('.', $e);
218
219 3
            return $this->parseDotSyntaxForPrimer($childFieldName, $childClass, $childDocuments);
220
        }
221
222
        // Case of reference(s) to prime:
223 21
        if ($mapping['reference']) {
224 21
            if (count($e) > 1) {
225
                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));
226
            }
227
228 21
            return ['fieldName' => $fieldName, 'class' => $class, 'documents' => $documents, 'mapping' => $mapping];
229
        }
230
    }
231
232
    /**
233
     * Adds identifiers from a PersistentCollection to $groupedIds.
234
     *
235
     * If the relation contains simple references, the mapping is assumed to
236
     * have a target document class defined. Without that, there is no way to
237
     * infer the class of the referenced documents.
238
     */
239 10
    private function addManyReferences(PersistentCollectionInterface $persistentCollection, array &$groupedIds) : void
240
    {
241 10
        $mapping   = $persistentCollection->getMapping();
242 10
        $class     = null;
243 10
        $className = null;
244
245 10
        if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) {
246 2
            $className = $mapping['targetDocument'];
247 2
            $class     = $this->dm->getClassMetadata($className);
248
        }
249
250 10
        foreach ($persistentCollection->getMongoData() as $reference) {
251 10
            $id = ClassMetadata::getReferenceId($reference, $mapping['storeAs']);
252
253 10
            if ($mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID) {
254 9
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
255 9
                $class     = $this->dm->getClassMetadata($className);
256
            }
257
258 10
            $document = $this->uow->tryGetById($id, $class);
259
260 10
            if ($document && ! (($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()))) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
261 1
                continue;
262
            }
263
264 9
            $id                                     = $class->getPHPIdentifierValue($id);
265 9
            $groupedIds[$className][serialize($id)] = $id;
266
        }
267 10
    }
268
}
269