Completed
Push — master ( 235134...a8fe50 )
by Maciej
09:09 queued 09:04
created

Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

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
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
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