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