Completed
Pull Request — master (#1321)
by Jefersson
14:35
created

HydratorFactory::hydrate()   D

Complexity

Conditions 10
Paths 64

Size

Total Lines 40
Code Lines 21

Duplication

Lines 9
Ratio 22.5 %

Code Coverage

Tests 29
CRAP Score 10

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 9
loc 40
ccs 29
cts 29
cp 1
rs 4.8197
cc 10
eloc 21
nc 64
nop 3
crap 10

How to fix   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
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB\Hydrator;
21
22
use Doctrine\Common\EventManager;
23
use Doctrine\ODM\MongoDB\DocumentManager;
24
use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs;
25
use Doctrine\ODM\MongoDB\Event\PreLoadEventArgs;
26
use Doctrine\ODM\MongoDB\Events;
27
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
28
use Doctrine\ODM\MongoDB\Proxy\Proxy;
29
use Doctrine\ODM\MongoDB\Types\Type;
30
use Doctrine\ODM\MongoDB\UnitOfWork;
31
use Doctrine\ODM\MongoDB\Configuration;
32
33
/**
34
 * The HydratorFactory class is responsible for instantiating a correct hydrator
35
 * type based on document's ClassMetadata
36
 *
37
 * @since       1.0
38
 * @author      Jonathan H. Wage <[email protected]>
39
 */
40
class HydratorFactory
41
{
42
    /**
43
     * The DocumentManager this factory is bound to.
44
     *
45
     * @var \Doctrine\ODM\MongoDB\DocumentManager
46
     */
47
    private $dm;
48
49
    /**
50
     * The UnitOfWork used to coordinate object-level transactions.
51
     *
52
     * @var \Doctrine\ODM\MongoDB\UnitOfWork
53
     */
54
    private $unitOfWork;
55
56
    /**
57
     * The EventManager associated with this Hydrator
58
     *
59
     * @var \Doctrine\Common\EventManager
60
     */
61
    private $evm;
62
63
    /**
64
     * Which algorithm to use to automatically (re)generate hydrator classes.
65
     *
66
     * @var integer
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 = array();
90
91
    /**
92
     * @param DocumentManager $dm
93
     * @param EventManager $evm
94
     * @param string $hydratorDir
95
     * @param string $hydratorNs
96
     * @param integer $autoGenerate
97
     * @throws HydratorException
98
     */
99 925
    public function __construct(DocumentManager $dm, EventManager $evm, $hydratorDir, $hydratorNs, $autoGenerate)
100
    {
101 925
        if ( ! $hydratorDir) {
102
            throw HydratorException::hydratorDirectoryRequired();
103
        }
104 925
        if ( ! $hydratorNs) {
105
            throw HydratorException::hydratorNamespaceRequired();
106
        }
107 925
        $this->dm = $dm;
108 925
        $this->evm = $evm;
109 925
        $this->hydratorDir = $hydratorDir;
110 925
        $this->hydratorNamespace = $hydratorNs;
111 925
        $this->autoGenerate = $autoGenerate;
112 925
    }
113
114
    /**
115
     * Sets the UnitOfWork instance.
116
     *
117
     * @param UnitOfWork $uow
118
     */
119 925
    public function setUnitOfWork(UnitOfWork $uow)
120
    {
121 925
        $this->unitOfWork = $uow;
122 925
    }
123
124
    /**
125
     * Gets the hydrator object for the given document class.
126
     *
127
     * @param string $className
128
     * @return \Doctrine\ODM\MongoDB\Hydrator\HydratorInterface $hydrator
129
     */
130 378
    public function getHydratorFor($className)
131
    {
132 378
        if (isset($this->hydrators[$className])) {
133 195
            return $this->hydrators[$className];
134
        }
135 378
        $hydratorClassName = str_replace('\\', '', $className) . 'Hydrator';
136 378
        $fqn = $this->hydratorNamespace . '\\' . $hydratorClassName;
137 378
        $class = $this->dm->getClassMetadata($className);
138
139 378
        if ( ! class_exists($fqn, false)) {
140 167
            $fileName = $this->hydratorDir . DIRECTORY_SEPARATOR . $hydratorClassName . '.php';
141 167
            switch ($this->autoGenerate) {
142 167
                case Configuration::AUTOGENERATE_NEVER:
143
                    require $fileName;
144
                    break;
145
                    
146 167
                case Configuration::AUTOGENERATE_ALWAYS:
147 167
                    $this->generateHydratorClass($class, $hydratorClassName, $fileName);
148 167
                    require $fileName;
149 167
                    break;
150
                    
151
                case Configuration::AUTOGENERATE_FILE_NOT_EXISTS:
152 1
                    if (!file_exists($fileName)) {
153
                        $this->generateHydratorClass($class, $hydratorClassName, $fileName);
154
                    }
155
                    require $fileName;
156
                    break;
157
                    
158
                case Configuration::AUTOGENERATE_EVAL:
159
                    $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...
160
                    break;
161 167
            }
162 167
        }
163 378
        $this->hydrators[$className] = new $fqn($this->dm, $this->unitOfWork, $class);
164 378
        return $this->hydrators[$className];
165
    }
166
167
    /**
168
     * Generates hydrator classes for all given classes.
169
     *
170
     * @param array $classes The classes (ClassMetadata instances) for which to generate hydrators.
171
     * @param string $toDir The target directory of the hydrator classes. If not specified, the
172
     *                      directory configured on the Configuration of the DocumentManager used
173
     *                      by this factory is used.
174
     */
175
    public function generateHydratorClasses(array $classes, $toDir = null)
176
    {
177
        $hydratorDir = $toDir ?: $this->hydratorDir;
178
        $hydratorDir = rtrim($hydratorDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
179
        foreach ($classes as $class) {
180
            $hydratorClassName = str_replace('\\', '', $class->name) . 'Hydrator';
181
            $hydratorFileName = $hydratorDir . $hydratorClassName . '.php';
182
            $this->generateHydratorClass($class, $hydratorClassName, $hydratorFileName);
183
        }
184
    }
185
186
    /**
187
     * @param ClassMetadata $class
188
     * @param string $hydratorClassName
189
     * @param string $fileName
190
     */
191 167
    private function generateHydratorClass(ClassMetadata $class, $hydratorClassName, $fileName)
192
    {
193 167
        $code = '';
194
195 167
        foreach ($class->fieldMappings as $fieldName => $mapping) {
196 167
            if (isset($mapping['alsoLoadFields'])) {
197 4
                foreach ($mapping['alsoLoadFields'] as $name) {
198 4
                    $code .= sprintf(<<<EOF
199
200
        /** @AlsoLoad("$name") */
201 4
        if (!array_key_exists('%1\$s', \$data) && array_key_exists('$name', \$data)) {
202 4
            \$data['%1\$s'] = \$data['$name'];
203
        }
204
205
EOF
206 4
                        ,
207 4
                        $mapping['name']
208 4
                    );
209 4
                }
210 4
            }
211
212 167
            if ($mapping['type'] === 'date') {
213 9
                $code .= sprintf(<<<EOF
214
215
        /** @Field(type="date") */
216
        if (isset(\$data['%1\$s'])) {
217
            \$value = \$data['%1\$s'];
218
            %3\$s
219
            \$this->class->reflFields['%2\$s']->setValue(\$document, clone \$return);
220
            \$hydratedData['%2\$s'] = \$return;
221
        }
222
223
EOF
224 9
                    ,
225 9
                    $mapping['name'],
226 9
                    $mapping['fieldName'],
227 9
                    Type::getType($mapping['type'])->closureToPHP()
228 9
                );
229
230
231 167
            } elseif ( ! isset($mapping['association'])) {
232 167
                $code .= sprintf(<<<EOF
233
234 167
        /** @Field(type="{$mapping['type']}") */
235
        if (isset(\$data['%1\$s'])) {
236
            \$value = \$data['%1\$s'];
237
            %3\$s
238
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
239
            \$hydratedData['%2\$s'] = \$return;
240
        }
241
242
EOF
243 167
                    ,
244 167
                    $mapping['name'],
245 167
                    $mapping['fieldName'],
246 167
                    Type::getType($mapping['type'])->closureToPHP()
247 167
                );
248 167 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...
249 46
                $code .= sprintf(<<<EOF
250
251
        /** @ReferenceOne */
252
        if (isset(\$data['%1\$s'])) {
253
            \$reference = \$data['%1\$s'];
254
            if (isset(\$this->class->fieldMappings['%2\$s']['simple']) && \$this->class->fieldMappings['%2\$s']['simple']) {
255
                \$className = \$this->class->fieldMappings['%2\$s']['targetDocument'];
256
                \$mongoId = \$reference;
257
            } else {
258
                \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$reference);
259
                \$mongoId = \$reference['\$id'];
260
            }
261
            \$targetMetadata = \$this->dm->getClassMetadata(\$className);
262
            \$id = \$targetMetadata->getPHPIdentifierValue(\$mongoId);
263
            \$return = \$this->dm->getReference(\$className, \$id);
264
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
265
            \$hydratedData['%2\$s'] = \$return;
266
        }
267
268
EOF
269 46
                    ,
270 46
                    $mapping['name'],
271 46
                    $mapping['fieldName']
272 46
                );
273 110
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isInverseSide']) {
274 6
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
275 1
                    $code .= sprintf(<<<EOF
276
277
        \$className = \$this->class->fieldMappings['%2\$s']['targetDocument'];
278
        \$return = \$this->dm->getRepository(\$className)->%3\$s(\$document);
279
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
280
        \$hydratedData['%2\$s'] = \$return;
281
282
EOF
283 1
                        ,
284 1
                        $mapping['name'],
285 1
                        $mapping['fieldName'],
286 1
                        $mapping['repositoryMethod']
287 1
                    );
288 1
                } else {
289 6
                    $code .= sprintf(<<<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 = isset(\$mappedByMapping['simple']) && \$mappedByMapping['simple'] ? \$mapping['mappedBy'] : \$mapping['mappedBy'].'.\$id';
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 6
                        ,
307 6
                        $mapping['name'],
308 6
                        $mapping['fieldName']
309 6
                    );
310
                }
311 91 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 70
                $code .= sprintf(<<<EOF
313
314
        /** @Many */
315
        \$mongoData = isset(\$data['%1\$s']) ? \$data['%1\$s'] : null;
316
        \$return = new \Doctrine\ODM\MongoDB\PersistentCollection(new \Doctrine\Common\Collections\ArrayCollection(), \$this->dm, \$this->unitOfWork);
317
        \$return->setHints(\$hints);
318
        \$return->setOwner(\$document, \$this->class->fieldMappings['%2\$s']);
319
        \$return->setInitialized(false);
320
        if (\$mongoData) {
321
            \$return->setMongoData(\$mongoData);
322
        }
323
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
324
        \$hydratedData['%2\$s'] = \$return;
325
326
EOF
327 70
                    ,
328 70
                    $mapping['name'],
329 70
                    $mapping['fieldName']
330 70
                );
331 90
            } elseif ($mapping['association'] === ClassMetadata::EMBED_ONE) {
332 40
                $code .= sprintf(<<<EOF
333
334
        /** @EmbedOne */
335
        if (isset(\$data['%1\$s'])) {
336
            \$embeddedDocument = \$data['%1\$s'];
337
            \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$embeddedDocument);
338
            \$embeddedMetadata = \$this->dm->getClassMetadata(\$className);
339
            \$return = \$embeddedMetadata->newInstance();
340
341
            \$this->unitOfWork->setParentAssociation(\$return, \$this->class->fieldMappings['%2\$s'], \$document, '%1\$s');
342
343
            \$embeddedData = \$this->dm->getHydratorFactory()->hydrate(\$return, \$embeddedDocument, \$hints);
344
            \$embeddedId = \$embeddedMetadata->identifier && isset(\$embeddedData[\$embeddedMetadata->identifier]) ? \$embeddedData[\$embeddedMetadata->identifier] : null;
345
346
            \$this->unitOfWork->registerManaged(\$return, \$embeddedId, \$embeddedData);
347
348
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
349
            \$hydratedData['%2\$s'] = \$return;
350
        }
351
352
EOF
353 40
                    ,
354 40
                    $mapping['name'],
355 40
                    $mapping['fieldName']
356 40
                );
357 40
            }
358 167
        }
359
360 167
        $namespace = $this->hydratorNamespace;
361 167
        $code = sprintf(<<<EOF
362
<?php
363
364
namespace $namespace;
365
366
use Doctrine\ODM\MongoDB\DocumentManager;
367
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
368
use Doctrine\ODM\MongoDB\Hydrator\HydratorInterface;
369
use Doctrine\ODM\MongoDB\UnitOfWork;
370
371
/**
372
 * THIS CLASS WAS GENERATED BY THE DOCTRINE ODM. DO NOT EDIT THIS FILE.
373
 */
374 167
class $hydratorClassName implements HydratorInterface
375
{
376
    private \$dm;
377
    private \$unitOfWork;
378
    private \$class;
379
380
    public function __construct(DocumentManager \$dm, UnitOfWork \$uow, ClassMetadata \$class)
381
    {
382
        \$this->dm = \$dm;
383
        \$this->unitOfWork = \$uow;
384
        \$this->class = \$class;
385
    }
386
387
    public function hydrate(\$document, \$data, array \$hints = array())
388
    {
389
        \$hydratedData = array();
390
%s        return \$hydratedData;
391
    }
392 167
}
393
EOF
394 167
            ,
395
            $code
396 167
        );
397
398 167
        if ($fileName === false) {
399
            if ( ! class_exists($namespace . '\\' . $hydratorClassName)) {
400
                eval(substr($code, 5));
401
            }
402
        } else {
403 167
            $parentDirectory = dirname($fileName);
404
405 167
            if ( ! is_dir($parentDirectory) && (false === @mkdir($parentDirectory, 0775, true))) {
406
                throw HydratorException::hydratorDirectoryNotWritable();
407
            }
408
409 167
            if ( ! is_writable($parentDirectory)) {
410
                throw HydratorException::hydratorDirectoryNotWritable();
411
            }
412
413 167
            $tmpFileName = $fileName . '.' . uniqid('', true);
414 167
            file_put_contents($tmpFileName, $code);
415 167
            rename($tmpFileName, $fileName);
416 167
            chmod($fileName, 0664);
417
        }
418 167
    }
419
420
    /**
421
     * Hydrate array of MongoDB document data into the given document object.
422
     *
423
     * @param object $document  The document object to hydrate the data into.
424
     * @param array $data The array of document data.
425
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
426
     * @return array $values The array of hydrated values.
427
     */
428 378
    public function hydrate($document, $data, array $hints = array())
429
    {
430 378
        $metadata = $this->dm->getClassMetadata(get_class($document));
431
        // Invoke preLoad lifecycle events and listeners
432 378
        if ( ! empty($metadata->lifecycleCallbacks[Events::preLoad])) {
433 14
            $args = array(new PreLoadEventArgs($document, $this->dm, $data));
434 14
            $metadata->invokeLifecycleCallbacks(Events::preLoad, $document, $args);
435 14
        }
436 378 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...
437 3
            $this->evm->dispatchEvent(Events::preLoad, new PreLoadEventArgs($document, $this->dm, $data));
438 3
        }
439
440
        // alsoLoadMethods may transform the document before hydration
441 378
        if ( ! empty($metadata->alsoLoadMethods)) {
442 12
            foreach ($metadata->alsoLoadMethods as $method => $fieldNames) {
443 12
                foreach ($fieldNames as $fieldName) {
444
                    // Invoke the method only once for the first field we find
445 12
                    if (array_key_exists($fieldName, $data)) {
446 8
                        $document->$method($data[$fieldName]);
447 8
                        continue 2;
448
                    }
449 10
                }
450 12
            }
451 12
        }
452
453 378
        $data = $this->getHydratorFor($metadata->name)->hydrate($document, $data, $hints);
454 378
        if ($document instanceof Proxy) {
455 61
            $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...
456 61
        }
457
458
        // Invoke the postLoad lifecycle callbacks and listeners
459 378 View Code Duplication
        if ( ! empty($metadata->lifecycleCallbacks[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...
460 13
            $metadata->invokeLifecycleCallbacks(Events::postLoad, $document, array(new LifecycleEventArgs($document, $this->dm)));
461 13
        }
462 378 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...
463 4
            $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($document, $this->dm));
464 4
        }
465
466 378
        return $data;
467
    }
468
}
469