Completed
Pull Request — master (#2076)
by Olivier
01:54
created

HydratorFactory   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 446
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 82.17%

Importance

Changes 0
Metric Value
wmc 45
lcom 1
cbo 6
dl 0
loc 446
ccs 106
cts 129
cp 0.8217
rs 8.8
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 3
A setUnitOfWork() 0 4 1
B getHydratorFor() 0 37 8
A generateHydratorClasses() 0 10 3
F generateHydratorClass() 0 259 20
B hydrate() 0 41 10

How to fix   Complexity   

Complex Class

Complex classes like HydratorFactory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HydratorFactory, and based on these observations, apply Extract Interface, too.

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 1779
    public function __construct(DocumentManager $dm, EventManager $evm, ?string $hydratorDir, ?string $hydratorNs, int $autoGenerate)
94
    {
95 1779
        if (! $hydratorDir) {
96
            throw HydratorException::hydratorDirectoryRequired();
97
        }
98 1779
        if (! $hydratorNs) {
99
            throw HydratorException::hydratorNamespaceRequired();
100
        }
101 1779
        $this->dm                = $dm;
102 1779
        $this->evm               = $evm;
103 1779
        $this->hydratorDir       = $hydratorDir;
104 1779
        $this->hydratorNamespace = $hydratorNs;
105 1779
        $this->autoGenerate      = $autoGenerate;
106 1779
    }
107
108
    /**
109
     * Sets the UnitOfWork instance.
110
     *
111
     * @internal
112
     */
113 1779
    public function setUnitOfWork(UnitOfWork $uow) : void
114
    {
115 1779
        $this->unitOfWork = $uow;
116 1779
    }
117
118
    /**
119
     * Gets the hydrator object for the given document class.
120
     */
121 394
    public function getHydratorFor(string $className) : HydratorInterface
122
    {
123 394
        if (isset($this->hydrators[$className])) {
124 213
            return $this->hydrators[$className];
125
        }
126 394
        $hydratorClassName = str_replace('\\', '', $className) . 'Hydrator';
127 394
        $fqn               = $this->hydratorNamespace . '\\' . $hydratorClassName;
128 394
        $class             = $this->dm->getClassMetadata($className);
129
130 394
        if (! class_exists($fqn, false)) {
131 180
            $fileName = $this->hydratorDir . DIRECTORY_SEPARATOR . $hydratorClassName . '.php';
132 180
            switch ($this->autoGenerate) {
133
                case Configuration::AUTOGENERATE_NEVER:
134
                    require $fileName;
135
                    break;
136
137
                case Configuration::AUTOGENERATE_ALWAYS:
138 180
                    $this->generateHydratorClass($class, $hydratorClassName, $fileName);
139 180
                    require $fileName;
140 180
                    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 394
        $this->hydrators[$className] = new $fqn($this->dm, $this->unitOfWork, $class);
155
156 394
        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 180
    private function generateHydratorClass(ClassMetadata $class, string $hydratorClassName, ?string $fileName) : void
179
    {
180 180
        $code = '';
181
182 180
        foreach ($class->fieldMappings as $fieldName => $mapping) {
183 180
            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 180
            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 180
            } elseif (! isset($mapping['association'])) {
219 180
                $code .= sprintf(
220
                    <<<EOF
221
222 180
        /** @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 180
                    $mapping['name'],
238 180
                    $mapping['fieldName'],
239 180
                    Type::getType($mapping['type'])->closureToPHP()
240
                );
241 119
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isOwningSide']) {
242 51
                $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 51
                    $mapping['name'],
265 51
                    $mapping['fieldName'],
266 51
                    $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 101
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isInverseSide']) {
269 6
                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 6
                    $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 6
                        $mapping['name'],
305 6
                        $mapping['fieldName']
306
                    );
307
                }
308 100
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_MANY || $mapping['association'] === ClassMetadata::EMBED_MANY) {
309 79
                $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 79
                    $mapping['name'],
332 79
                    $mapping['fieldName'],
333 79
                    $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 45
            } elseif ($mapping['association'] === ClassMetadata::EMBED_ONE) {
336 45
                $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 45
                    $mapping['name'],
367 45
                    $mapping['fieldName'],
368 45
                    $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 180
        $namespace = $this->hydratorNamespace;
374 180
        $code      = sprintf(
375
            <<<EOF
376
<?php
377
378 180
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 180
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 180
            $code
412
        );
413
414 180
        if ($fileName === null) {
415
            if (! class_exists($namespace . '\\' . $hydratorClassName)) {
416
                eval(substr($code, 5));
417
            }
418
419
            return;
420
        }
421
422 180
        $parentDirectory = dirname($fileName);
423
424 180
        if (! is_dir($parentDirectory) && (@mkdir($parentDirectory, 0775, true) === false)) {
425
            throw HydratorException::hydratorDirectoryNotWritable();
426
        }
427
428 180
        if (! is_writable($parentDirectory)) {
429
            throw HydratorException::hydratorDirectoryNotWritable();
430
        }
431
432 180
        $tmpFileName = $fileName . '.' . uniqid('', true);
433 180
        file_put_contents($tmpFileName, $code);
434 180
        rename($tmpFileName, $fileName);
435 180
        chmod($fileName, 0664);
436 180
    }
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 394
    public function hydrate(object $document, array $data, array $hints = []) : array
444
    {
445 394
        $metadata = $this->dm->getClassMetadata(get_class($document));
446
        // Invoke preLoad lifecycle events and listeners
447 394
        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 394
        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 394
        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 394
        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...
469 73
            $document->setProxyInitializer(null);
470
        }
471
472 394
        $data = $this->getHydratorFor($metadata->name)->hydrate($document, $data, $hints);
473
474
        // Invoke the postLoad lifecycle callbacks and listeners
475 390
        if (! empty($metadata->lifecycleCallbacks[Events::postLoad])) {
476 13
            $metadata->invokeLifecycleCallbacks(Events::postLoad, $document, [new LifecycleEventArgs($document, $this->dm)]);
477
        }
478 390
        if ($this->evm->hasListeners(Events::postLoad)) {
479 4
            $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($document, $this->dm));
480
        }
481
482 390
        return $data;
483
    }
484
}
485