Completed
Pull Request — master (#1875)
by Andreas
61:52
created

HydratorFactory::hydrate()   B

Complexity

Conditions 10
Paths 64

Size

Total Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 10

Importance

Changes 0
Metric Value
dl 0
loc 41
ccs 23
cts 23
cp 1
rs 7.6666
c 0
b 0
f 0
cc 10
nc 64
nop 3
crap 10

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 1636
    public function __construct(DocumentManager $dm, EventManager $evm, ?string $hydratorDir, ?string $hydratorNs, int $autoGenerate)
94
    {
95 1636
        if (! $hydratorDir) {
96
            throw HydratorException::hydratorDirectoryRequired();
97
        }
98 1636
        if (! $hydratorNs) {
99
            throw HydratorException::hydratorNamespaceRequired();
100
        }
101 1636
        $this->dm                = $dm;
102 1636
        $this->evm               = $evm;
103 1636
        $this->hydratorDir       = $hydratorDir;
104 1636
        $this->hydratorNamespace = $hydratorNs;
105 1636
        $this->autoGenerate      = $autoGenerate;
106 1636
    }
107
108
    /**
109
     * Sets the UnitOfWork instance.
110
     */
111 1636
    public function setUnitOfWork(UnitOfWork $uow) : void
112
    {
113 1636
        $this->unitOfWork = $uow;
114 1636
    }
115
116
    /**
117
     * Gets the hydrator object for the given document class.
118
     */
119 398
    public function getHydratorFor(string $className) : HydratorInterface
120
    {
121 398
        if (isset($this->hydrators[$className])) {
122 213
            return $this->hydrators[$className];
123
        }
124 398
        $hydratorClassName = str_replace('\\', '', $className) . 'Hydrator';
125 398
        $fqn               = $this->hydratorNamespace . '\\' . $hydratorClassName;
126 398
        $class             = $this->dm->getClassMetadata($className);
127
128 398
        if (! class_exists($fqn, false)) {
129 182
            $fileName = $this->hydratorDir . DIRECTORY_SEPARATOR . $hydratorClassName . '.php';
130 182
            switch ($this->autoGenerate) {
131
                case Configuration::AUTOGENERATE_NEVER:
132
                    require $fileName;
133
                    break;
134
135
                case Configuration::AUTOGENERATE_ALWAYS:
136 182
                    $this->generateHydratorClass($class, $hydratorClassName, $fileName);
137 182
                    require $fileName;
138 182
                    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 398
        $this->hydrators[$className] = new $fqn($this->dm, $this->unitOfWork, $class);
153 398
        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 182
    private function generateHydratorClass(ClassMetadata $class, string $hydratorClassName, $fileName) : void
179
    {
180 182
        $code = '';
181
182 182
        foreach ($class->fieldMappings as $fieldName => $mapping) {
183 182
            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 182
            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
            \$this->class->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 182
            } elseif (! isset($mapping['association'])) {
219 181
                $code .= sprintf(
220
                    <<<EOF
221
222 181
        /** @Field(type="{$mapping['type']}") */
223
        if (isset(\$data['%1\$s']) || (! empty(\$this->class->fieldMappings['%2\$s']['nullable']) && array_key_exists('%1\$s', \$data))) {
224
            \$value = \$data['%1\$s'];
225
            if (\$value !== null) {
226
                \$typeIdentifier = \$this->class->fieldMappings['%2\$s']['type'];
227
                %3\$s
228
            } else {
229
                \$return = null;
230
            }
231
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
232
            \$hydratedData['%2\$s'] = \$return;
233
        }
234
235
EOF
236
                    ,
237 181
                    $mapping['name'],
238 181
                    $mapping['fieldName'],
239 181
                    Type::getType($mapping['type'])->closureToPHP()
240
                );
241 120
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isOwningSide']) {
242 52
                $code .= sprintf(
243
                    <<<EOF
244
245
        /** @ReferenceOne */
246
        if (isset(\$data['%1\$s'])) {
247
            \$reference = \$data['%1\$s'];
248
            \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$reference);
249
            \$identifier = ClassMetadata::getReferenceId(\$reference, \$this->class->fieldMappings['%2\$s']['storeAs']);
250
            \$targetMetadata = \$this->dm->getClassMetadata(\$className);
251
            \$id = \$targetMetadata->getPHPIdentifierValue(\$identifier);
252
            \$return = \$this->dm->getReference(\$className, \$id);
253
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
254
            \$hydratedData['%2\$s'] = \$return;
255
        }
256
257
EOF
258
                    ,
259 52
                    $mapping['name'],
260 52
                    $mapping['fieldName']
261
                );
262 101
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isInverseSide']) {
263 6
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
264 1
                    $code .= sprintf(
265
                        <<<EOF
266
267
        \$className = \$this->class->fieldMappings['%2\$s']['targetDocument'];
268
        \$return = \$this->dm->getRepository(\$className)->%3\$s(\$document);
269
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
270
        \$hydratedData['%2\$s'] = \$return;
271
272
EOF
273
                        ,
274 1
                        $mapping['name'],
275 1
                        $mapping['fieldName'],
276 1
                        $mapping['repositoryMethod']
277
                    );
278
                } else {
279 6
                    $code .= sprintf(
280
                        <<<EOF
281
282
        \$mapping = \$this->class->fieldMappings['%2\$s'];
283
        \$className = \$mapping['targetDocument'];
284
        \$targetClass = \$this->dm->getClassMetadata(\$mapping['targetDocument']);
285
        \$mappedByMapping = \$targetClass->fieldMappings[\$mapping['mappedBy']];
286
        \$mappedByFieldName = ClassMetadata::getReferenceFieldName(\$mappedByMapping['storeAs'], \$mapping['mappedBy']);
287
        \$criteria = array_merge(
288
            array(\$mappedByFieldName => \$data['_id']),
289
            isset(\$this->class->fieldMappings['%2\$s']['criteria']) ? \$this->class->fieldMappings['%2\$s']['criteria'] : array()
290
        );
291
        \$sort = isset(\$this->class->fieldMappings['%2\$s']['sort']) ? \$this->class->fieldMappings['%2\$s']['sort'] : array();
292
        \$return = \$this->unitOfWork->getDocumentPersister(\$className)->load(\$criteria, null, array(), 0, \$sort);
293
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
294
        \$hydratedData['%2\$s'] = \$return;
295
296
EOF
297
                        ,
298 6
                        $mapping['name'],
299 6
                        $mapping['fieldName']
300
                    );
301
                }
302 100
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_MANY || $mapping['association'] === ClassMetadata::EMBED_MANY) {
303 80
                $code .= sprintf(
304
                    <<<EOF
305
306
        /** @Many */
307
        \$mongoData = isset(\$data['%1\$s']) ? \$data['%1\$s'] : null;
308
        \$return = \$this->dm->getConfiguration()->getPersistentCollectionFactory()->create(\$this->dm, \$this->class->fieldMappings['%2\$s']);
309
        \$return->setHints(\$hints);
310
        \$return->setOwner(\$document, \$this->class->fieldMappings['%2\$s']);
311
        \$return->setInitialized(false);
312
        if (\$mongoData) {
313
            \$return->setMongoData(\$mongoData);
314
        }
315
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
316
        \$hydratedData['%2\$s'] = \$return;
317
318
EOF
319
                    ,
320 80
                    $mapping['name'],
321 80
                    $mapping['fieldName']
322
                );
323 45
            } elseif ($mapping['association'] === ClassMetadata::EMBED_ONE) {
324 45
                $code .= sprintf(
325
                    <<<EOF
326
327
        /** @EmbedOne */
328
        if (isset(\$data['%1\$s'])) {
329
            \$embeddedDocument = \$data['%1\$s'];
330
            \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$embeddedDocument);
331
            \$embeddedMetadata = \$this->dm->getClassMetadata(\$className);
332
            \$return = \$embeddedMetadata->newInstance();
333
334
            \$this->unitOfWork->setParentAssociation(\$return, \$this->class->fieldMappings['%2\$s'], \$document, '%1\$s');
335
336
            \$embeddedData = \$this->dm->getHydratorFactory()->hydrate(\$return, \$embeddedDocument, \$hints);
337
            \$embeddedId = \$embeddedMetadata->identifier && isset(\$embeddedData[\$embeddedMetadata->identifier]) ? \$embeddedData[\$embeddedMetadata->identifier] : null;
338
339
            if (empty(\$hints[Query::HINT_READ_ONLY])) {
340
                \$this->unitOfWork->registerManaged(\$return, \$embeddedId, \$embeddedData);
341
            }
342
343
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
344
            \$hydratedData['%2\$s'] = \$return;
345
        }
346
347
EOF
348
                    ,
349 45
                    $mapping['name'],
350 182
                    $mapping['fieldName']
351
                );
352
            }
353
        }
354
355 182
        $namespace = $this->hydratorNamespace;
356 182
        $code      = sprintf(
357
            <<<EOF
358
<?php
359
360 182
namespace $namespace;
361
362
use Doctrine\ODM\MongoDB\DocumentManager;
363
use Doctrine\ODM\MongoDB\Hydrator\HydratorInterface;
364
use Doctrine\ODM\MongoDB\Query\Query;
365
use Doctrine\ODM\MongoDB\UnitOfWork;
366
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
367
368
/**
369
 * THIS CLASS WAS GENERATED BY THE DOCTRINE ODM. DO NOT EDIT THIS FILE.
370
 */
371 182
class $hydratorClassName implements HydratorInterface
372
{
373
    private \$dm;
374
    private \$unitOfWork;
375
    private \$class;
376
377
    public function __construct(DocumentManager \$dm, UnitOfWork \$uow, ClassMetadata \$class)
378
    {
379
        \$this->dm = \$dm;
380
        \$this->unitOfWork = \$uow;
381
        \$this->class = \$class;
382
    }
383
384
    public function hydrate(object \$document, array \$data, array \$hints = array()): array
385
    {
386
        \$hydratedData = array();
387
%s        return \$hydratedData;
388
    }
389
}
390
EOF
391
            ,
392 182
            $code
393
        );
394
395 182
        if ($fileName === false) {
396
            if (! class_exists($namespace . '\\' . $hydratorClassName)) {
397
                eval(substr($code, 5));
398
            }
399
        } else {
400 182
            $parentDirectory = dirname($fileName);
401
402 182
            if (! is_dir($parentDirectory) && (@mkdir($parentDirectory, 0775, true) === false)) {
403
                throw HydratorException::hydratorDirectoryNotWritable();
404
            }
405
406 182
            if (! is_writable($parentDirectory)) {
407
                throw HydratorException::hydratorDirectoryNotWritable();
408
            }
409
410 182
            $tmpFileName = $fileName . '.' . uniqid('', true);
411 182
            file_put_contents($tmpFileName, $code);
412 182
            rename($tmpFileName, $fileName);
413 182
            chmod($fileName, 0664);
414
        }
415 182
    }
416
417
    /**
418
     * Hydrate array of MongoDB document data into the given document object.
419
     *
420
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
421
     */
422 398
    public function hydrate(object $document, array $data, array $hints = []) : array
423
    {
424 398
        $metadata = $this->dm->getClassMetadata(get_class($document));
425
        // Invoke preLoad lifecycle events and listeners
426 398
        if (! empty($metadata->lifecycleCallbacks[Events::preLoad])) {
427 14
            $args = [new PreLoadEventArgs($document, $this->dm, $data)];
428 14
            $metadata->invokeLifecycleCallbacks(Events::preLoad, $document, $args);
429
        }
430 398
        if ($this->evm->hasListeners(Events::preLoad)) {
431 3
            $this->evm->dispatchEvent(Events::preLoad, new PreLoadEventArgs($document, $this->dm, $data));
432
        }
433
434
        // alsoLoadMethods may transform the document before hydration
435 398
        if (! empty($metadata->alsoLoadMethods)) {
436 12
            foreach ($metadata->alsoLoadMethods as $method => $fieldNames) {
437 12
                foreach ($fieldNames as $fieldName) {
438
                    // Invoke the method only once for the first field we find
439 12
                    if (array_key_exists($fieldName, $data)) {
440 8
                        $document->$method($data[$fieldName]);
441 12
                        continue 2;
442
                    }
443
                }
444
            }
445
        }
446
447 398
        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...
448 79
            $document->setProxyInitializer(null);
449 79
        }
450 79
451
        $data = $this->getHydratorFor($metadata->name)->hydrate($document, $data, $hints);
452
453 398
        // Invoke the postLoad lifecycle callbacks and listeners
454
        if (! empty($metadata->lifecycleCallbacks[Events::postLoad])) {
455 398
            $metadata->invokeLifecycleCallbacks(Events::postLoad, $document, [new LifecycleEventArgs($document, $this->dm)]);
456
        }
457 79
        if ($this->evm->hasListeners(Events::postLoad)) {
458 79
            $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($document, $this->dm));
459 31
        }
460 26
461
        return $data;
462
    }
463
}
464