Completed
Pull Request — master (#1908)
by
unknown
15:50
created

HydratorFactory::getHydratorFor()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 11.0764

Importance

Changes 0
Metric Value
dl 0
loc 36
ccs 14
cts 22
cp 0.6364
rs 8.0995
c 0
b 0
f 0
cc 8
nc 8
nop 1
crap 11.0764
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 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
class HydratorFactory
40
{
41
    /**
42
     * The DocumentManager this factory is bound to.
43
     *
44
     * @var DocumentManager
45
     */
46
    private $dm;
47
48
    /**
49
     * The UnitOfWork used to coordinate object-level transactions.
50
     *
51
     * @var UnitOfWork
52
     */
53
    private $unitOfWork;
54
55
    /**
56
     * The EventManager associated with this Hydrator
57
     *
58
     * @var EventManager
59
     */
60
    private $evm;
61
62
    /**
63
     * Which algorithm to use to automatically (re)generate hydrator classes.
64
     *
65
     * @var int
66
     */
67
    private $autoGenerate;
68
69
    /**
70
     * The namespace that contains all hydrator classes.
71
     *
72
     * @var string|null
73
     */
74
    private $hydratorNamespace;
75
76
    /**
77
     * The directory that contains all hydrator classes.
78
     *
79
     * @var string|null
80
     */
81
    private $hydratorDir;
82
83
    /**
84
     * Array of instantiated document hydrators.
85
     *
86
     * @var array
87
     */
88
    private $hydrators = [];
89
90
    /**
91
     * @throws HydratorException
92
     */
93 1642
    public function __construct(DocumentManager $dm, EventManager $evm, ?string $hydratorDir, ?string $hydratorNs, int $autoGenerate)
94
    {
95 1642
        if (! $hydratorDir) {
96
            throw HydratorException::hydratorDirectoryRequired();
97
        }
98 1642
        if (! $hydratorNs) {
99
            throw HydratorException::hydratorNamespaceRequired();
100
        }
101 1642
        $this->dm                = $dm;
102 1642
        $this->evm               = $evm;
103 1642
        $this->hydratorDir       = $hydratorDir;
104 1642
        $this->hydratorNamespace = $hydratorNs;
105 1642
        $this->autoGenerate      = $autoGenerate;
106 1642
    }
107
108
    /**
109
     * Sets the UnitOfWork instance.
110
     */
111 1642
    public function setUnitOfWork(UnitOfWork $uow) : void
112
    {
113 1642
        $this->unitOfWork = $uow;
114 1642
    }
115
116
    /**
117
     * Gets the hydrator object for the given document class.
118
     */
119 385
    public function getHydratorFor(string $className) : HydratorInterface
120
    {
121 385
        if (isset($this->hydrators[$className])) {
122 213
            return $this->hydrators[$className];
123
        }
124 385
        $hydratorClassName = str_replace('\\', '', $className) . 'Hydrator';
125 385
        $fqn               = $this->hydratorNamespace . '\\' . $hydratorClassName;
126 385
        $class             = $this->dm->getClassMetadata($className);
127
128 385
        if (! class_exists($fqn, false)) {
129 177
            $fileName = $this->hydratorDir . DIRECTORY_SEPARATOR . $hydratorClassName . '.php';
130 177
            switch ($this->autoGenerate) {
131
                case Configuration::AUTOGENERATE_NEVER:
132
                    require $fileName;
133
                    break;
134
135
                case Configuration::AUTOGENERATE_ALWAYS:
136 177
                    $this->generateHydratorClass($class, $hydratorClassName, $fileName);
137 177
                    require $fileName;
138 177
                    break;
139
140
                case Configuration::AUTOGENERATE_FILE_NOT_EXISTS:
141
                    if (! file_exists($fileName)) {
142
                        $this->generateHydratorClass($class, $hydratorClassName, $fileName);
143
                    }
144
                    require $fileName;
145
                    break;
146
147
                case Configuration::AUTOGENERATE_EVAL:
148
                    $this->generateHydratorClass($class, $hydratorClassName, false);
149
                    break;
150
            }
151
        }
152 385
        $this->hydrators[$className] = new $fqn($this->dm, $this->unitOfWork, $class);
153 385
        return $this->hydrators[$className];
154
    }
155
156
    /**
157
     * Generates hydrator classes for all given classes.
158
     *
159
     * @param array  $classes The classes (ClassMetadata instances) for which to generate hydrators.
160
     * @param string $toDir   The target directory of the hydrator classes. If not specified, the
161
     *                        directory configured on the Configuration of the DocumentManager used
162
     *                        by this factory is used.
163
     */
164
    public function generateHydratorClasses(array $classes, ?string $toDir = null) : void
165
    {
166
        $hydratorDir = $toDir ?: $this->hydratorDir;
167
        $hydratorDir = rtrim($hydratorDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
168
        foreach ($classes as $class) {
169
            $hydratorClassName = str_replace('\\', '', $class->name) . 'Hydrator';
170
            $hydratorFileName  = $hydratorDir . $hydratorClassName . '.php';
171
            $this->generateHydratorClass($class, $hydratorClassName, $hydratorFileName);
172
        }
173
    }
174
175
    /**
176
     * @param string|false $fileName Filename where class code to be written or false to eval code.
177
     */
178 177
    private function generateHydratorClass(ClassMetadata $class, string $hydratorClassName, $fileName) : void
179
    {
180 177
        $code = '';
181
182 177
        foreach ($class->fieldMappings as $fieldName => $mapping) {
183 177
            if (isset($mapping['alsoLoadFields'])) {
184 5
                foreach ($mapping['alsoLoadFields'] as $name) {
185 5
                    $code .= sprintf(
186
                        <<<EOF
187
188 5
        /** @AlsoLoad("$name") */
189 5
        if (!array_key_exists('%1\$s', \$data) && array_key_exists('$name', \$data)) {
190 5
            \$data['%1\$s'] = \$data['$name'];
191
        }
192
193
EOF
194
                        ,
195 5
                        $mapping['name']
196
                    );
197
                }
198
            }
199
200 177
            if ($mapping['type'] === 'date') {
201 9
                $code .= sprintf(
202
                    <<<EOF
203
204
        /** @Field(type="date") */
205
        if (isset(\$data['%1\$s'])) {
206
            \$value = \$data['%1\$s'];
207
            %3\$s
208
            \$reflFields['%2\$s']->setValue(\$document, clone \$return);
209
            \$hydratedData['%2\$s'] = \$return;
210
        }
211
212
EOF
213
                    ,
214 9
                    $mapping['name'],
215 9
                    $mapping['fieldName'],
216 9
                    Type::getType($mapping['type'])->closureToPHP()
217
                );
218 177
            } elseif (! isset($mapping['association'])) {
219 176
                $condition      = null;
0 ignored issues
show
Unused Code introduced by
$condition is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
220 176
                $conditionBlock = null;
0 ignored issues
show
Unused Code introduced by
$conditionBlock is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
221 176
                if (! empty($mapping['nullable'])) {
222 6
                    $condition      = sprintf("isset(\$data['%1\$s']) || array_key_exists('%1\$s', \$data)", $mapping['name']);
223 6
                    $conditionBlock = sprintf(
224
                        <<<EOF
225 6
if (\$value !== null) {
226
                \$typeIdentifier = \$fieldMappings['%1\$s']['type'];
227
                %2\$s
228
            } else {
229
                \$return = null;
230
            }
231
EOF
232
                        ,
233 6
                        $mapping['fieldName'],
234 6
                        Type::getType($mapping['type'])->closureToPHP()
235
                    );
236
                } else {
237 176
                    $condition      = sprintf("isset(\$data['%1\$s'])", $mapping['name']);
238 176
                    $conditionBlock = sprintf(
239
                        <<<EOF
240 176
\$typeIdentifier = \$fieldMappings['%1\$s']['type'];
241
            %2\$s
242
EOF
243
                        ,
244 176
                        $mapping['fieldName'],
245 176
                        Type::getType($mapping['type'])->closureToPHP()
246
                    );
247
                }
248 176
                $code .= sprintf(
249
                    <<<EOF
250
251 176
        /** @Field(type="{$mapping['type']}") */
252 176
        if ($condition) {
253
            \$value = \$data['%1\$s'];
254 176
            {$conditionBlock}
255
            \$reflFields['%2\$s']->setValue(\$document, \$return);
256
            \$hydratedData['%2\$s'] = \$return;
257
        }
258
259
EOF
260
                    ,
261 176
                    $mapping['name'],
262 176
                    $mapping['fieldName'],
263 176
                    Type::getType($mapping['type'])->closureToPHP()
264
                );
265 118
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isOwningSide']) {
266 50
                $code .= sprintf(
267
                    <<<EOF
268
269
        /** @ReferenceOne */
270
        if (isset(\$data['%1\$s'])) {
271
            \$reference = \$data['%1\$s'];
272
            \$className = \$this->unitOfWork->getClassNameForAssociation(\$fieldMappings['%2\$s'], \$reference);
273
            \$identifier = ClassMetadata::getReferenceId(\$reference, \$fieldMappings['%2\$s']['storeAs']);
274
            \$targetMetadata = \$this->dm->getClassMetadata(\$className);
275
            \$id = \$targetMetadata->getPHPIdentifierValue(\$identifier);
276
            \$return = \$this->dm->getReference(\$className, \$id);
277
            \$reflFields['%2\$s']->setValue(\$document, \$return);
278
            \$hydratedData['%2\$s'] = \$return;
279
        }
280
281
EOF
282
                    ,
283 50
                    $mapping['name'],
284 50
                    $mapping['fieldName']
285
                );
286 100
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isInverseSide']) {
287 6
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
288 1
                    $code .= sprintf(
289
                        <<<EOF
290
291
        \$className = \$fieldMappings['%2\$s']['targetDocument'];
292
        \$return = \$this->dm->getRepository(\$className)->%3\$s(\$document);
293
        \$reflFields['%2\$s']->setValue(\$document, \$return);
294
        \$hydratedData['%2\$s'] = \$return;
295
296
EOF
297
                        ,
298 1
                        $mapping['name'],
299 1
                        $mapping['fieldName'],
300 1
                        $mapping['repositoryMethod']
301
                    );
302
                } else {
303 6
                    $code .= sprintf(
304
                        <<<EOF
305
306
        \$mapping = \$fieldMappings['%2\$s'];
307
        \$className = \$mapping['targetDocument'];
308
        \$targetClass = \$this->dm->getClassMetadata(\$mapping['targetDocument']);
309
        \$mappedByMapping = \$targetClass->fieldMappings[\$mapping['mappedBy']];
310
        \$mappedByFieldName = ClassMetadata::getReferenceFieldName(\$mappedByMapping['storeAs'], \$mapping['mappedBy']);
311
        \$criteria = array_merge(
312
            array(\$mappedByFieldName => \$data['_id']),
313
            isset(\$fieldMappings['%2\$s']['criteria']) ? \$fieldMappings['%2\$s']['criteria'] : array()
314
        );
315
        \$sort = isset(\$fieldMappings['%2\$s']['sort']) ? \$fieldMappings['%2\$s']['sort'] : array();
316
        \$return = \$this->unitOfWork->getDocumentPersister(\$className)->load(\$criteria, null, array(), 0, \$sort);
317
        \$reflFields['%2\$s']->setValue(\$document, \$return);
318
        \$hydratedData['%2\$s'] = \$return;
319
320
EOF
321
                        ,
322 6
                        $mapping['name'],
323 6
                        $mapping['fieldName']
324
                    );
325
                }
326 99
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_MANY || $mapping['association'] === ClassMetadata::EMBED_MANY) {
327 79
                $code .= sprintf(
328
                    <<<EOF
329
330
        /** @Many */
331
        \$mongoData = isset(\$data['%1\$s']) ? \$data['%1\$s'] : null;
332
        \$return = \$this->persistentCollectionFactory->create(\$this->dm, \$fieldMappings['%2\$s']);
333
        \$return->setHints(\$hints);
334
        \$return->setOwner(\$document, \$fieldMappings['%2\$s']);
335
        \$return->setInitialized(false);
336
        if (\$mongoData) {
337
            \$return->setMongoData(\$mongoData);
338
        }
339
        \$reflFields['%2\$s']->setValue(\$document, \$return);
340
        \$hydratedData['%2\$s'] = \$return;
341
342
EOF
343
                    ,
344 79
                    $mapping['name'],
345 79
                    $mapping['fieldName']
346
                );
347 44
            } elseif ($mapping['association'] === ClassMetadata::EMBED_ONE) {
348 44
                $code .= sprintf(
349
                    <<<EOF
350
351
        /** @EmbedOne */
352
        if (isset(\$data['%1\$s'])) {
353
            \$embeddedDocument = \$data['%1\$s'];
354
            \$className = \$this->unitOfWork->getClassNameForAssociation(\$fieldMappings['%2\$s'], \$embeddedDocument);
355
            \$embeddedMetadata = \$this->dm->getClassMetadata(\$className);
356
            \$return = \$embeddedMetadata->newInstance();
357
358
            \$this->unitOfWork->setParentAssociation(\$return, \$fieldMappings['%2\$s'], \$document, '%1\$s');
359
360
            \$embeddedData = \$this->dm->getHydratorFactory()->hydrate(\$return, \$embeddedDocument, \$hints);
361
            \$embeddedId = \$embeddedMetadata->identifier && isset(\$embeddedData[\$embeddedMetadata->identifier]) ? \$embeddedData[\$embeddedMetadata->identifier] : null;
362
363
            if (empty(\$hints[Query::HINT_READ_ONLY])) {
364
                \$this->unitOfWork->registerManaged(\$return, \$embeddedId, \$embeddedData);
365
            }
366
367
            \$reflFields['%2\$s']->setValue(\$document, \$return);
368
            \$hydratedData['%2\$s'] = \$return;
369
        }
370
371
EOF
372
                    ,
373 44
                    $mapping['name'],
374 177
                    $mapping['fieldName']
375
                );
376
            }
377
        }
378
379 177
        $namespace = $this->hydratorNamespace;
380 177
        $code      = sprintf(
381
            <<<EOF
382
<?php
383
384 177
namespace $namespace;
385
386
use Doctrine\ODM\MongoDB\DocumentManager;
387
use Doctrine\ODM\MongoDB\Hydrator\HydratorInterface;
388
use Doctrine\ODM\MongoDB\Query\Query;
389
use Doctrine\ODM\MongoDB\UnitOfWork;
390
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
391
392
/**
393
 * THIS CLASS WAS GENERATED BY THE DOCTRINE ODM. DO NOT EDIT THIS FILE.
394
 */
395 177
class $hydratorClassName implements HydratorInterface
396
{
397
    private \$dm;
398
    private \$unitOfWork;
399
    private \$fieldMappings;
400
    private \$reflFields;
401
    private \$persistentCollectionFactory;
402
403
    public function __construct(DocumentManager \$dm, UnitOfWork \$uow, ClassMetadata \$class)
404
    {
405
        \$this->dm = \$dm;
406
        \$this->unitOfWork = \$uow;
407
        \$this->fieldMappings = \$class->fieldMappings;
408
        \$this->reflFields = \$class->reflFields;
409
        \$this->persistentCollectionFactory = \$dm->getConfiguration()->getPersistentCollectionFactory();
410
    }
411
412
    public function hydrate(object \$document, array \$data, array \$hints = array()): array
413
    {
414
        \$hydratedData = array();
415
        \$fieldMappings = \$this->fieldMappings;
416
        \$reflFields = \$this->reflFields;
417
%s        return \$hydratedData;
418
    }
419
}
420
EOF
421
            ,
422 177
            $code
423
        );
424
425 177
        if ($fileName === false) {
426
            if (! class_exists($namespace . '\\' . $hydratorClassName)) {
427
                eval(substr($code, 5));
428
            }
429
        } else {
430 177
            $parentDirectory = dirname($fileName);
431
432 177
            if (! is_dir($parentDirectory) && (@mkdir($parentDirectory, 0775, true) === false)) {
433
                throw HydratorException::hydratorDirectoryNotWritable();
434
            }
435
436 177
            if (! is_writable($parentDirectory)) {
437
                throw HydratorException::hydratorDirectoryNotWritable();
438
            }
439
440 177
            $tmpFileName = $fileName . '.' . uniqid('', true);
441 177
            file_put_contents($tmpFileName, $code);
442 177
            rename($tmpFileName, $fileName);
443 177
            chmod($fileName, 0664);
444
        }
445 177
    }
446
447
    /**
448
     * Hydrate array of MongoDB document data into the given document object.
449
     *
450
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
451
     */
452 385
    public function hydrate(object $document, array $data, array $hints = []) : array
453
    {
454 385
        $metadata = $this->dm->getClassMetadata(get_class($document));
455
        // Invoke preLoad lifecycle events and listeners
456 385
        if (! empty($metadata->lifecycleCallbacks[Events::preLoad])) {
457 14
            $args = [new PreLoadEventArgs($document, $this->dm, $data)];
458 14
            $metadata->invokeLifecycleCallbacks(Events::preLoad, $document, $args);
459
        }
460 385
        if ($this->evm->hasListeners(Events::preLoad)) {
461 3
            $this->evm->dispatchEvent(Events::preLoad, new PreLoadEventArgs($document, $this->dm, $data));
462
        }
463
464
        // alsoLoadMethods may transform the document before hydration
465 385
        if (! empty($metadata->alsoLoadMethods)) {
466 12
            foreach ($metadata->alsoLoadMethods as $method => $fieldNames) {
467 12
                foreach ($fieldNames as $fieldName) {
468
                    // Invoke the method only once for the first field we find
469 12
                    if (array_key_exists($fieldName, $data)) {
470 8
                        $document->$method($data[$fieldName]);
471 12
                        continue 2;
472
                    }
473
                }
474
            }
475
        }
476
477 385
        if ($document instanceof GhostObjectInterface) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
478 73
            $document->setProxyInitializer(null);
479
        }
480
481 385
        $data = $this->getHydratorFor($metadata->name)->hydrate($document, $data, $hints);
482
483
        // Invoke the postLoad lifecycle callbacks and listeners
484 385
        if (! empty($metadata->lifecycleCallbacks[Events::postLoad])) {
485 13
            $metadata->invokeLifecycleCallbacks(Events::postLoad, $document, [new LifecycleEventArgs($document, $this->dm)]);
486
        }
487 385
        if ($this->evm->hasListeners(Events::postLoad)) {
488 4
            $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($document, $this->dm));
489
        }
490
491 385
        return $data;
492
    }
493
}
494