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\Hydrator; |
||
6 | |||
7 | use Doctrine\Common\EventManager; |
||
8 | use Doctrine\ODM\MongoDB\Configuration; |
||
9 | use Doctrine\ODM\MongoDB\DocumentManager; |
||
10 | use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs; |
||
11 | use Doctrine\ODM\MongoDB\Event\PreLoadEventArgs; |
||
12 | use Doctrine\ODM\MongoDB\Events; |
||
13 | use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; |
||
14 | use Doctrine\ODM\MongoDB\Types\Type; |
||
15 | use Doctrine\ODM\MongoDB\UnitOfWork; |
||
16 | use ProxyManager\Proxy\GhostObjectInterface; |
||
17 | use const DIRECTORY_SEPARATOR; |
||
18 | use const E_USER_DEPRECATED; |
||
19 | use function array_key_exists; |
||
20 | use function chmod; |
||
21 | use function class_exists; |
||
22 | use function dirname; |
||
23 | use function file_exists; |
||
24 | use function file_put_contents; |
||
25 | use function get_class; |
||
26 | use function is_dir; |
||
27 | use function is_writable; |
||
28 | use function mkdir; |
||
29 | use function rename; |
||
30 | use function rtrim; |
||
31 | use function sprintf; |
||
32 | use function str_replace; |
||
33 | use function substr; |
||
34 | use function trigger_error; |
||
35 | use function uniqid; |
||
36 | |||
37 | /** |
||
38 | * The HydratorFactory class is responsible for instantiating a correct hydrator |
||
39 | * type based on document's ClassMetadata |
||
40 | * |
||
41 | * @final |
||
42 | */ |
||
43 | class HydratorFactory |
||
44 | { |
||
45 | /** |
||
46 | * The DocumentManager this factory is bound to. |
||
47 | * |
||
48 | * @var DocumentManager |
||
49 | */ |
||
50 | private $dm; |
||
51 | |||
52 | /** |
||
53 | * The UnitOfWork used to coordinate object-level transactions. |
||
54 | * |
||
55 | * @var UnitOfWork |
||
56 | */ |
||
57 | private $unitOfWork; |
||
58 | |||
59 | /** |
||
60 | * The EventManager associated with this Hydrator |
||
61 | * |
||
62 | * @var EventManager |
||
63 | */ |
||
64 | private $evm; |
||
65 | |||
66 | /** |
||
67 | * Which algorithm to use to automatically (re)generate hydrator classes. |
||
68 | * |
||
69 | * @var int |
||
70 | */ |
||
71 | private $autoGenerate; |
||
72 | |||
73 | /** |
||
74 | * The namespace that contains all hydrator classes. |
||
75 | * |
||
76 | * @var string|null |
||
77 | */ |
||
78 | private $hydratorNamespace; |
||
79 | |||
80 | /** |
||
81 | * The directory that contains all hydrator classes. |
||
82 | * |
||
83 | * @var string |
||
84 | */ |
||
85 | private $hydratorDir; |
||
86 | |||
87 | /** |
||
88 | * Array of instantiated document hydrators. |
||
89 | * |
||
90 | * @var array |
||
91 | */ |
||
92 | private $hydrators = []; |
||
93 | |||
94 | /** |
||
95 | * @throws HydratorException |
||
96 | */ |
||
97 | 1656 | public function __construct(DocumentManager $dm, EventManager $evm, ?string $hydratorDir, ?string $hydratorNs, int $autoGenerate) |
|
98 | { |
||
99 | 1656 | if (self::class !== static::class) { |
|
100 | @trigger_error(sprintf('The class "%s" extends "%s" which will be final in MongoDB ODM 2.0.', static::class, self::class), E_USER_DEPRECATED); |
||
0 ignored issues
–
show
|
|||
101 | } |
||
102 | 1656 | if (! $hydratorDir) { |
|
103 | throw HydratorException::hydratorDirectoryRequired(); |
||
104 | } |
||
105 | 1656 | if (! $hydratorNs) { |
|
106 | throw HydratorException::hydratorNamespaceRequired(); |
||
107 | } |
||
108 | 1656 | $this->dm = $dm; |
|
109 | 1656 | $this->evm = $evm; |
|
110 | 1656 | $this->hydratorDir = $hydratorDir; |
|
111 | 1656 | $this->hydratorNamespace = $hydratorNs; |
|
112 | 1656 | $this->autoGenerate = $autoGenerate; |
|
113 | 1656 | } |
|
114 | |||
115 | /** |
||
116 | * Sets the UnitOfWork instance. |
||
117 | * |
||
118 | * @internal |
||
119 | */ |
||
120 | 1656 | public function setUnitOfWork(UnitOfWork $uow) : void |
|
121 | { |
||
122 | 1656 | $this->unitOfWork = $uow; |
|
123 | 1656 | } |
|
124 | |||
125 | /** |
||
126 | * Gets the hydrator object for the given document class. |
||
127 | */ |
||
128 | 386 | public function getHydratorFor(string $className) : HydratorInterface |
|
129 | { |
||
130 | 386 | if (isset($this->hydrators[$className])) { |
|
131 | 213 | return $this->hydrators[$className]; |
|
132 | } |
||
133 | 386 | $hydratorClassName = str_replace('\\', '', $className) . 'Hydrator'; |
|
134 | 386 | $fqn = $this->hydratorNamespace . '\\' . $hydratorClassName; |
|
135 | 386 | $class = $this->dm->getClassMetadata($className); |
|
136 | |||
137 | 386 | if (! class_exists($fqn, false)) { |
|
138 | 178 | $fileName = $this->hydratorDir . DIRECTORY_SEPARATOR . $hydratorClassName . '.php'; |
|
139 | 178 | switch ($this->autoGenerate) { |
|
140 | case Configuration::AUTOGENERATE_NEVER: |
||
141 | require $fileName; |
||
142 | break; |
||
143 | |||
144 | case Configuration::AUTOGENERATE_ALWAYS: |
||
145 | 178 | $this->generateHydratorClass($class, $hydratorClassName, $fileName); |
|
146 | 178 | require $fileName; |
|
147 | 178 | break; |
|
148 | |||
149 | case Configuration::AUTOGENERATE_FILE_NOT_EXISTS: |
||
150 | if (! file_exists($fileName)) { |
||
151 | $this->generateHydratorClass($class, $hydratorClassName, $fileName); |
||
152 | } |
||
153 | require $fileName; |
||
154 | break; |
||
155 | |||
156 | case Configuration::AUTOGENERATE_EVAL: |
||
157 | $this->generateHydratorClass($class, $hydratorClassName, null); |
||
158 | break; |
||
159 | } |
||
160 | } |
||
161 | 386 | $this->hydrators[$className] = new $fqn($this->dm, $this->unitOfWork, $class); |
|
162 | 386 | return $this->hydrators[$className]; |
|
163 | } |
||
164 | |||
165 | /** |
||
166 | * Generates hydrator classes for all given classes. |
||
167 | * |
||
168 | * @param array $classes The classes (ClassMetadata instances) for which to generate hydrators. |
||
169 | * @param string $toDir The target directory of the hydrator classes. If not specified, the |
||
170 | * directory configured on the Configuration of the DocumentManager used |
||
171 | * by this factory is used. |
||
172 | */ |
||
173 | public function generateHydratorClasses(array $classes, ?string $toDir = null) : void |
||
174 | { |
||
175 | $hydratorDir = $toDir ?: $this->hydratorDir; |
||
176 | $hydratorDir = rtrim($hydratorDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; |
||
177 | foreach ($classes as $class) { |
||
178 | $hydratorClassName = str_replace('\\', '', $class->name) . 'Hydrator'; |
||
179 | $hydratorFileName = $hydratorDir . $hydratorClassName . '.php'; |
||
180 | $this->generateHydratorClass($class, $hydratorClassName, $hydratorFileName); |
||
181 | } |
||
182 | } |
||
183 | |||
184 | 178 | private function generateHydratorClass(ClassMetadata $class, string $hydratorClassName, ?string $fileName) : void |
|
185 | { |
||
186 | 178 | $code = ''; |
|
187 | |||
188 | 178 | foreach ($class->fieldMappings as $fieldName => $mapping) { |
|
189 | 178 | if (isset($mapping['alsoLoadFields'])) { |
|
190 | 5 | foreach ($mapping['alsoLoadFields'] as $name) { |
|
191 | 5 | $code .= sprintf( |
|
192 | <<<EOF |
||
193 | |||
194 | 5 | /** @AlsoLoad("$name") */ |
|
195 | 5 | if (!array_key_exists('%1\$s', \$data) && array_key_exists('$name', \$data)) { |
|
196 | 5 | \$data['%1\$s'] = \$data['$name']; |
|
197 | } |
||
198 | |||
199 | EOF |
||
200 | , |
||
201 | 5 | $mapping['name'] |
|
202 | ); |
||
203 | } |
||
204 | } |
||
205 | |||
206 | 178 | if ($mapping['type'] === 'date') { |
|
207 | 9 | $code .= sprintf( |
|
208 | <<<EOF |
||
209 | |||
210 | /** @Field(type="date") */ |
||
211 | if (isset(\$data['%1\$s'])) { |
||
212 | \$value = \$data['%1\$s']; |
||
213 | %3\$s |
||
214 | \$this->class->reflFields['%2\$s']->setValue(\$document, clone \$return); |
||
215 | \$hydratedData['%2\$s'] = \$return; |
||
216 | } |
||
217 | |||
218 | EOF |
||
219 | , |
||
220 | 9 | $mapping['name'], |
|
221 | 9 | $mapping['fieldName'], |
|
222 | 9 | Type::getType($mapping['type'])->closureToPHP() |
|
223 | ); |
||
224 | 178 | } elseif (! isset($mapping['association'])) { |
|
225 | 178 | $code .= sprintf( |
|
226 | <<<EOF |
||
227 | |||
228 | 178 | /** @Field(type="{$mapping['type']}") */ |
|
229 | if (isset(\$data['%1\$s']) || (! empty(\$this->class->fieldMappings['%2\$s']['nullable']) && array_key_exists('%1\$s', \$data))) { |
||
230 | \$value = \$data['%1\$s']; |
||
231 | if (\$value !== null) { |
||
232 | \$typeIdentifier = \$this->class->fieldMappings['%2\$s']['type']; |
||
233 | %3\$s |
||
234 | } else { |
||
235 | \$return = null; |
||
236 | } |
||
237 | \$this->class->reflFields['%2\$s']->setValue(\$document, \$return); |
||
238 | \$hydratedData['%2\$s'] = \$return; |
||
239 | } |
||
240 | |||
241 | EOF |
||
242 | , |
||
243 | 178 | $mapping['name'], |
|
244 | 178 | $mapping['fieldName'], |
|
245 | 178 | Type::getType($mapping['type'])->closureToPHP() |
|
246 | ); |
||
247 | 119 | } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isOwningSide']) { |
|
248 | 51 | $code .= sprintf( |
|
249 | <<<EOF |
||
250 | |||
251 | /** @ReferenceOne */ |
||
252 | if (isset(\$data['%1\$s'])) { |
||
253 | \$reference = \$data['%1\$s']; |
||
254 | \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$reference); |
||
255 | \$identifier = ClassMetadata::getReferenceId(\$reference, \$this->class->fieldMappings['%2\$s']['storeAs']); |
||
256 | \$targetMetadata = \$this->dm->getClassMetadata(\$className); |
||
257 | \$id = \$targetMetadata->getPHPIdentifierValue(\$identifier); |
||
258 | \$return = \$this->dm->getReference(\$className, \$id); |
||
259 | \$this->class->reflFields['%2\$s']->setValue(\$document, \$return); |
||
260 | \$hydratedData['%2\$s'] = \$return; |
||
261 | } |
||
262 | |||
263 | EOF |
||
264 | , |
||
265 | 51 | $mapping['name'], |
|
266 | 51 | $mapping['fieldName'] |
|
267 | ); |
||
268 | 101 | } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isInverseSide']) { |
|
269 | 6 | if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) { |
|
270 | 1 | $code .= sprintf( |
|
271 | <<<EOF |
||
272 | |||
273 | \$className = \$this->class->fieldMappings['%2\$s']['targetDocument']; |
||
274 | \$return = \$this->dm->getRepository(\$className)->%3\$s(\$document); |
||
275 | \$this->class->reflFields['%2\$s']->setValue(\$document, \$return); |
||
276 | \$hydratedData['%2\$s'] = \$return; |
||
277 | |||
278 | EOF |
||
279 | , |
||
280 | 1 | $mapping['name'], |
|
281 | 1 | $mapping['fieldName'], |
|
282 | 1 | $mapping['repositoryMethod'] |
|
283 | ); |
||
284 | } else { |
||
285 | 6 | $code .= sprintf( |
|
286 | <<<EOF |
||
287 | |||
288 | \$mapping = \$this->class->fieldMappings['%2\$s']; |
||
289 | \$className = \$mapping['targetDocument']; |
||
290 | \$targetClass = \$this->dm->getClassMetadata(\$mapping['targetDocument']); |
||
291 | \$mappedByMapping = \$targetClass->fieldMappings[\$mapping['mappedBy']]; |
||
292 | \$mappedByFieldName = ClassMetadata::getReferenceFieldName(\$mappedByMapping['storeAs'], \$mapping['mappedBy']); |
||
293 | \$criteria = array_merge( |
||
294 | array(\$mappedByFieldName => \$data['_id']), |
||
295 | isset(\$this->class->fieldMappings['%2\$s']['criteria']) ? \$this->class->fieldMappings['%2\$s']['criteria'] : array() |
||
296 | ); |
||
297 | \$sort = isset(\$this->class->fieldMappings['%2\$s']['sort']) ? \$this->class->fieldMappings['%2\$s']['sort'] : array(); |
||
298 | \$return = \$this->unitOfWork->getDocumentPersister(\$className)->load(\$criteria, null, array(), 0, \$sort); |
||
299 | \$this->class->reflFields['%2\$s']->setValue(\$document, \$return); |
||
300 | \$hydratedData['%2\$s'] = \$return; |
||
301 | |||
302 | EOF |
||
303 | , |
||
304 | 6 | $mapping['name'], |
|
305 | 6 | $mapping['fieldName'] |
|
306 | ); |
||
307 | } |
||
308 | 100 | } elseif ($mapping['association'] === ClassMetadata::REFERENCE_MANY || $mapping['association'] === ClassMetadata::EMBED_MANY) { |
|
309 | 79 | $code .= sprintf( |
|
310 | <<<EOF |
||
311 | |||
312 | /** @Many */ |
||
313 | \$mongoData = isset(\$data['%1\$s']) ? \$data['%1\$s'] : null; |
||
314 | \$return = \$this->dm->getConfiguration()->getPersistentCollectionFactory()->create(\$this->dm, \$this->class->fieldMappings['%2\$s']); |
||
315 | \$return->setHints(\$hints); |
||
316 | \$return->setOwner(\$document, \$this->class->fieldMappings['%2\$s']); |
||
317 | \$return->setInitialized(false); |
||
318 | if (\$mongoData) { |
||
319 | \$return->setMongoData(\$mongoData); |
||
320 | } |
||
321 | \$this->class->reflFields['%2\$s']->setValue(\$document, \$return); |
||
322 | \$hydratedData['%2\$s'] = \$return; |
||
323 | |||
324 | EOF |
||
325 | , |
||
326 | 79 | $mapping['name'], |
|
327 | 79 | $mapping['fieldName'] |
|
328 | ); |
||
329 | 45 | } elseif ($mapping['association'] === ClassMetadata::EMBED_ONE) { |
|
330 | 45 | $code .= sprintf( |
|
331 | <<<EOF |
||
332 | |||
333 | /** @EmbedOne */ |
||
334 | if (isset(\$data['%1\$s'])) { |
||
335 | \$embeddedDocument = \$data['%1\$s']; |
||
336 | \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$embeddedDocument); |
||
337 | \$embeddedMetadata = \$this->dm->getClassMetadata(\$className); |
||
338 | \$return = \$embeddedMetadata->newInstance(); |
||
339 | |||
340 | \$this->unitOfWork->setParentAssociation(\$return, \$this->class->fieldMappings['%2\$s'], \$document, '%1\$s'); |
||
341 | |||
342 | \$embeddedData = \$this->dm->getHydratorFactory()->hydrate(\$return, \$embeddedDocument, \$hints); |
||
343 | \$embeddedId = \$embeddedMetadata->identifier && isset(\$embeddedData[\$embeddedMetadata->identifier]) ? \$embeddedData[\$embeddedMetadata->identifier] : null; |
||
344 | |||
345 | if (empty(\$hints[Query::HINT_READ_ONLY])) { |
||
346 | \$this->unitOfWork->registerManaged(\$return, \$embeddedId, \$embeddedData); |
||
347 | } |
||
348 | |||
349 | \$this->class->reflFields['%2\$s']->setValue(\$document, \$return); |
||
350 | \$hydratedData['%2\$s'] = \$return; |
||
351 | } |
||
352 | |||
353 | EOF |
||
354 | , |
||
355 | 45 | $mapping['name'], |
|
356 | 45 | $mapping['fieldName'] |
|
357 | ); |
||
358 | } |
||
359 | } |
||
360 | |||
361 | 178 | $namespace = $this->hydratorNamespace; |
|
362 | 178 | $code = sprintf( |
|
363 | <<<EOF |
||
364 | <?php |
||
365 | |||
366 | 178 | namespace $namespace; |
|
367 | |||
368 | use Doctrine\ODM\MongoDB\DocumentManager; |
||
369 | use Doctrine\ODM\MongoDB\Hydrator\HydratorInterface; |
||
370 | use Doctrine\ODM\MongoDB\Query\Query; |
||
371 | use Doctrine\ODM\MongoDB\UnitOfWork; |
||
372 | use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; |
||
373 | |||
374 | /** |
||
375 | * THIS CLASS WAS GENERATED BY THE DOCTRINE ODM. DO NOT EDIT THIS FILE. |
||
376 | */ |
||
377 | 178 | class $hydratorClassName implements HydratorInterface |
|
378 | { |
||
379 | private \$dm; |
||
380 | private \$unitOfWork; |
||
381 | private \$class; |
||
382 | |||
383 | public function __construct(DocumentManager \$dm, UnitOfWork \$uow, ClassMetadata \$class) |
||
384 | { |
||
385 | \$this->dm = \$dm; |
||
386 | \$this->unitOfWork = \$uow; |
||
387 | \$this->class = \$class; |
||
388 | } |
||
389 | |||
390 | public function hydrate(object \$document, array \$data, array \$hints = array()): array |
||
391 | { |
||
392 | \$hydratedData = array(); |
||
393 | %s return \$hydratedData; |
||
394 | } |
||
395 | } |
||
396 | EOF |
||
397 | , |
||
398 | 178 | $code |
|
399 | ); |
||
400 | |||
401 | 178 | if ($fileName === null) { |
|
402 | if (! class_exists($namespace . '\\' . $hydratorClassName)) { |
||
403 | eval(substr($code, 5)); |
||
404 | } |
||
405 | |||
406 | return; |
||
407 | } |
||
408 | |||
409 | 178 | $parentDirectory = dirname($fileName); |
|
410 | |||
411 | 178 | if (! is_dir($parentDirectory) && (@mkdir($parentDirectory, 0775, true) === false)) { |
|
412 | throw HydratorException::hydratorDirectoryNotWritable(); |
||
413 | } |
||
414 | |||
415 | 178 | if (! is_writable($parentDirectory)) { |
|
416 | throw HydratorException::hydratorDirectoryNotWritable(); |
||
417 | } |
||
418 | |||
419 | 178 | $tmpFileName = $fileName . '.' . uniqid('', true); |
|
420 | 178 | file_put_contents($tmpFileName, $code); |
|
421 | 178 | rename($tmpFileName, $fileName); |
|
422 | 178 | chmod($fileName, 0664); |
|
423 | 178 | } |
|
424 | |||
425 | /** |
||
426 | * Hydrate array of MongoDB document data into the given document object. |
||
427 | * |
||
428 | * @param array $hints Any hints to account for during reconstitution/lookup of the document. |
||
429 | */ |
||
430 | 386 | public function hydrate(object $document, array $data, array $hints = []) : array |
|
431 | { |
||
432 | 386 | $metadata = $this->dm->getClassMetadata(get_class($document)); |
|
433 | // Invoke preLoad lifecycle events and listeners |
||
434 | 386 | if (! empty($metadata->lifecycleCallbacks[Events::preLoad])) { |
|
435 | 14 | $args = [new PreLoadEventArgs($document, $this->dm, $data)]; |
|
436 | 14 | $metadata->invokeLifecycleCallbacks(Events::preLoad, $document, $args); |
|
437 | } |
||
438 | 386 | if ($this->evm->hasListeners(Events::preLoad)) { |
|
439 | 3 | $this->evm->dispatchEvent(Events::preLoad, new PreLoadEventArgs($document, $this->dm, $data)); |
|
440 | } |
||
441 | |||
442 | // alsoLoadMethods may transform the document before hydration |
||
443 | 386 | if (! empty($metadata->alsoLoadMethods)) { |
|
444 | 12 | foreach ($metadata->alsoLoadMethods as $method => $fieldNames) { |
|
445 | 12 | foreach ($fieldNames as $fieldName) { |
|
446 | // Invoke the method only once for the first field we find |
||
447 | 12 | if (array_key_exists($fieldName, $data)) { |
|
448 | 8 | $document->$method($data[$fieldName]); |
|
449 | 8 | continue 2; |
|
450 | } |
||
451 | } |
||
452 | } |
||
453 | } |
||
454 | |||
455 | 386 | if ($document instanceof GhostObjectInterface) { |
|
456 | 73 | $document->setProxyInitializer(null); |
|
457 | } |
||
458 | |||
459 | 386 | $data = $this->getHydratorFor($metadata->name)->hydrate($document, $data, $hints); |
|
460 | |||
461 | // Invoke the postLoad lifecycle callbacks and listeners |
||
462 | 386 | if (! empty($metadata->lifecycleCallbacks[Events::postLoad])) { |
|
463 | 13 | $metadata->invokeLifecycleCallbacks(Events::postLoad, $document, [new LifecycleEventArgs($document, $this->dm)]); |
|
464 | } |
||
465 | 386 | if ($this->evm->hasListeners(Events::postLoad)) { |
|
466 | 4 | $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($document, $this->dm)); |
|
467 | } |
||
468 | |||
469 | 386 | return $data; |
|
470 | } |
||
471 | } |
||
472 |
If you suppress an error, we recommend checking for the error condition explicitly: