These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /* |
||
3 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||
4 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||
5 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||
6 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||
7 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||
8 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||
9 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||
10 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||
11 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||
12 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||
13 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||
14 | * |
||
15 | * This software consists of voluntary contributions made by many individuals |
||
16 | * and is licensed under the MIT license. For more information, see |
||
17 | * <http://www.doctrine-project.org>. |
||
18 | */ |
||
19 | |||
20 | namespace Doctrine\ODM\MongoDB\Query; |
||
21 | |||
22 | use Doctrine\ODM\MongoDB\DocumentManager; |
||
23 | use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; |
||
24 | use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo; |
||
25 | use Doctrine\ODM\MongoDB\PersistentCollection; |
||
26 | use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface; |
||
27 | use Doctrine\ODM\MongoDB\Proxy\Proxy; |
||
28 | use Doctrine\ODM\MongoDB\UnitOfWork; |
||
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 | * @since 1.0 |
||
41 | */ |
||
42 | class ReferencePrimer |
||
43 | { |
||
44 | /** |
||
45 | * The default primer Closure. |
||
46 | * |
||
47 | * @var \Closure |
||
48 | */ |
||
49 | private $defaultPrimer; |
||
50 | |||
51 | /** |
||
52 | * The DocumentManager instance. |
||
53 | * |
||
54 | * @var DocumentManager $dm |
||
55 | */ |
||
56 | private $dm; |
||
57 | |||
58 | /** |
||
59 | * The UnitOfWork instance. |
||
60 | * |
||
61 | * @var UnitOfWork |
||
62 | */ |
||
63 | private $uow; |
||
64 | |||
65 | /** |
||
66 | * Initializes this instance with the specified document manager and unit of work. |
||
67 | * |
||
68 | * @param DocumentManager $dm Document manager. |
||
69 | * @param UnitOfWork $uow Unit of work. |
||
70 | */ |
||
71 | 24 | public function __construct(DocumentManager $dm, UnitOfWork $uow) |
|
72 | { |
||
73 | 24 | $this->dm = $dm; |
|
74 | 24 | $this->uow = $uow; |
|
75 | |||
76 | 14 | $this->defaultPrimer = function(DocumentManager $dm, ClassMetadata $class, array $ids, array $hints) { |
|
77 | 14 | $qb = $dm->createQueryBuilder($class->name) |
|
78 | 14 | ->field($class->identifier)->in($ids); |
|
79 | |||
80 | 14 | if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) { |
|
0 ignored issues
–
show
|
|||
81 | $qb->slaveOkay(true); |
||
0 ignored issues
–
show
The method
Doctrine\ODM\MongoDB\Query\Builder::slaveOkay() has been deprecated with message: in version 1.2 - use setReadPreference instead.
This method has been deprecated. The supplier of the class has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.
Loading history...
|
|||
82 | } |
||
83 | |||
84 | 14 | View Code Duplication | if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) { |
85 | $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]); |
||
86 | } |
||
87 | |||
88 | 14 | $qb->getQuery()->execute()->toArray(false); |
|
89 | 14 | }; |
|
90 | 24 | } |
|
91 | |||
92 | |||
93 | /** |
||
94 | * Prime references within a mapped field of one or more documents. |
||
95 | * |
||
96 | * If a $primer callable is provided, it should have the same signature as |
||
97 | * the default primer defined in the constructor. If $primer is not |
||
98 | * callable, the default primer will be used. |
||
99 | * |
||
100 | * @param ClassMetadata $class Class metadata for the document |
||
101 | * @param array|\Traversable $documents Documents containing references to prime |
||
102 | * @param string $fieldName Field name containing references to prime |
||
103 | * @param array $hints UnitOfWork hints for priming queries |
||
104 | * @param callable $primer Optional primer callable |
||
105 | * @throws \InvalidArgumentException If the mapped field is not the owning |
||
106 | * side of a reference relationship. |
||
107 | * @throws \InvalidArgumentException If $primer is not callable |
||
108 | * @throws \LogicException If the mapped field is a simple reference and is |
||
109 | * missing a target document class. |
||
110 | */ |
||
111 | 21 | public function primeReferences(ClassMetadata $class, $documents, $fieldName, array $hints = array(), $primer = null) |
|
112 | { |
||
113 | 21 | $data = $this->parseDotSyntaxForPrimer($fieldName, $class, $documents); |
|
114 | 20 | $mapping = $data['mapping']; |
|
115 | 20 | $fieldName = $data['fieldName']; |
|
116 | 20 | $class = $data['class']; |
|
117 | 20 | $documents = $data['documents']; |
|
118 | |||
119 | /* Inverse-side references would need to be populated before we can |
||
120 | * collect references to be primed. This is not supported. |
||
121 | */ |
||
122 | 20 | if ( ! isset($mapping['reference']) || ! $mapping['isOwningSide']) { |
|
123 | 1 | throw new \InvalidArgumentException(sprintf('Field "%s" is not the owning side of a reference relationship in class "%s"', $fieldName, $class->name)); |
|
124 | } |
||
125 | |||
126 | /* Simple reference require a target document class so we can construct |
||
127 | * the priming query. |
||
128 | */ |
||
129 | 19 | if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID && empty($mapping['targetDocument'])) { |
|
130 | throw new \LogicException(sprintf('Field "%s" is a simple reference without a target document class in class "%s"', $fieldName, $class->name)); |
||
131 | } |
||
132 | |||
133 | 19 | if ($primer !== null && ! is_callable($primer)) { |
|
134 | throw new \InvalidArgumentException('$primer is not callable'); |
||
135 | } |
||
136 | |||
137 | 19 | $primer = $primer ?: $this->defaultPrimer; |
|
138 | 19 | $groupedIds = array(); |
|
139 | |||
140 | /* @var $document PersistentCollectionInterface */ |
||
141 | 19 | foreach ($documents as $document) { |
|
142 | 19 | $fieldValue = $class->getFieldValue($document, $fieldName); |
|
143 | |||
144 | /* The field will need to be either a Proxy (reference-one) or |
||
145 | * PersistentCollection (reference-many) in order to prime anything. |
||
146 | */ |
||
147 | 19 | if ( ! is_object($fieldValue)) { |
|
148 | 2 | continue; |
|
149 | } |
||
150 | |||
151 | 17 | if ($mapping['type'] === 'one' && $fieldValue instanceof Proxy && ! $fieldValue->__isInitialized()) { |
|
152 | 12 | $refClass = $this->dm->getClassMetadata(get_class($fieldValue)); |
|
153 | 12 | $id = $this->uow->getDocumentIdentifier($fieldValue); |
|
154 | 12 | $groupedIds[$refClass->name][serialize($id)] = $id; |
|
155 | 10 | } elseif ($mapping['type'] == 'many' && $fieldValue instanceof PersistentCollectionInterface) { |
|
156 | 10 | $this->addManyReferences($fieldValue, $groupedIds); |
|
157 | } |
||
158 | } |
||
159 | |||
160 | 19 | foreach ($groupedIds as $className => $ids) { |
|
161 | 16 | $refClass = $this->dm->getClassMetadata($className); |
|
162 | 16 | call_user_func($primer, $this->dm, $refClass, array_values($ids), $hints); |
|
163 | } |
||
164 | 19 | } |
|
165 | |||
166 | /** |
||
167 | * If you are priming references inside an embedded document you'll need to parse the dot syntax. |
||
168 | * This method will traverse through embedded documents to find the reference to prime. |
||
169 | * However this method will not traverse through multiple layers of references. |
||
170 | * I.e. you can prime this: myDocument.embeddedDocument.embeddedDocuments.embeddedDocuments.referencedDocument(s) |
||
171 | * ... but you cannot prime this: myDocument.embeddedDocument.referencedDocuments.referencedDocument(s) |
||
172 | * This addresses Issue #624. |
||
173 | * |
||
174 | * @param string $fieldName |
||
175 | * @param ClassMetadata $class |
||
176 | * @param array|\Traversable $documents |
||
177 | * @param array $mapping |
||
178 | * @return array |
||
179 | */ |
||
180 | 21 | private function parseDotSyntaxForPrimer($fieldName, $class, $documents, $mapping = null) |
|
181 | { |
||
182 | // Recursion passthrough: |
||
183 | 21 | if ($mapping != null) { |
|
184 | return array('fieldName' => $fieldName, 'class' => $class, 'documents' => $documents, 'mapping' => $mapping); |
||
185 | } |
||
186 | |||
187 | // Gather mapping data: |
||
188 | 21 | $e = explode('.', $fieldName); |
|
189 | |||
190 | 21 | if ( ! isset($class->fieldMappings[$e[0]])) { |
|
191 | throw new \InvalidArgumentException(sprintf('Field %s cannot be further parsed for priming because it is unmapped.', $fieldName)); |
||
192 | } |
||
193 | |||
194 | 21 | $mapping = $class->fieldMappings[$e[0]]; |
|
195 | 21 | $e[0] = $mapping['fieldName']; |
|
196 | |||
197 | // Case of embedded document(s) to recurse through: |
||
198 | 21 | if ( ! isset($mapping['reference'])) { |
|
199 | 4 | if (empty($mapping['embedded'])) { |
|
200 | 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)); |
|
201 | } |
||
202 | |||
203 | 3 | if ( ! isset($mapping['targetDocument'])) { |
|
204 | 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'])); |
||
205 | } |
||
206 | |||
207 | 3 | $childDocuments = array(); |
|
208 | |||
209 | 3 | foreach ($documents as $document) { |
|
210 | 3 | $fieldValue = $class->getFieldValue($document, $e[0]); |
|
211 | |||
212 | 3 | if ($fieldValue instanceof PersistentCollectionInterface) { |
|
213 | 3 | foreach ($fieldValue as $elemDocument) { |
|
214 | 3 | array_push($childDocuments, $elemDocument); |
|
215 | } |
||
216 | } else { |
||
217 | 2 | array_push($childDocuments,$fieldValue); |
|
218 | } |
||
219 | } |
||
220 | |||
221 | 3 | array_shift($e); |
|
222 | |||
223 | 3 | $childClass = $this->dm->getClassMetadata($mapping['targetDocument']); |
|
224 | |||
225 | 3 | if ( ! $childClass->hasField($e[0])) { |
|
226 | 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'])); |
||
227 | } |
||
228 | |||
229 | 3 | $childFieldName = implode('.',$e); |
|
230 | |||
231 | 3 | return $this->parseDotSyntaxForPrimer($childFieldName, $childClass, $childDocuments); |
|
232 | } |
||
233 | |||
234 | // Case of reference(s) to prime: |
||
235 | 20 | if ($mapping['reference']) { |
|
236 | 20 | if (count($e) > 1) { |
|
237 | 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)); |
||
238 | } |
||
239 | |||
240 | 20 | return array('fieldName' => $fieldName, 'class' => $class, 'documents' => $documents, 'mapping' => $mapping); |
|
241 | } |
||
242 | } |
||
243 | |||
244 | /** |
||
245 | * Adds identifiers from a PersistentCollection to $groupedIds. |
||
246 | * |
||
247 | * If the relation contains simple references, the mapping is assumed to |
||
248 | * have a target document class defined. Without that, there is no way to |
||
249 | * infer the class of the referenced documents. |
||
250 | * |
||
251 | * @param PersistentCollectionInterface $persistentCollection |
||
252 | * @param array $groupedIds |
||
253 | */ |
||
254 | 10 | private function addManyReferences(PersistentCollectionInterface $persistentCollection, array &$groupedIds) |
|
255 | { |
||
256 | 10 | $mapping = $persistentCollection->getMapping(); |
|
257 | |||
258 | 10 | if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) { |
|
259 | 2 | $className = $mapping['targetDocument']; |
|
260 | 2 | $class = $this->dm->getClassMetadata($className); |
|
261 | } |
||
262 | |||
263 | 10 | foreach ($persistentCollection->getMongoData() as $reference) { |
|
264 | 10 | if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) { |
|
265 | 2 | $id = $reference; |
|
266 | } else { |
||
267 | 9 | $id = $reference['$id']; |
|
268 | 9 | $className = $this->uow->getClassNameForAssociation($mapping, $reference); |
|
269 | 9 | $class = $this->dm->getClassMetadata($className); |
|
270 | } |
||
271 | |||
272 | 10 | $document = $this->uow->tryGetById($id, $class); |
|
273 | |||
274 | 10 | if ( ! $document || ($document instanceof Proxy && ! $document->__isInitialized())) { |
|
275 | 9 | $id = $class->getPHPIdentifierValue($id); |
|
276 | 9 | $groupedIds[$className][serialize($id)] = $id; |
|
277 | } |
||
278 | } |
||
279 | 10 | } |
|
280 | } |
||
281 |
This class constant has been deprecated.