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