Completed
Pull Request — master (#1757)
by Maciej
15:59
created

HydratorFactory::__construct()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3.054

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 9
cts 11
cp 0.8182
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 10
nc 3
nop 5
crap 3.054
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 1576
    public function __construct(DocumentManager $dm, EventManager $evm, $hydratorDir, $hydratorNs, $autoGenerate)
98
    {
99 1576
        if (! $hydratorDir) {
100
            throw HydratorException::hydratorDirectoryRequired();
101
        }
102 1576
        if (! $hydratorNs) {
103
            throw HydratorException::hydratorNamespaceRequired();
104
        }
105 1576
        $this->dm = $dm;
106 1576
        $this->evm = $evm;
107 1576
        $this->hydratorDir = $hydratorDir;
108 1576
        $this->hydratorNamespace = $hydratorNs;
109 1576
        $this->autoGenerate = $autoGenerate;
110 1576
    }
111
112
    /**
113
     * Sets the UnitOfWork instance.
114
     *
115
     */
116 1576
    public function setUnitOfWork(UnitOfWork $uow)
117
    {
118 1576
        $this->unitOfWork = $uow;
119 1576
    }
120
121
    /**
122
     * Gets the hydrator object for the given document class.
123
     *
124
     * @param string $className
125
     * @return HydratorInterface $hydrator
126
     */
127 366
    public function getHydratorFor($className)
128
    {
129 366
        if (isset($this->hydrators[$className])) {
130 199
            return $this->hydrators[$className];
131
        }
132 366
        $hydratorClassName = str_replace('\\', '', $className) . 'Hydrator';
133 366
        $fqn = $this->hydratorNamespace . '\\' . $hydratorClassName;
134 366
        $class = $this->dm->getClassMetadata($className);
135
136 366
        if (! class_exists($fqn, false)) {
137 174
            $fileName = $this->hydratorDir . DIRECTORY_SEPARATOR . $hydratorClassName . '.php';
138 174
            switch ($this->autoGenerate) {
139
                case Configuration::AUTOGENERATE_NEVER:
140
                    require $fileName;
141
                    break;
142
143
                case Configuration::AUTOGENERATE_ALWAYS:
144 174
                    $this->generateHydratorClass($class, $hydratorClassName, $fileName);
145 174
                    require $fileName;
146 174
                    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 366
        $this->hydrators[$className] = new $fqn($this->dm, $this->unitOfWork, $class);
161 366
        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 174
    private function generateHydratorClass(ClassMetadata $class, $hydratorClassName, $fileName)
188
    {
189 174
        $code = '';
190
191 174
        foreach ($class->fieldMappings as $fieldName => $mapping) {
192 174
            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 174
            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 174
            } elseif (! isset($mapping['association'])) {
228 174
                $code .= sprintf(
229
                    <<<EOF
230
231 174
        /** @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 174
                    $mapping['name'],
247 174
                    $mapping['fieldName'],
248 174
                    Type::getType($mapping['type'])->closureToPHP()
249
                );
250 112 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 94
            } 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 93 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 73
                $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 73
                    $mapping['name'],
330 73
                    $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 174
                    $mapping['fieldName']
360
                );
361
            }
362
        }
363
364 174
        $namespace = $this->hydratorNamespace;
365 174
        $code = sprintf(
366
            <<<EOF
367
<?php
368
369 174
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 174
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 174
            $code
402
        );
403
404 174
        if ($fileName === false) {
405
            if (! class_exists($namespace . '\\' . $hydratorClassName)) {
406
                eval(substr($code, 5));
407
            }
408
        } else {
409 174
            $parentDirectory = dirname($fileName);
410
411 174
            if (! is_dir($parentDirectory) && (@mkdir($parentDirectory, 0775, true) === false)) {
412
                throw HydratorException::hydratorDirectoryNotWritable();
413
            }
414
415 174
            if (! is_writable($parentDirectory)) {
416
                throw HydratorException::hydratorDirectoryNotWritable();
417
            }
418
419 174
            $tmpFileName = $fileName . '.' . uniqid('', true);
420 174
            file_put_contents($tmpFileName, $code);
421 174
            rename($tmpFileName, $fileName);
422 174
            chmod($fileName, 0664);
423
        }
424 174
    }
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 366
    public function hydrate($document, $data, array $hints = [])
435
    {
436 366
        $metadata = $this->dm->getClassMetadata(get_class($document));
437
        // Invoke preLoad lifecycle events and listeners
438 366
        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 366 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 366
        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 366
        $data = $this->getHydratorFor($metadata->name)->hydrate($document, $data, $hints);
460 366
        if ($document instanceof Proxy) {
461 71
            $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...
462 71
            $document->__setInitializer(null);
463 71
            $document->__setCloner(null);
464
            // lazy properties may be left uninitialized
465 71
            $properties = $document->__getLazyProperties();
466 71
            foreach ($properties as $propertyName => $property) {
467 29
                if (isset($document->$propertyName)) {
468 24
                    continue;
469
                }
470
471 9
                $document->$propertyName = $properties[$propertyName];
472
            }
473
        }
474
475
        // Invoke the postLoad lifecycle callbacks and listeners
476 366
        if (! empty($metadata->lifecycleCallbacks[Events::postLoad])) {
477 13
            $metadata->invokeLifecycleCallbacks(Events::postLoad, $document, [new LifecycleEventArgs($document, $this->dm)]);
478
        }
479 366 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...
480 4
            $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($document, $this->dm));
481
        }
482
483 366
        return $data;
484
    }
485
}
486