Completed
Pull Request — master (#1709)
by Andreas
16:45 queued 14:38
created

HydratorFactory::setUnitOfWork()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Doctrine\ODM\MongoDB\Hydrator;
4
5
use Doctrine\Common\EventManager;
6
use Doctrine\ODM\MongoDB\DocumentManager;
7
use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs;
8
use Doctrine\ODM\MongoDB\Event\PreLoadEventArgs;
9
use Doctrine\ODM\MongoDB\Events;
10
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
11
use Doctrine\ODM\MongoDB\Proxy\Proxy;
12
use Doctrine\ODM\MongoDB\Types\Type;
13
use Doctrine\ODM\MongoDB\UnitOfWork;
14
use Doctrine\ODM\MongoDB\Configuration;
15
16
/**
17
 * The HydratorFactory class is responsible for instantiating a correct hydrator
18
 * type based on document's ClassMetadata
19
 *
20
 * @since       1.0
21
 */
22
class HydratorFactory
23
{
24
    /**
25
     * The DocumentManager this factory is bound to.
26
     *
27
     * @var \Doctrine\ODM\MongoDB\DocumentManager
28
     */
29
    private $dm;
30
31
    /**
32
     * The UnitOfWork used to coordinate object-level transactions.
33
     *
34
     * @var \Doctrine\ODM\MongoDB\UnitOfWork
35
     */
36
    private $unitOfWork;
37
38
    /**
39
     * The EventManager associated with this Hydrator
40
     *
41
     * @var \Doctrine\Common\EventManager
42
     */
43
    private $evm;
44
45
    /**
46
     * Which algorithm to use to automatically (re)generate hydrator classes.
47
     *
48
     * @var integer
49
     */
50
    private $autoGenerate;
51
52
    /**
53
     * The namespace that contains all hydrator classes.
54
     *
55
     * @var string
56
     */
57
    private $hydratorNamespace;
58
59
    /**
60
     * The directory that contains all hydrator classes.
61
     *
62
     * @var string
63
     */
64
    private $hydratorDir;
65
66
    /**
67
     * Array of instantiated document hydrators.
68
     *
69
     * @var array
70
     */
71
    private $hydrators = array();
72
73
    /**
74
     * @param DocumentManager $dm
75
     * @param EventManager $evm
76
     * @param string $hydratorDir
77
     * @param string $hydratorNs
78
     * @param integer $autoGenerate
79
     * @throws HydratorException
80
     */
81 1627
    public function __construct(DocumentManager $dm, EventManager $evm, $hydratorDir, $hydratorNs, $autoGenerate)
82
    {
83 1627
        if ( ! $hydratorDir) {
84
            throw HydratorException::hydratorDirectoryRequired();
85
        }
86 1627
        if ( ! $hydratorNs) {
87
            throw HydratorException::hydratorNamespaceRequired();
88
        }
89 1627
        $this->dm = $dm;
90 1627
        $this->evm = $evm;
91 1627
        $this->hydratorDir = $hydratorDir;
92 1627
        $this->hydratorNamespace = $hydratorNs;
93 1627
        $this->autoGenerate = $autoGenerate;
94 1627
    }
95
96
    /**
97
     * Sets the UnitOfWork instance.
98
     *
99
     * @param UnitOfWork $uow
100
     */
101 1627
    public function setUnitOfWork(UnitOfWork $uow)
102
    {
103 1627
        $this->unitOfWork = $uow;
104 1627
    }
105
106
    /**
107
     * Gets the hydrator object for the given document class.
108
     *
109
     * @param string $className
110
     * @return \Doctrine\ODM\MongoDB\Hydrator\HydratorInterface $hydrator
111
     */
112 371
    public function getHydratorFor($className)
113
    {
114 371
        if (isset($this->hydrators[$className])) {
115 199
            return $this->hydrators[$className];
116
        }
117 371
        $hydratorClassName = str_replace('\\', '', $className) . 'Hydrator';
118 371
        $fqn = $this->hydratorNamespace . '\\' . $hydratorClassName;
119 371
        $class = $this->dm->getClassMetadata($className);
120
121 371 View Code Duplication
        if ( ! class_exists($fqn, false)) {
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...
122 176
            $fileName = $this->hydratorDir . DIRECTORY_SEPARATOR . $hydratorClassName . '.php';
123 176
            switch ($this->autoGenerate) {
124
                case Configuration::AUTOGENERATE_NEVER:
125
                    require $fileName;
126
                    break;
127
                    
128
                case Configuration::AUTOGENERATE_ALWAYS:
129 176
                    $this->generateHydratorClass($class, $hydratorClassName, $fileName);
130 176
                    require $fileName;
131 176
                    break;
132
                    
133
                case Configuration::AUTOGENERATE_FILE_NOT_EXISTS:
134
                    if (!file_exists($fileName)) {
135
                        $this->generateHydratorClass($class, $hydratorClassName, $fileName);
136
                    }
137
                    require $fileName;
138
                    break;
139
                    
140
                case Configuration::AUTOGENERATE_EVAL:
141
                    $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...
142
                    break;
143
            }
144
        }
145 371
        $this->hydrators[$className] = new $fqn($this->dm, $this->unitOfWork, $class);
146 371
        return $this->hydrators[$className];
147
    }
148
149
    /**
150
     * Generates hydrator classes for all given classes.
151
     *
152
     * @param array $classes The classes (ClassMetadata instances) for which to generate hydrators.
153
     * @param string $toDir The target directory of the hydrator classes. If not specified, the
154
     *                      directory configured on the Configuration of the DocumentManager used
155
     *                      by this factory is used.
156
     */
157
    public function generateHydratorClasses(array $classes, $toDir = null)
158
    {
159
        $hydratorDir = $toDir ?: $this->hydratorDir;
160
        $hydratorDir = rtrim($hydratorDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
161
        foreach ($classes as $class) {
162
            $hydratorClassName = str_replace('\\', '', $class->name) . 'Hydrator';
163
            $hydratorFileName = $hydratorDir . $hydratorClassName . '.php';
164
            $this->generateHydratorClass($class, $hydratorClassName, $hydratorFileName);
165
        }
166
    }
167
168
    /**
169
     * @param ClassMetadata $class
170
     * @param string $hydratorClassName
171
     * @param string $fileName
172
     */
173 176
    private function generateHydratorClass(ClassMetadata $class, $hydratorClassName, $fileName)
174
    {
175 176
        $code = '';
176
177 176
        foreach ($class->fieldMappings as $fieldName => $mapping) {
178 176
            if (isset($mapping['alsoLoadFields'])) {
179 5
                foreach ($mapping['alsoLoadFields'] as $name) {
180 5
                    $code .= sprintf(<<<EOF
181
182 5
        /** @AlsoLoad("$name") */
183 5
        if (!array_key_exists('%1\$s', \$data) && array_key_exists('$name', \$data)) {
184 5
            \$data['%1\$s'] = \$data['$name'];
185
        }
186
187
EOF
188
                        ,
189 5
                        $mapping['name']
190
                    );
191
                }
192
            }
193
194 176
            if ($mapping['type'] === 'date') {
195 8
                $code .= sprintf(<<<EOF
196
197
        /** @Field(type="date") */
198
        if (isset(\$data['%1\$s'])) {
199
            \$value = \$data['%1\$s'];
200
            %3\$s
201
            \$this->class->reflFields['%2\$s']->setValue(\$document, clone \$return);
202
            \$hydratedData['%2\$s'] = \$return;
203
        }
204
205
EOF
206
                    ,
207 8
                    $mapping['name'],
208 8
                    $mapping['fieldName'],
209 8
                    Type::getType($mapping['type'])->closureToPHP()
210
                );
211
212
213 176
            } elseif ( ! isset($mapping['association'])) {
214 176
                $code .= sprintf(<<<EOF
215
216 176
        /** @Field(type="{$mapping['type']}") */
217
        if (isset(\$data['%1\$s']) || (! empty(\$this->class->fieldMappings['%2\$s']['nullable']) && array_key_exists('%1\$s', \$data))) {
218
            \$value = \$data['%1\$s'];
219
            if (\$value !== null) {
220
                \$typeIdentifier = \$this->class->fieldMappings['%2\$s']['type'];
221
                %3\$s
222
            } else {
223
                \$return = null;
224
            }
225
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
226
            \$hydratedData['%2\$s'] = \$return;
227
        }
228
229
EOF
230
                    ,
231 176
                    $mapping['name'],
232 176
                    $mapping['fieldName'],
233 176
                    Type::getType($mapping['type'])->closureToPHP()
234
                );
235 113 View Code Duplication
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isOwningSide']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
236 50
                $code .= sprintf(<<<EOF
237
238
        /** @ReferenceOne */
239
        if (isset(\$data['%1\$s'])) {
240
            \$reference = \$data['%1\$s'];
241
            \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$reference);
242
            \$identifier = ClassMetadataInfo::getReferenceId(\$reference, \$this->class->fieldMappings['%2\$s']['storeAs']);
243
            \$targetMetadata = \$this->dm->getClassMetadata(\$className);
244
            \$id = \$targetMetadata->getPHPIdentifierValue(\$identifier);
245
            \$return = \$this->dm->getReference(\$className, \$id);
246
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
247
            \$hydratedData['%2\$s'] = \$return;
248
        }
249
250
EOF
251
                    ,
252 50
                    $mapping['name'],
253 50
                    $mapping['fieldName']
254
                );
255 94
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isInverseSide']) {
256 5
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
257 1
                    $code .= sprintf(<<<EOF
258
259
        \$className = \$this->class->fieldMappings['%2\$s']['targetDocument'];
260
        \$return = \$this->dm->getRepository(\$className)->%3\$s(\$document);
261
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
262
        \$hydratedData['%2\$s'] = \$return;
263
264
EOF
265
                        ,
266 1
                        $mapping['name'],
267 1
                        $mapping['fieldName'],
268 1
                        $mapping['repositoryMethod']
269
                    );
270
                } else {
271 5
                    $code .= sprintf(<<<EOF
272
273
        \$mapping = \$this->class->fieldMappings['%2\$s'];
274
        \$className = \$mapping['targetDocument'];
275
        \$targetClass = \$this->dm->getClassMetadata(\$mapping['targetDocument']);
276
        \$mappedByMapping = \$targetClass->fieldMappings[\$mapping['mappedBy']];
277
        \$mappedByFieldName = ClassMetadataInfo::getReferenceFieldName(\$mappedByMapping['storeAs'], \$mapping['mappedBy']);
278
        \$criteria = array_merge(
279
            array(\$mappedByFieldName => \$data['_id']),
280
            isset(\$this->class->fieldMappings['%2\$s']['criteria']) ? \$this->class->fieldMappings['%2\$s']['criteria'] : array()
281
        );
282
        \$sort = isset(\$this->class->fieldMappings['%2\$s']['sort']) ? \$this->class->fieldMappings['%2\$s']['sort'] : array();
283
        \$return = \$this->unitOfWork->getDocumentPersister(\$className)->load(\$criteria, null, array(), 0, \$sort);
284
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
285
        \$hydratedData['%2\$s'] = \$return;
286
287
EOF
288
                        ,
289 5
                        $mapping['name'],
290 5
                        $mapping['fieldName']
291
                    );
292
                }
293 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...
294 73
                $code .= sprintf(<<<EOF
295
296
        /** @Many */
297
        \$mongoData = isset(\$data['%1\$s']) ? \$data['%1\$s'] : null;
298
        \$return = \$this->dm->getConfiguration()->getPersistentCollectionFactory()->create(\$this->dm, \$this->class->fieldMappings['%2\$s']);
299
        \$return->setHints(\$hints);
300
        \$return->setOwner(\$document, \$this->class->fieldMappings['%2\$s']);
301
        \$return->setInitialized(false);
302
        if (\$mongoData) {
303
            \$return->setMongoData(\$mongoData);
304
        }
305
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
306
        \$hydratedData['%2\$s'] = \$return;
307
308
EOF
309
                    ,
310 73
                    $mapping['name'],
311 73
                    $mapping['fieldName']
312
                );
313 42
            } elseif ($mapping['association'] === ClassMetadata::EMBED_ONE) {
314 42
                $code .= sprintf(<<<EOF
315
316
        /** @EmbedOne */
317
        if (isset(\$data['%1\$s'])) {
318
            \$embeddedDocument = \$data['%1\$s'];
319
            \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$embeddedDocument);
320
            \$embeddedMetadata = \$this->dm->getClassMetadata(\$className);
321
            \$return = \$embeddedMetadata->newInstance();
322
323
            \$this->unitOfWork->setParentAssociation(\$return, \$this->class->fieldMappings['%2\$s'], \$document, '%1\$s');
324
325
            \$embeddedData = \$this->dm->getHydratorFactory()->hydrate(\$return, \$embeddedDocument, \$hints);
326
            \$embeddedId = \$embeddedMetadata->identifier && isset(\$embeddedData[\$embeddedMetadata->identifier]) ? \$embeddedData[\$embeddedMetadata->identifier] : null;
327
328
            if (empty(\$hints[Query::HINT_READ_ONLY])) {
329
                \$this->unitOfWork->registerManaged(\$return, \$embeddedId, \$embeddedData);
330
            }
331
332
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
333
            \$hydratedData['%2\$s'] = \$return;
334
        }
335
336
EOF
337
                    ,
338 42
                    $mapping['name'],
339 176
                    $mapping['fieldName']
340
                );
341
            }
342
        }
343
344 176
        $namespace = $this->hydratorNamespace;
345 176
        $code = sprintf(<<<EOF
346
<?php
347
348 176
namespace $namespace;
349
350
use Doctrine\ODM\MongoDB\DocumentManager;
351
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
352
use Doctrine\ODM\MongoDB\Hydrator\HydratorInterface;
353
use Doctrine\ODM\MongoDB\Query\Query;
354
use Doctrine\ODM\MongoDB\UnitOfWork;
355
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
356
357
/**
358
 * THIS CLASS WAS GENERATED BY THE DOCTRINE ODM. DO NOT EDIT THIS FILE.
359
 */
360 176
class $hydratorClassName implements HydratorInterface
361
{
362
    private \$dm;
363
    private \$unitOfWork;
364
    private \$class;
365
366
    public function __construct(DocumentManager \$dm, UnitOfWork \$uow, ClassMetadata \$class)
367
    {
368
        \$this->dm = \$dm;
369
        \$this->unitOfWork = \$uow;
370
        \$this->class = \$class;
371
    }
372
373
    public function hydrate(\$document, \$data, array \$hints = array())
374
    {
375
        \$hydratedData = array();
376
%s        return \$hydratedData;
377
    }
378
}
379
EOF
380
            ,
381 176
            $code
382
        );
383
384 176
        if ($fileName === false) {
385
            if ( ! class_exists($namespace . '\\' . $hydratorClassName)) {
386
                eval(substr($code, 5));
387
            }
388 View Code Duplication
        } else {
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...
389 176
            $parentDirectory = dirname($fileName);
390
391 176
            if ( ! is_dir($parentDirectory) && (false === @mkdir($parentDirectory, 0775, true))) {
392
                throw HydratorException::hydratorDirectoryNotWritable();
393
            }
394
395 176
            if ( ! is_writable($parentDirectory)) {
396
                throw HydratorException::hydratorDirectoryNotWritable();
397
            }
398
399 176
            $tmpFileName = $fileName . '.' . uniqid('', true);
400 176
            file_put_contents($tmpFileName, $code);
401 176
            rename($tmpFileName, $fileName);
402 176
            chmod($fileName, 0664);
403
        }
404 176
    }
405
406
    /**
407
     * Hydrate array of MongoDB document data into the given document object.
408
     *
409
     * @param object $document  The document object to hydrate the data into.
410
     * @param array $data The array of document data.
411
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
412
     * @return array $values The array of hydrated values.
413
     */
414 371
    public function hydrate($document, $data, array $hints = array())
415
    {
416 371
        $metadata = $this->dm->getClassMetadata(get_class($document));
417
        // Invoke preLoad lifecycle events and listeners
418 371
        if ( ! empty($metadata->lifecycleCallbacks[Events::preLoad])) {
419 14
            $args = array(new PreLoadEventArgs($document, $this->dm, $data));
420 14
            $metadata->invokeLifecycleCallbacks(Events::preLoad, $document, $args);
421
        }
422 371 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...
423 3
            $this->evm->dispatchEvent(Events::preLoad, new PreLoadEventArgs($document, $this->dm, $data));
424
        }
425
426
        // alsoLoadMethods may transform the document before hydration
427 371
        if ( ! empty($metadata->alsoLoadMethods)) {
428 12
            foreach ($metadata->alsoLoadMethods as $method => $fieldNames) {
429 12
                foreach ($fieldNames as $fieldName) {
430
                    // Invoke the method only once for the first field we find
431 12
                    if (array_key_exists($fieldName, $data)) {
432 8
                        $document->$method($data[$fieldName]);
433 12
                        continue 2;
434
                    }
435
                }
436
            }
437
        }
438
439 371
        $data = $this->getHydratorFor($metadata->name)->hydrate($document, $data, $hints);
440 371
        if ($document instanceof Proxy) {
441 72
            $document->__isInitialized__ = true;
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
442 72
            $document->__setInitializer(null);
443 72
            $document->__setCloner(null);
444
            // lazy properties may be left uninitialized
445 72
            $properties = $document->__getLazyProperties();
446 72
            foreach ($properties as $propertyName => $property) {
447 30
                if ( ! isset($document->$propertyName)) {
448 30
                    $document->$propertyName = $properties[$propertyName];
449
                }
450
            }
451
        }
452
453
        // Invoke the postLoad lifecycle callbacks and listeners
454 371 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...
455 13
            $metadata->invokeLifecycleCallbacks(Events::postLoad, $document, array(new LifecycleEventArgs($document, $this->dm)));
456
        }
457 371 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...
458 4
            $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($document, $this->dm));
459
        }
460
461 371
        return $data;
462
    }
463
}
464