Completed
Push — master ( f66da8...61f8e0 )
by Maciej
19:24 queued 07:02
created

HydratorFactory::getHydratorFor()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 36
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 11.4924
Metric Value
dl 0
loc 36
ccs 18
cts 29
cp 0.6207
rs 5.3846
cc 8
eloc 26
nc 8
nop 1
crap 11.4924
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
 */
39
class HydratorFactory
40
{
41
    /**
42
     * The DocumentManager this factory is bound to.
43
     *
44
     * @var \Doctrine\ODM\MongoDB\DocumentManager
45
     */
46
    private $dm;
47
48
    /**
49
     * The UnitOfWork used to coordinate object-level transactions.
50
     *
51
     * @var \Doctrine\ODM\MongoDB\UnitOfWork
52
     */
53
    private $unitOfWork;
54
55
    /**
56
     * The EventManager associated with this Hydrator
57
     *
58
     * @var \Doctrine\Common\EventManager
59
     */
60
    private $evm;
61
62
    /**
63
     * Which algorithm to use to automatically (re)generate hydrator classes.
64
     *
65
     * @var integer
66
     */
67
    private $autoGenerate;
68
69
    /**
70
     * The namespace that contains all hydrator classes.
71
     *
72
     * @var string
73
     */
74
    private $hydratorNamespace;
75
76
    /**
77
     * The directory that contains all hydrator classes.
78
     *
79
     * @var string
80
     */
81
    private $hydratorDir;
82
83
    /**
84
     * Array of instantiated document hydrators.
85
     *
86
     * @var array
87
     */
88
    private $hydrators = array();
89
90
    /**
91
     * @param DocumentManager $dm
92
     * @param EventManager $evm
93
     * @param string $hydratorDir
94
     * @param string $hydratorNs
95
     * @param integer $autoGenerate
96
     * @throws HydratorException
97
     */
98 941
    public function __construct(DocumentManager $dm, EventManager $evm, $hydratorDir, $hydratorNs, $autoGenerate)
99
    {
100 941
        if ( ! $hydratorDir) {
101
            throw HydratorException::hydratorDirectoryRequired();
102
        }
103 941
        if ( ! $hydratorNs) {
104
            throw HydratorException::hydratorNamespaceRequired();
105
        }
106 941
        $this->dm = $dm;
107 941
        $this->evm = $evm;
108 941
        $this->hydratorDir = $hydratorDir;
109 941
        $this->hydratorNamespace = $hydratorNs;
110 941
        $this->autoGenerate = $autoGenerate;
111 941
    }
112
113
    /**
114
     * Sets the UnitOfWork instance.
115
     *
116
     * @param UnitOfWork $uow
117
     */
118 941
    public function setUnitOfWork(UnitOfWork $uow)
119
    {
120 941
        $this->unitOfWork = $uow;
121 941
    }
122
123
    /**
124
     * Gets the hydrator object for the given document class.
125
     *
126
     * @param string $className
127
     * @return \Doctrine\ODM\MongoDB\Hydrator\HydratorInterface $hydrator
128
     */
129 383
    public function getHydratorFor($className)
130
    {
131 383
        if (isset($this->hydrators[$className])) {
132 196
            return $this->hydrators[$className];
133
        }
134 383
        $hydratorClassName = str_replace('\\', '', $className) . 'Hydrator';
135 383
        $fqn = $this->hydratorNamespace . '\\' . $hydratorClassName;
136 383
        $class = $this->dm->getClassMetadata($className);
137
138 383
        if ( ! class_exists($fqn, false)) {
139 170
            $fileName = $this->hydratorDir . DIRECTORY_SEPARATOR . $hydratorClassName . '.php';
140 170
            switch ($this->autoGenerate) {
141 170
                case Configuration::AUTOGENERATE_NEVER:
142
                    require $fileName;
143
                    break;
144
                    
145 170
                case Configuration::AUTOGENERATE_ALWAYS:
146 170
                    $this->generateHydratorClass($class, $hydratorClassName, $fileName);
147 170
                    require $fileName;
148 170
                    break;
149
                    
150
                case Configuration::AUTOGENERATE_FILE_NOT_EXISTS:
151
                    if (!file_exists($fileName)) {
152
                        $this->generateHydratorClass($class, $hydratorClassName, $fileName);
153
                    }
154
                    require $fileName;
155
                    break;
156
                    
157
                case Configuration::AUTOGENERATE_EVAL:
158
                    $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...
159
                    break;
160 170
            }
161 170
        }
162 383
        $this->hydrators[$className] = new $fqn($this->dm, $this->unitOfWork, $class);
163 383
        return $this->hydrators[$className];
164
    }
165
166
    /**
167
     * Generates hydrator classes for all given classes.
168
     *
169
     * @param array $classes The classes (ClassMetadata instances) for which to generate hydrators.
170
     * @param string $toDir The target directory of the hydrator classes. If not specified, the
171
     *                      directory configured on the Configuration of the DocumentManager used
172
     *                      by this factory is used.
173
     */
174
    public function generateHydratorClasses(array $classes, $toDir = null)
175
    {
176
        $hydratorDir = $toDir ?: $this->hydratorDir;
177
        $hydratorDir = rtrim($hydratorDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
178
        foreach ($classes as $class) {
179
            $hydratorClassName = str_replace('\\', '', $class->name) . 'Hydrator';
180
            $hydratorFileName = $hydratorDir . $hydratorClassName . '.php';
181
            $this->generateHydratorClass($class, $hydratorClassName, $hydratorFileName);
182
        }
183
    }
184
185
    /**
186
     * @param ClassMetadata $class
187
     * @param string $hydratorClassName
188
     * @param string $fileName
189
     */
190 170
    private function generateHydratorClass(ClassMetadata $class, $hydratorClassName, $fileName)
191
    {
192 170
        $code = '';
193
194 170
        foreach ($class->fieldMappings as $fieldName => $mapping) {
195 170
            if (isset($mapping['alsoLoadFields'])) {
196 4
                foreach ($mapping['alsoLoadFields'] as $name) {
197 4
                    $code .= sprintf(<<<EOF
198
199
        /** @AlsoLoad("$name") */
200 4
        if (!array_key_exists('%1\$s', \$data) && array_key_exists('$name', \$data)) {
201 4
            \$data['%1\$s'] = \$data['$name'];
202
        }
203
204
EOF
205 4
                        ,
206 4
                        $mapping['name']
207 4
                    );
208 4
                }
209 4
            }
210
211 170
            if ($mapping['type'] === 'date') {
212 9
                $code .= sprintf(<<<EOF
213
214
        /** @Field(type="date") */
215
        if (isset(\$data['%1\$s'])) {
216
            \$value = \$data['%1\$s'];
217
            %3\$s
218
            \$this->class->reflFields['%2\$s']->setValue(\$document, clone \$return);
219
            \$hydratedData['%2\$s'] = \$return;
220
        }
221
222
EOF
223 9
                    ,
224 9
                    $mapping['name'],
225 9
                    $mapping['fieldName'],
226 9
                    Type::getType($mapping['type'])->closureToPHP()
227 9
                );
228
229
230 170
            } elseif ( ! isset($mapping['association'])) {
231 170
                $code .= sprintf(<<<EOF
232
233 170
        /** @Field(type="{$mapping['type']}") */
234
        if (isset(\$data['%1\$s'])) {
235
            \$value = \$data['%1\$s'];
236
            %3\$s
237
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
238
            \$hydratedData['%2\$s'] = \$return;
239
        }
240
241
EOF
242 170
                    ,
243 170
                    $mapping['name'],
244 170
                    $mapping['fieldName'],
245 170
                    Type::getType($mapping['type'])->closureToPHP()
246 170
                );
247 170 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...
248 46
                $code .= sprintf(<<<EOF
249
250
        /** @ReferenceOne */
251
        if (isset(\$data['%1\$s'])) {
252
            \$reference = \$data['%1\$s'];
253
            if (isset(\$this->class->fieldMappings['%2\$s']['simple']) && \$this->class->fieldMappings['%2\$s']['simple']) {
254
                \$className = \$this->class->fieldMappings['%2\$s']['targetDocument'];
255
                \$mongoId = \$reference;
256
            } else {
257
                \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$reference);
258
                \$mongoId = \$reference['\$id'];
259
            }
260
            \$targetMetadata = \$this->dm->getClassMetadata(\$className);
261
            \$id = \$targetMetadata->getPHPIdentifierValue(\$mongoId);
262
            \$return = \$this->dm->getReference(\$className, \$id);
263
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
264
            \$hydratedData['%2\$s'] = \$return;
265
        }
266
267
EOF
268 46
                    ,
269 46
                    $mapping['name'],
270 46
                    $mapping['fieldName']
271 46
                );
272 113
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isInverseSide']) {
273 6
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
274 1
                    $code .= sprintf(<<<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 1
                        ,
283 1
                        $mapping['name'],
284 1
                        $mapping['fieldName'],
285 1
                        $mapping['repositoryMethod']
286 1
                    );
287 1
                } else {
288 6
                    $code .= sprintf(<<<EOF
289
290
        \$mapping = \$this->class->fieldMappings['%2\$s'];
291
        \$className = \$mapping['targetDocument'];
292
        \$targetClass = \$this->dm->getClassMetadata(\$mapping['targetDocument']);
293
        \$mappedByMapping = \$targetClass->fieldMappings[\$mapping['mappedBy']];
294
        \$mappedByFieldName = isset(\$mappedByMapping['simple']) && \$mappedByMapping['simple'] ? \$mapping['mappedBy'] : \$mapping['mappedBy'].'.\$id';
295
        \$criteria = array_merge(
296
            array(\$mappedByFieldName => \$data['_id']),
297
            isset(\$this->class->fieldMappings['%2\$s']['criteria']) ? \$this->class->fieldMappings['%2\$s']['criteria'] : array()
298
        );
299
        \$sort = isset(\$this->class->fieldMappings['%2\$s']['sort']) ? \$this->class->fieldMappings['%2\$s']['sort'] : array();
300
        \$return = \$this->unitOfWork->getDocumentPersister(\$className)->load(\$criteria, null, array(), 0, \$sort);
301
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
302
        \$hydratedData['%2\$s'] = \$return;
303
304
EOF
305 6
                        ,
306 6
                        $mapping['name'],
307 6
                        $mapping['fieldName']
308 6
                    );
309
                }
310 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...
311 73
                $code .= sprintf(<<<EOF
312
313
        /** @Many */
314
        \$mongoData = isset(\$data['%1\$s']) ? \$data['%1\$s'] : null;
315
        \$return = new \Doctrine\ODM\MongoDB\PersistentCollection(new \Doctrine\Common\Collections\ArrayCollection(), \$this->dm, \$this->unitOfWork);
316
        \$return->setHints(\$hints);
317
        \$return->setOwner(\$document, \$this->class->fieldMappings['%2\$s']);
318
        \$return->setInitialized(false);
319
        if (\$mongoData) {
320
            \$return->setMongoData(\$mongoData);
321
        }
322
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
323
        \$hydratedData['%2\$s'] = \$return;
324
325
EOF
326 73
                    ,
327 73
                    $mapping['name'],
328 73
                    $mapping['fieldName']
329 73
                );
330 93
            } elseif ($mapping['association'] === ClassMetadata::EMBED_ONE) {
331 40
                $code .= sprintf(<<<EOF
332
333
        /** @EmbedOne */
334
        if (isset(\$data['%1\$s'])) {
335
            \$embeddedDocument = \$data['%1\$s'];
336
            \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$embeddedDocument);
337
            \$embeddedMetadata = \$this->dm->getClassMetadata(\$className);
338
            \$return = \$embeddedMetadata->newInstance();
339
340
            \$this->unitOfWork->setParentAssociation(\$return, \$this->class->fieldMappings['%2\$s'], \$document, '%1\$s');
341
342
            \$embeddedData = \$this->dm->getHydratorFactory()->hydrate(\$return, \$embeddedDocument, \$hints);
343
            \$embeddedId = \$embeddedMetadata->identifier && isset(\$embeddedData[\$embeddedMetadata->identifier]) ? \$embeddedData[\$embeddedMetadata->identifier] : null;
344
345
            \$this->unitOfWork->registerManaged(\$return, \$embeddedId, \$embeddedData);
346
347
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
348
            \$hydratedData['%2\$s'] = \$return;
349
        }
350
351
EOF
352 40
                    ,
353 40
                    $mapping['name'],
354 40
                    $mapping['fieldName']
355 40
                );
356 40
            }
357 170
        }
358
359 170
        $namespace = $this->hydratorNamespace;
360 170
        $code = sprintf(<<<EOF
361
<?php
362
363
namespace $namespace;
364
365
use Doctrine\ODM\MongoDB\DocumentManager;
366
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
367
use Doctrine\ODM\MongoDB\Hydrator\HydratorInterface;
368
use Doctrine\ODM\MongoDB\UnitOfWork;
369
370
/**
371
 * THIS CLASS WAS GENERATED BY THE DOCTRINE ODM. DO NOT EDIT THIS FILE.
372
 */
373 170
class $hydratorClassName implements HydratorInterface
374
{
375
    private \$dm;
376
    private \$unitOfWork;
377
    private \$class;
378
379
    public function __construct(DocumentManager \$dm, UnitOfWork \$uow, ClassMetadata \$class)
380
    {
381
        \$this->dm = \$dm;
382
        \$this->unitOfWork = \$uow;
383
        \$this->class = \$class;
384
    }
385
386
    public function hydrate(\$document, \$data, array \$hints = array())
387
    {
388
        \$hydratedData = array();
389
%s        return \$hydratedData;
390
    }
391 170
}
392
EOF
393 170
            ,
394
            $code
395 170
        );
396
397 170
        if ($fileName === false) {
398
            if ( ! class_exists($namespace . '\\' . $hydratorClassName)) {
399
                eval(substr($code, 5));
400
            }
401
        } else {
402 170
            $parentDirectory = dirname($fileName);
403
404 170
            if ( ! is_dir($parentDirectory) && (false === @mkdir($parentDirectory, 0775, true))) {
405
                throw HydratorException::hydratorDirectoryNotWritable();
406
            }
407
408 170
            if ( ! is_writable($parentDirectory)) {
409
                throw HydratorException::hydratorDirectoryNotWritable();
410
            }
411
412 170
            $tmpFileName = $fileName . '.' . uniqid('', true);
413 170
            file_put_contents($tmpFileName, $code);
414 170
            rename($tmpFileName, $fileName);
415 170
            chmod($fileName, 0664);
416
        }
417 170
    }
418
419
    /**
420
     * Hydrate array of MongoDB document data into the given document object.
421
     *
422
     * @param object $document  The document object to hydrate the data into.
423
     * @param array $data The array of document data.
424
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
425
     * @return array $values The array of hydrated values.
426
     */
427 383
    public function hydrate($document, $data, array $hints = array())
428
    {
429 383
        $metadata = $this->dm->getClassMetadata(get_class($document));
430
        // Invoke preLoad lifecycle events and listeners
431 383
        if ( ! empty($metadata->lifecycleCallbacks[Events::preLoad])) {
432 14
            $args = array(new PreLoadEventArgs($document, $this->dm, $data));
433 14
            $metadata->invokeLifecycleCallbacks(Events::preLoad, $document, $args);
434 14
        }
435 383 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...
436 3
            $this->evm->dispatchEvent(Events::preLoad, new PreLoadEventArgs($document, $this->dm, $data));
437 3
        }
438
439
        // alsoLoadMethods may transform the document before hydration
440 383
        if ( ! empty($metadata->alsoLoadMethods)) {
441 12
            foreach ($metadata->alsoLoadMethods as $method => $fieldNames) {
442 12
                foreach ($fieldNames as $fieldName) {
443
                    // Invoke the method only once for the first field we find
444 12
                    if (array_key_exists($fieldName, $data)) {
445 8
                        $document->$method($data[$fieldName]);
446 8
                        continue 2;
447
                    }
448 10
                }
449 12
            }
450 12
        }
451
452 383
        $data = $this->getHydratorFor($metadata->name)->hydrate($document, $data, $hints);
453 383
        if ($document instanceof Proxy) {
454 62
            $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...
455 62
            $document->__setInitializer(null);
456 62
            $document->__setCloner(null);
457
            // lazy properties may be left uninitialized
458 62
            $properties = $document->__getLazyProperties();
459 62
            foreach ($properties as $propertyName => $property) {
460 24
                if ( ! isset($document->$propertyName)) {
461 9
                    $document->$propertyName = $properties[$propertyName];
462 9
                }
463 62
            }
464 62
        }
465
466
        // Invoke the postLoad lifecycle callbacks and listeners
467 383 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...
468 13
            $metadata->invokeLifecycleCallbacks(Events::postLoad, $document, array(new LifecycleEventArgs($document, $this->dm)));
469 13
        }
470 383 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...
471 4
            $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($document, $this->dm));
472 4
        }
473
474 383
        return $data;
475
    }
476
}
477