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