Completed
Pull Request — master (#1309)
by Jefersson
09:54
created

HydratorFactory::getHydratorFor()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 36
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 10.6235
Metric Value
dl 0
loc 36
ccs 19
cts 29
cp 0.6552
rs 5.3846
cc 8
eloc 26
nc 8
nop 1
crap 10.6235
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 923
    public function __construct(DocumentManager $dm, EventManager $evm, $hydratorDir, $hydratorNs, $autoGenerate)
100
    {
101 923
        if ( ! $hydratorDir) {
102
            throw HydratorException::hydratorDirectoryRequired();
103
        }
104 923
        if ( ! $hydratorNs) {
105
            throw HydratorException::hydratorNamespaceRequired();
106
        }
107 923
        $this->dm = $dm;
108 923
        $this->evm = $evm;
109 923
        $this->hydratorDir = $hydratorDir;
110 923
        $this->hydratorNamespace = $hydratorNs;
111 923
        $this->autoGenerate = $autoGenerate;
112 923
    }
113
114
    /**
115
     * Sets the UnitOfWork instance.
116
     *
117
     * @param UnitOfWork $uow
118
     */
119 923
    public function setUnitOfWork(UnitOfWork $uow)
120
    {
121 923
        $this->unitOfWork = $uow;
122 923
    }
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
        $className = $class->name;
0 ignored issues
show
Unused Code introduced by
$className is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
361 167
        $namespace = $this->hydratorNamespace;
362 167
        $code = sprintf(<<<EOF
363
<?php
364
365
namespace $namespace;
366
367
use Doctrine\ODM\MongoDB\DocumentManager;
368
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
369
use Doctrine\ODM\MongoDB\Hydrator\HydratorInterface;
370
use Doctrine\ODM\MongoDB\UnitOfWork;
371
372
/**
373
 * THIS CLASS WAS GENERATED BY THE DOCTRINE ODM. DO NOT EDIT THIS FILE.
374
 */
375 167
class $hydratorClassName implements HydratorInterface
376
{
377
    private \$dm;
378
    private \$unitOfWork;
379
    private \$class;
380
381
    public function __construct(DocumentManager \$dm, UnitOfWork \$uow, ClassMetadata \$class)
382
    {
383
        \$this->dm = \$dm;
384
        \$this->unitOfWork = \$uow;
385
        \$this->class = \$class;
386
    }
387
388
    public function hydrate(\$document, \$data, array \$hints = array())
389
    {
390
        \$hydratedData = array();
391
%s        return \$hydratedData;
392
    }
393 167
}
394
EOF
395 167
            ,
396
            $code
397 167
        );
398
399 167
        if ($fileName === false) {
400
            if ( ! class_exists($namespace . '\\' . $hydratorClassName)) {
401
                eval(substr($code, 5));
402
            }
403
        } else {
404 167
            $parentDirectory = dirname($fileName);
405
406 167
            if ( ! is_dir($parentDirectory) && (false === @mkdir($parentDirectory, 0775, true))) {
407
                throw HydratorException::hydratorDirectoryNotWritable();
408
            }
409
410 167
            if ( ! is_writable($parentDirectory)) {
411
                throw HydratorException::hydratorDirectoryNotWritable();
412
            }
413
414 167
            $tmpFileName = $fileName . '.' . uniqid('', true);
415 167
            file_put_contents($tmpFileName, $code);
416 167
            rename($tmpFileName, $fileName);
417 167
            chmod($fileName, 0664);
418
        }
419 167
    }
420
421
    /**
422
     * Hydrate array of MongoDB document data into the given document object.
423
     *
424
     * @param object $document  The document object to hydrate the data into.
425
     * @param array $data The array of document data.
426
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
427
     * @return array $values The array of hydrated values.
428
     */
429 378
    public function hydrate($document, $data, array $hints = array())
430
    {
431 378
        $metadata = $this->dm->getClassMetadata(get_class($document));
432
        // Invoke preLoad lifecycle events and listeners
433 378
        if ( ! empty($metadata->lifecycleCallbacks[Events::preLoad])) {
434 14
            $args = array(new PreLoadEventArgs($document, $this->dm, $data));
435 14
            $metadata->invokeLifecycleCallbacks(Events::preLoad, $document, $args);
436 14
        }
437 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...
438 3
            $this->evm->dispatchEvent(Events::preLoad, new PreLoadEventArgs($document, $this->dm, $data));
439 3
        }
440
441
        // alsoLoadMethods may transform the document before hydration
442 378
        if ( ! empty($metadata->alsoLoadMethods)) {
443 12
            foreach ($metadata->alsoLoadMethods as $method => $fieldNames) {
444 12
                foreach ($fieldNames as $fieldName) {
445
                    // Invoke the method only once for the first field we find
446 12
                    if (array_key_exists($fieldName, $data)) {
447 8
                        $document->$method($data[$fieldName]);
448 8
                        continue 2;
449
                    }
450 10
                }
451 12
            }
452 12
        }
453
454 378
        $data = $this->getHydratorFor($metadata->name)->hydrate($document, $data, $hints);
455 378
        if ($document instanceof Proxy) {
456 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...
457 61
        }
458
459
        // Invoke the postLoad lifecycle callbacks and listeners
460 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...
461 13
            $metadata->invokeLifecycleCallbacks(Events::postLoad, $document, array(new LifecycleEventArgs($document, $this->dm)));
462 13
        }
463 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...
464 4
            $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($document, $this->dm));
465 4
        }
466
467 378
        return $data;
468
    }
469
}
470