Completed
Pull Request — master (#1787)
by Stefano
21:31
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\Proxy\Proxy;
15
use Doctrine\ODM\MongoDB\Types\Type;
16
use Doctrine\ODM\MongoDB\UnitOfWork;
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
 */
40
class HydratorFactory
41
{
42
    /**
43
     * The DocumentManager this factory is bound to.
44
     *
45
     * @var DocumentManager
46
     */
47
    private $dm;
48
49
    /**
50
     * The UnitOfWork used to coordinate object-level transactions.
51
     *
52
     * @var UnitOfWork
53
     */
54
    private $unitOfWork;
55
56
    /**
57
     * The EventManager associated with this Hydrator
58
     *
59
     * @var EventManager
60
     */
61
    private $evm;
62
63
    /**
64
     * Which algorithm to use to automatically (re)generate hydrator classes.
65
     *
66
     * @var int
67
     */
68
    private $autoGenerate;
69
70
    /**
71
     * The namespace that contains all hydrator classes.
72
     *
73
     * @var string
74
     */
75
    private $hydratorNamespace;
76
77
    /**
78
     * The directory that contains all hydrator classes.
79
     *
80
     * @var string
81
     */
82
    private $hydratorDir;
83
84
    /**
85
     * Array of instantiated document hydrators.
86
     *
87
     * @var array
88
     */
89
    private $hydrators = [];
90
91
    /**
92
     * @param string $hydratorDir
93
     * @param string $hydratorNs
94
     * @param int    $autoGenerate
95
     * @throws HydratorException
96
     */
97 1584
    public function __construct(DocumentManager $dm, EventManager $evm, $hydratorDir, $hydratorNs, $autoGenerate)
98
    {
99 1584
        if (! $hydratorDir) {
100
            throw HydratorException::hydratorDirectoryRequired();
101
        }
102 1584
        if (! $hydratorNs) {
103
            throw HydratorException::hydratorNamespaceRequired();
104
        }
105 1584
        $this->dm = $dm;
106 1584
        $this->evm = $evm;
107 1584
        $this->hydratorDir = $hydratorDir;
108 1584
        $this->hydratorNamespace = $hydratorNs;
109 1584
        $this->autoGenerate = $autoGenerate;
110 1584
    }
111
112
    /**
113
     * Sets the UnitOfWork instance.
114
     *
115
     */
116 1584
    public function setUnitOfWork(UnitOfWork $uow)
117
    {
118 1584
        $this->unitOfWork = $uow;
119 1584
    }
120
121
    /**
122
     * Gets the hydrator object for the given document class.
123
     *
124
     * @param string $className
125
     * @return HydratorInterface $hydrator
126
     */
127 367
    public function getHydratorFor($className)
128
    {
129 367
        if (isset($this->hydrators[$className])) {
130 196
            return $this->hydrators[$className];
131
        }
132 367
        $hydratorClassName = str_replace('\\', '', $className) . 'Hydrator';
133 367
        $fqn = $this->hydratorNamespace . '\\' . $hydratorClassName;
134 367
        $class = $this->dm->getClassMetadata($className);
135
136 367
        if (! class_exists($fqn, false)) {
137 175
            $fileName = $this->hydratorDir . DIRECTORY_SEPARATOR . $hydratorClassName . '.php';
138 175
            switch ($this->autoGenerate) {
139
                case Configuration::AUTOGENERATE_NEVER:
140
                    require $fileName;
141
                    break;
142
143
                case Configuration::AUTOGENERATE_ALWAYS:
144 175
                    $this->generateHydratorClass($class, $hydratorClassName, $fileName);
145 175
                    require $fileName;
146 175
                    break;
147
148
                case Configuration::AUTOGENERATE_FILE_NOT_EXISTS:
149
                    if (! file_exists($fileName)) {
150
                        $this->generateHydratorClass($class, $hydratorClassName, $fileName);
151
                    }
152
                    require $fileName;
153
                    break;
154
155
                case Configuration::AUTOGENERATE_EVAL:
156
                    $this->generateHydratorClass($class, $hydratorClassName, false);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
157
                    break;
158
            }
159
        }
160 367
        $this->hydrators[$className] = new $fqn($this->dm, $this->unitOfWork, $class);
161 367
        return $this->hydrators[$className];
162
    }
163
164
    /**
165
     * Generates hydrator classes for all given classes.
166
     *
167
     * @param array  $classes The classes (ClassMetadata instances) for which to generate hydrators.
168
     * @param string $toDir   The target directory of the hydrator classes. If not specified, the
169
     *                        directory configured on the Configuration of the DocumentManager used
170
     *                        by this factory is used.
171
     */
172
    public function generateHydratorClasses(array $classes, $toDir = null)
173
    {
174
        $hydratorDir = $toDir ?: $this->hydratorDir;
175
        $hydratorDir = rtrim($hydratorDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
176
        foreach ($classes as $class) {
177
            $hydratorClassName = str_replace('\\', '', $class->name) . 'Hydrator';
178
            $hydratorFileName = $hydratorDir . $hydratorClassName . '.php';
179
            $this->generateHydratorClass($class, $hydratorClassName, $hydratorFileName);
180
        }
181
    }
182
183
    /**
184
     * @param string $hydratorClassName
185
     * @param string $fileName
186
     */
187 175
    private function generateHydratorClass(ClassMetadata $class, $hydratorClassName, $fileName)
188
    {
189 175
        $code = '';
190
191 175
        foreach ($class->fieldMappings as $fieldName => $mapping) {
192 175
            if (isset($mapping['alsoLoadFields'])) {
193 5
                foreach ($mapping['alsoLoadFields'] as $name) {
194 5
                    $code .= sprintf(
195
                        <<<EOF
196
197 5
        /** @AlsoLoad("$name") */
198 5
        if (!array_key_exists('%1\$s', \$data) && array_key_exists('$name', \$data)) {
199 5
            \$data['%1\$s'] = \$data['$name'];
200
        }
201
202
EOF
203
                        ,
204 5
                        $mapping['name']
205
                    );
206
                }
207
            }
208
209 175
            if ($mapping['type'] === 'date') {
210 8
                $code .= sprintf(
211
                    <<<EOF
212
213
        /** @Field(type="date") */
214
        if (isset(\$data['%1\$s'])) {
215
            \$value = \$data['%1\$s'];
216
            %3\$s
217
            \$this->class->reflFields['%2\$s']->setValue(\$document, clone \$return);
218
            \$hydratedData['%2\$s'] = \$return;
219
        }
220
221
EOF
222
                    ,
223 8
                    $mapping['name'],
224 8
                    $mapping['fieldName'],
225 8
                    Type::getType($mapping['type'])->closureToPHP()
226
                );
227 175
            } elseif (! isset($mapping['association'])) {
228 175
                $code .= sprintf(
229
                    <<<EOF
230
231 175
        /** @Field(type="{$mapping['type']}") */
232
        if (isset(\$data['%1\$s']) || (! empty(\$this->class->fieldMappings['%2\$s']['nullable']) && array_key_exists('%1\$s', \$data))) {
233
            \$value = \$data['%1\$s'];
234
            if (\$value !== null) {
235
                \$typeIdentifier = \$this->class->fieldMappings['%2\$s']['type'];
236
                %3\$s
237
            } else {
238
                \$return = null;
239
            }
240
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
241
            \$hydratedData['%2\$s'] = \$return;
242
        }
243
244
EOF
245
                    ,
246 175
                    $mapping['name'],
247 175
                    $mapping['fieldName'],
248 175
                    Type::getType($mapping['type'])->closureToPHP()
249
                );
250 113 View Code Duplication
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isOwningSide']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
251 49
                $code .= sprintf(
252
                    <<<EOF
253
254
        /** @ReferenceOne */
255
        if (isset(\$data['%1\$s'])) {
256
            \$reference = \$data['%1\$s'];
257
            \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$reference);
258
            \$identifier = ClassMetadata::getReferenceId(\$reference, \$this->class->fieldMappings['%2\$s']['storeAs']);
259
            \$targetMetadata = \$this->dm->getClassMetadata(\$className);
260
            \$id = \$targetMetadata->getPHPIdentifierValue(\$identifier);
261
            \$return = \$this->dm->getReference(\$className, \$id);
262
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
263
            \$hydratedData['%2\$s'] = \$return;
264
        }
265
266
EOF
267
                    ,
268 49
                    $mapping['name'],
269 49
                    $mapping['fieldName']
270
                );
271 95
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isInverseSide']) {
272 5
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
273 1
                    $code .= sprintf(
274
                        <<<EOF
275
276
        \$className = \$this->class->fieldMappings['%2\$s']['targetDocument'];
277
        \$return = \$this->dm->getRepository(\$className)->%3\$s(\$document);
278
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
279
        \$hydratedData['%2\$s'] = \$return;
280
281
EOF
282
                        ,
283 1
                        $mapping['name'],
284 1
                        $mapping['fieldName'],
285 1
                        $mapping['repositoryMethod']
286
                    );
287
                } else {
288 5
                    $code .= sprintf(
289
                        <<<EOF
290
291
        \$mapping = \$this->class->fieldMappings['%2\$s'];
292
        \$className = \$mapping['targetDocument'];
293
        \$targetClass = \$this->dm->getClassMetadata(\$mapping['targetDocument']);
294
        \$mappedByMapping = \$targetClass->fieldMappings[\$mapping['mappedBy']];
295
        \$mappedByFieldName = ClassMetadata::getReferenceFieldName(\$mappedByMapping['storeAs'], \$mapping['mappedBy']);
296
        \$criteria = array_merge(
297
            array(\$mappedByFieldName => \$data['_id']),
298
            isset(\$this->class->fieldMappings['%2\$s']['criteria']) ? \$this->class->fieldMappings['%2\$s']['criteria'] : array()
299
        );
300
        \$sort = isset(\$this->class->fieldMappings['%2\$s']['sort']) ? \$this->class->fieldMappings['%2\$s']['sort'] : array();
301
        \$return = \$this->unitOfWork->getDocumentPersister(\$className)->load(\$criteria, null, array(), 0, \$sort);
302
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
303
        \$hydratedData['%2\$s'] = \$return;
304
305
EOF
306
                        ,
307 5
                        $mapping['name'],
308 5
                        $mapping['fieldName']
309
                    );
310
                }
311 94 View Code Duplication
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_MANY || $mapping['association'] === ClassMetadata::EMBED_MANY) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
312 74
                $code .= sprintf(
313
                    <<<EOF
314
315
        /** @Many */
316
        \$mongoData = isset(\$data['%1\$s']) ? \$data['%1\$s'] : null;
317
        \$return = \$this->dm->getConfiguration()->getPersistentCollectionFactory()->create(\$this->dm, \$this->class->fieldMappings['%2\$s']);
318
        \$return->setHints(\$hints);
319
        \$return->setOwner(\$document, \$this->class->fieldMappings['%2\$s']);
320
        \$return->setInitialized(false);
321
        if (\$mongoData) {
322
            \$return->setMongoData(\$mongoData);
323
        }
324
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
325
        \$hydratedData['%2\$s'] = \$return;
326
327
EOF
328
                    ,
329 74
                    $mapping['name'],
330 74
                    $mapping['fieldName']
331
                );
332 42
            } elseif ($mapping['association'] === ClassMetadata::EMBED_ONE) {
333 42
                $code .= sprintf(
334
                    <<<EOF
335
336
        /** @EmbedOne */
337
        if (isset(\$data['%1\$s'])) {
338
            \$embeddedDocument = \$data['%1\$s'];
339
            \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$embeddedDocument);
340
            \$embeddedMetadata = \$this->dm->getClassMetadata(\$className);
341
            \$return = \$embeddedMetadata->newInstance();
342
343
            \$this->unitOfWork->setParentAssociation(\$return, \$this->class->fieldMappings['%2\$s'], \$document, '%1\$s');
344
345
            \$embeddedData = \$this->dm->getHydratorFactory()->hydrate(\$return, \$embeddedDocument, \$hints);
346
            \$embeddedId = \$embeddedMetadata->identifier && isset(\$embeddedData[\$embeddedMetadata->identifier]) ? \$embeddedData[\$embeddedMetadata->identifier] : null;
347
348
            if (empty(\$hints[Query::HINT_READ_ONLY])) {
349
                \$this->unitOfWork->registerManaged(\$return, \$embeddedId, \$embeddedData);
350
            }
351
352
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
353
            \$hydratedData['%2\$s'] = \$return;
354
        }
355
356
EOF
357
                    ,
358 42
                    $mapping['name'],
359 175
                    $mapping['fieldName']
360
                );
361
            }
362
        }
363
364 175
        $namespace = $this->hydratorNamespace;
365 175
        $code = sprintf(
366
            <<<EOF
367
<?php
368
369 175
namespace $namespace;
370
371
use Doctrine\ODM\MongoDB\DocumentManager;
372
use Doctrine\ODM\MongoDB\Hydrator\HydratorInterface;
373
use Doctrine\ODM\MongoDB\Query\Query;
374
use Doctrine\ODM\MongoDB\UnitOfWork;
375
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
376
377
/**
378
 * THIS CLASS WAS GENERATED BY THE DOCTRINE ODM. DO NOT EDIT THIS FILE.
379
 */
380 175
class $hydratorClassName implements HydratorInterface
381
{
382
    private \$dm;
383
    private \$unitOfWork;
384
    private \$class;
385
386
    public function __construct(DocumentManager \$dm, UnitOfWork \$uow, ClassMetadata \$class)
387
    {
388
        \$this->dm = \$dm;
389
        \$this->unitOfWork = \$uow;
390
        \$this->class = \$class;
391
    }
392
393
    public function hydrate(\$document, \$data, array \$hints = array())
394
    {
395
        \$hydratedData = array();
396
%s        return \$hydratedData;
397
    }
398
}
399
EOF
400
            ,
401 175
            $code
402
        );
403
404 175
        if ($fileName === false) {
405
            if (! class_exists($namespace . '\\' . $hydratorClassName)) {
406
                eval(substr($code, 5));
407
            }
408
        } else {
409 175
            $parentDirectory = dirname($fileName);
410
411 175
            if (! is_dir($parentDirectory) && (@mkdir($parentDirectory, 0775, true) === false)) {
412
                throw HydratorException::hydratorDirectoryNotWritable();
413
            }
414
415 175
            if (! is_writable($parentDirectory)) {
416
                throw HydratorException::hydratorDirectoryNotWritable();
417
            }
418
419 175
            $tmpFileName = $fileName . '.' . uniqid('', true);
420 175
            file_put_contents($tmpFileName, $code);
421 175
            rename($tmpFileName, $fileName);
422 175
            chmod($fileName, 0664);
423
        }
424 175
    }
425
426
    /**
427
     * Hydrate array of MongoDB document data into the given document object.
428
     *
429
     * @param object $document The document object to hydrate the data into.
430
     * @param array  $data     The array of document data.
431
     * @param array  $hints    Any hints to account for during reconstitution/lookup of the document.
432
     * @return array $values The array of hydrated values.
433
     */
434 367
    public function hydrate($document, $data, array $hints = [])
435
    {
436 367
        $metadata = $this->dm->getClassMetadata(get_class($document));
437
        // Invoke preLoad lifecycle events and listeners
438 367
        if (! empty($metadata->lifecycleCallbacks[Events::preLoad])) {
439 14
            $args = [new PreLoadEventArgs($document, $this->dm, $data)];
440 14
            $metadata->invokeLifecycleCallbacks(Events::preLoad, $document, $args);
441
        }
442 367 View Code Duplication
        if ($this->evm->hasListeners(Events::preLoad)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
443 3
            $this->evm->dispatchEvent(Events::preLoad, new PreLoadEventArgs($document, $this->dm, $data));
444
        }
445
446
        // alsoLoadMethods may transform the document before hydration
447 367
        if (! empty($metadata->alsoLoadMethods)) {
448 12
            foreach ($metadata->alsoLoadMethods as $method => $fieldNames) {
449 12
                foreach ($fieldNames as $fieldName) {
450
                    // Invoke the method only once for the first field we find
451 12
                    if (array_key_exists($fieldName, $data)) {
452 8
                        $document->$method($data[$fieldName]);
453 12
                        continue 2;
454
                    }
455
                }
456
            }
457
        }
458
459 367
        if ($document instanceof Proxy) {
460 72
            $document->__isInitialized__ = true;
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
461 72
            $document->__setInitializer(null);
462 72
            $document->__setCloner(null);
463
        }
464
465 367
        $data = $this->getHydratorFor($metadata->name)->hydrate($document, $data, $hints);
466
467 367
        if ($document instanceof Proxy) {
468
            // lazy properties may be left uninitialized
469 72
            $properties = $document->__getLazyProperties();
470 72
            foreach ($properties as $propertyName => $property) {
471 30
                if (isset($document->$propertyName)) {
472 25
                    continue;
473
                }
474
475 9
                $document->$propertyName = $properties[$propertyName];
476
            }
477
        }
478
479
        // Invoke the postLoad lifecycle callbacks and listeners
480 367
        if (! empty($metadata->lifecycleCallbacks[Events::postLoad])) {
481 13
            $metadata->invokeLifecycleCallbacks(Events::postLoad, $document, [new LifecycleEventArgs($document, $this->dm)]);
482
        }
483 367 View Code Duplication
        if ($this->evm->hasListeners(Events::postLoad)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
484 4
            $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($document, $this->dm));
485
        }
486
487 367
        return $data;
488
    }
489
}
490