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); |
|
0 ignored issues
–
show
|
|||
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); |
|
0 ignored issues
–
show
The method
getPHPIdentifierValue() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata . Did you maybe mean getIdentifierValues() ?
This check marks calls to methods that do not seem to exist on an object. This is most likely the result of a method being renamed without all references to it being renamed likewise.
Loading history...
|
|||
273 | 9 | $groupedIds[$className][serialize($id)] = $id; |
|
274 | } |
||
275 | 10 | } |
|
276 | } |
||
277 |
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: