HydratorFactory::generateHydratorClass()   F
last analyzed

Complexity

Conditions 20
Paths 85

Size

Total Lines 259

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 59
CRAP Score 20.1905

Importance

Changes 0
Metric Value
dl 0
loc 259
ccs 59
cts 64
cp 0.9219
rs 3.3333
c 0
b 0
f 0
cc 20
nc 85
nop 3
crap 20.1905

How to fix   Long Method    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
final 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
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 1832
    public function __construct(DocumentManager $dm, EventManager $evm, ?string $hydratorDir, ?string $hydratorNs, int $autoGenerate)
94
    {
95 1832
        if (! $hydratorDir) {
96
            throw HydratorException::hydratorDirectoryRequired();
97
        }
98 1832
        if (! $hydratorNs) {
99
            throw HydratorException::hydratorNamespaceRequired();
100
        }
101 1832
        $this->dm                = $dm;
102 1832
        $this->evm               = $evm;
103 1832
        $this->hydratorDir       = $hydratorDir;
104 1832
        $this->hydratorNamespace = $hydratorNs;
105 1832
        $this->autoGenerate      = $autoGenerate;
106 1832
    }
107
108
    /**
109
     * Sets the UnitOfWork instance.
110
     *
111
     * @internal
112
     */
113 1832
    public function setUnitOfWork(UnitOfWork $uow) : void
114
    {
115 1832
        $this->unitOfWork = $uow;
116 1832
    }
117
118
    /**
119
     * Gets the hydrator object for the given document class.
120
     */
121 399
    public function getHydratorFor(string $className) : HydratorInterface
122
    {
123 399
        if (isset($this->hydrators[$className])) {
124 215
            return $this->hydrators[$className];
125
        }
126 399
        $hydratorClassName = str_replace('\\', '', $className) . 'Hydrator';
127 399
        $fqn               = $this->hydratorNamespace . '\\' . $hydratorClassName;
128 399
        $class             = $this->dm->getClassMetadata($className);
129
130 399
        if (! class_exists($fqn, false)) {
131 184
            $fileName = $this->hydratorDir . DIRECTORY_SEPARATOR . $hydratorClassName . '.php';
132 184
            switch ($this->autoGenerate) {
133
                case Configuration::AUTOGENERATE_NEVER:
134
                    require $fileName;
135
                    break;
136
137
                case Configuration::AUTOGENERATE_ALWAYS:
138 184
                    $this->generateHydratorClass($class, $hydratorClassName, $fileName);
139 184
                    require $fileName;
140 184
                    break;
141
142
                case Configuration::AUTOGENERATE_FILE_NOT_EXISTS:
143
                    if (! file_exists($fileName)) {
144
                        $this->generateHydratorClass($class, $hydratorClassName, $fileName);
145
                    }
146
                    require $fileName;
147
                    break;
148
149
                case Configuration::AUTOGENERATE_EVAL:
150
                    $this->generateHydratorClass($class, $hydratorClassName, null);
151
                    break;
152
            }
153
        }
154 399
        $this->hydrators[$className] = new $fqn($this->dm, $this->unitOfWork, $class);
155
156 399
        return $this->hydrators[$className];
157
    }
158
159
    /**
160
     * Generates hydrator classes for all given classes.
161
     *
162
     * @param array  $classes The classes (ClassMetadata instances) for which to generate hydrators.
163
     * @param string $toDir   The target directory of the hydrator classes. If not specified, the
164
     *                        directory configured on the Configuration of the DocumentManager used
165
     *                        by this factory is used.
166
     */
167
    public function generateHydratorClasses(array $classes, ?string $toDir = null) : void
168
    {
169
        $hydratorDir = $toDir ?: $this->hydratorDir;
170
        $hydratorDir = rtrim($hydratorDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
171
        foreach ($classes as $class) {
172
            $hydratorClassName = str_replace('\\', '', $class->name) . 'Hydrator';
173
            $hydratorFileName  = $hydratorDir . $hydratorClassName . '.php';
174
            $this->generateHydratorClass($class, $hydratorClassName, $hydratorFileName);
175
        }
176
    }
177
178 184
    private function generateHydratorClass(ClassMetadata $class, string $hydratorClassName, ?string $fileName) : void
179
    {
180 184
        $code = '';
181
182 184
        foreach ($class->fieldMappings as $fieldName => $mapping) {
183 184
            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 184
            if ($mapping['type'] === 'date') {
201 10
                $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 10
                    $mapping['name'],
215 10
                    $mapping['fieldName'],
216 10
                    Type::getType($mapping['type'])->closureToPHP()
217
                );
218 184
            } elseif (! isset($mapping['association'])) {
219 184
                $code .= sprintf(
220
                    <<<EOF
221
222 184
        /** @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 184
                    $mapping['name'],
238 184
                    $mapping['fieldName'],
239 184
                    Type::getType($mapping['type'])->closureToPHP()
240
                );
241 123
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isOwningSide']) {
242 54
                $code .= sprintf(
243
                    <<<EOF
244
245
        /** @ReferenceOne */
246
        if (isset(\$data['%1\$s'])) {
247
            \$reference = \$data['%1\$s'];
248
249
            if (\$this->class->fieldMappings['%2\$s']['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID && ! is_array(\$reference)) {
250
                throw HydratorException::associationTypeMismatch('%3\$s', '%1\$s', 'array', gettype(\$reference));
251
            }
252
253
            \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$reference);
254
            \$identifier = ClassMetadata::getReferenceId(\$reference, \$this->class->fieldMappings['%2\$s']['storeAs']);
255
            \$targetMetadata = \$this->dm->getClassMetadata(\$className);
256
            \$id = \$targetMetadata->getPHPIdentifierValue(\$identifier);
257
            \$return = \$this->dm->getReference(\$className, \$id);
258
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
259
            \$hydratedData['%2\$s'] = \$return;
260
        }
261
262
EOF
263
                    ,
264 54
                    $mapping['name'],
265 54
                    $mapping['fieldName'],
266 54
                    $class->getName()
0 ignored issues
show
Bug introduced by
Consider using $class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
267
                );
268 103
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isInverseSide']) {
269 7
                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 7
                    $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 7
                        $mapping['name'],
305 7
                        $mapping['fieldName']
306
                    );
307
                }
308 102
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_MANY || $mapping['association'] === ClassMetadata::EMBED_MANY) {
309 80
                $code .= sprintf(
310
                    <<<EOF
311
312
        /** @Many */
313
        \$mongoData = isset(\$data['%1\$s']) ? \$data['%1\$s'] : null;
314
315
        if (\$mongoData !== null && ! is_array(\$mongoData)) {
316
            throw HydratorException::associationTypeMismatch('%3\$s', '%1\$s', 'array', gettype(\$mongoData));
317
        }
318
319
        \$return = \$this->dm->getConfiguration()->getPersistentCollectionFactory()->create(\$this->dm, \$this->class->fieldMappings['%2\$s']);
320
        \$return->setHints(\$hints);
321
        \$return->setOwner(\$document, \$this->class->fieldMappings['%2\$s']);
322
        \$return->setInitialized(false);
323
        if (\$mongoData) {
324
            \$return->setMongoData(\$mongoData);
325
        }
326
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
327
        \$hydratedData['%2\$s'] = \$return;
328
329
EOF
330
                    ,
331 80
                    $mapping['name'],
332 80
                    $mapping['fieldName'],
333 80
                    $class->getName()
0 ignored issues
show
Bug introduced by
Consider using $class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
334
                );
335 46
            } elseif ($mapping['association'] === ClassMetadata::EMBED_ONE) {
336 46
                $code .= sprintf(
337
                    <<<EOF
338
339
        /** @EmbedOne */
340
        if (isset(\$data['%1\$s'])) {
341
            \$embeddedDocument = \$data['%1\$s'];
342
343
            if (! is_array(\$embeddedDocument)) {
344
                throw HydratorException::associationTypeMismatch('%3\$s', '%1\$s', 'array', gettype(\$embeddedDocument));
345
            }
346
        
347
            \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$embeddedDocument);
348
            \$embeddedMetadata = \$this->dm->getClassMetadata(\$className);
349
            \$return = \$embeddedMetadata->newInstance();
350
351
            \$this->unitOfWork->setParentAssociation(\$return, \$this->class->fieldMappings['%2\$s'], \$document, '%1\$s');
352
353
            \$embeddedData = \$this->dm->getHydratorFactory()->hydrate(\$return, \$embeddedDocument, \$hints);
354
            \$embeddedId = \$embeddedMetadata->identifier && isset(\$embeddedData[\$embeddedMetadata->identifier]) ? \$embeddedData[\$embeddedMetadata->identifier] : null;
355
356
            if (empty(\$hints[Query::HINT_READ_ONLY])) {
357
                \$this->unitOfWork->registerManaged(\$return, \$embeddedId, \$embeddedData);
358
            }
359
360
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
361
            \$hydratedData['%2\$s'] = \$return;
362
        }
363
364
EOF
365
                    ,
366 46
                    $mapping['name'],
367 46
                    $mapping['fieldName'],
368 46
                    $class->getName()
0 ignored issues
show
Bug introduced by
Consider using $class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
369
                );
370
            }
371
        }
372
373 184
        $namespace = $this->hydratorNamespace;
374 184
        $code      = sprintf(
375
            <<<EOF
376
<?php
377
378 184
namespace $namespace;
379
380
use Doctrine\ODM\MongoDB\DocumentManager;
381
use Doctrine\ODM\MongoDB\Hydrator\HydratorException;
382
use Doctrine\ODM\MongoDB\Hydrator\HydratorInterface;
383
use Doctrine\ODM\MongoDB\Query\Query;
384
use Doctrine\ODM\MongoDB\UnitOfWork;
385
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
386
387
/**
388
 * THIS CLASS WAS GENERATED BY THE DOCTRINE ODM. DO NOT EDIT THIS FILE.
389
 */
390 184
class $hydratorClassName implements HydratorInterface
391
{
392
    private \$dm;
393
    private \$unitOfWork;
394
    private \$class;
395
396
    public function __construct(DocumentManager \$dm, UnitOfWork \$uow, ClassMetadata \$class)
397
    {
398
        \$this->dm = \$dm;
399
        \$this->unitOfWork = \$uow;
400
        \$this->class = \$class;
401
    }
402
403
    public function hydrate(object \$document, array \$data, array \$hints = array()): array
404
    {
405
        \$hydratedData = array();
406
%s        return \$hydratedData;
407
    }
408
}
409
EOF
410
            ,
411 184
            $code
412
        );
413
414 184
        if ($fileName === null) {
415
            if (! class_exists($namespace . '\\' . $hydratorClassName)) {
416
                eval(substr($code, 5));
417
            }
418
419
            return;
420
        }
421
422 184
        $parentDirectory = dirname($fileName);
423
424 184
        if (! is_dir($parentDirectory) && (@mkdir($parentDirectory, 0775, true) === false)) {
425
            throw HydratorException::hydratorDirectoryNotWritable();
426
        }
427
428 184
        if (! is_writable($parentDirectory)) {
429
            throw HydratorException::hydratorDirectoryNotWritable();
430
        }
431
432 184
        $tmpFileName = $fileName . '.' . uniqid('', true);
433 184
        file_put_contents($tmpFileName, $code);
434 184
        rename($tmpFileName, $fileName);
435 184
        chmod($fileName, 0664);
436 184
    }
437
438
    /**
439
     * Hydrate array of MongoDB document data into the given document object.
440
     *
441
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
442
     */
443 399
    public function hydrate(object $document, array $data, array $hints = []) : array
444
    {
445 399
        $metadata = $this->dm->getClassMetadata(get_class($document));
446
        // Invoke preLoad lifecycle events and listeners
447 399
        if (! empty($metadata->lifecycleCallbacks[Events::preLoad])) {
448 14
            $args = [new PreLoadEventArgs($document, $this->dm, $data)];
449 14
            $metadata->invokeLifecycleCallbacks(Events::preLoad, $document, $args);
450
        }
451 399
        if ($this->evm->hasListeners(Events::preLoad)) {
452 3
            $this->evm->dispatchEvent(Events::preLoad, new PreLoadEventArgs($document, $this->dm, $data));
453
        }
454
455
        // alsoLoadMethods may transform the document before hydration
456 399
        if (! empty($metadata->alsoLoadMethods)) {
457 12
            foreach ($metadata->alsoLoadMethods as $method => $fieldNames) {
458 12
                foreach ($fieldNames as $fieldName) {
459
                    // Invoke the method only once for the first field we find
460 12
                    if (array_key_exists($fieldName, $data)) {
461 8
                        $document->$method($data[$fieldName]);
462 8
                        continue 2;
463
                    }
464
                }
465
            }
466
        }
467
468 399
        if ($document instanceof GhostObjectInterface && $document->getProxyInitializer() !== null) {
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...
469
            // Inject an empty initialiser to not load any object data
470
            $document->setProxyInitializer(static function (
471
                GhostObjectInterface $ghostObject,
472
                string $method, // we don't care
473
                array $parameters, // we don't care
474
                &$initializer,
475
                array $properties // we currently do not use this
0 ignored issues
show
Unused Code introduced by
The parameter $properties is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

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