Completed
Push — master ( 7b9f4b...1cd743 )
by Andreas
13s queued 10s
created

Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php (8 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\Hydrator;
6
7
use Doctrine\Common\EventManager;
8
use Doctrine\ODM\MongoDB\Configuration;
9
use Doctrine\ODM\MongoDB\DocumentManager;
10
use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs;
11
use Doctrine\ODM\MongoDB\Event\PreLoadEventArgs;
12
use Doctrine\ODM\MongoDB\Events;
13
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
14
use Doctrine\ODM\MongoDB\Types\Type;
15
use Doctrine\ODM\MongoDB\UnitOfWork;
16
use ProxyManager\Proxy\GhostObjectInterface;
17
use const DIRECTORY_SEPARATOR;
18
use function array_key_exists;
19
use function chmod;
20
use function class_exists;
21
use function dirname;
22
use function file_exists;
23
use function file_put_contents;
24
use function get_class;
25
use function is_dir;
26
use function is_writable;
27
use function mkdir;
28
use function rename;
29
use function rtrim;
30
use function sprintf;
31
use function str_replace;
32
use function substr;
33
use function uniqid;
34
35
/**
36
 * The HydratorFactory class is responsible for instantiating a correct hydrator
37
 * type based on document's ClassMetadata
38
 */
39
class HydratorFactory
40
{
41
    /**
42
     * The DocumentManager this factory is bound to.
43
     *
44
     * @var DocumentManager
45
     */
46
    private $dm;
47
48
    /**
49
     * The UnitOfWork used to coordinate object-level transactions.
50
     *
51
     * @var UnitOfWork
52
     */
53
    private $unitOfWork;
54
55
    /**
56
     * The EventManager associated with this Hydrator
57
     *
58
     * @var EventManager
59
     */
60
    private $evm;
61
62
    /**
63
     * Which algorithm to use to automatically (re)generate hydrator classes.
64
     *
65
     * @var int
66
     */
67
    private $autoGenerate;
68
69
    /**
70
     * The namespace that contains all hydrator classes.
71
     *
72
     * @var string|null
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 = [];
89
90
    /**
91
     * @throws HydratorException
92
     */
93 1651
    public function __construct(DocumentManager $dm, EventManager $evm, ?string $hydratorDir, ?string $hydratorNs, int $autoGenerate)
94
    {
95 1651
        if (! $hydratorDir) {
96
            throw HydratorException::hydratorDirectoryRequired();
97
        }
98 1651
        if (! $hydratorNs) {
99
            throw HydratorException::hydratorNamespaceRequired();
100
        }
101 1651
        $this->dm                = $dm;
102 1651
        $this->evm               = $evm;
103 1651
        $this->hydratorDir       = $hydratorDir;
104 1651
        $this->hydratorNamespace = $hydratorNs;
105 1651
        $this->autoGenerate      = $autoGenerate;
106 1651
    }
107
108
    /**
109
     * Sets the UnitOfWork instance.
110
     */
111 1651
    public function setUnitOfWork(UnitOfWork $uow) : void
112
    {
113 1651
        $this->unitOfWork = $uow;
114 1651
    }
115
116
    /**
117
     * Gets the hydrator object for the given document class.
118
     */
119 385
    public function getHydratorFor(string $className) : HydratorInterface
120
    {
121 385
        if (isset($this->hydrators[$className])) {
122 213
            return $this->hydrators[$className];
123
        }
124 385
        $hydratorClassName = str_replace('\\', '', $className) . 'Hydrator';
125 385
        $fqn               = $this->hydratorNamespace . '\\' . $hydratorClassName;
126 385
        $class             = $this->dm->getClassMetadata($className);
127
128 385
        if (! class_exists($fqn, false)) {
129 177
            $fileName = $this->hydratorDir . DIRECTORY_SEPARATOR . $hydratorClassName . '.php';
130 177
            switch ($this->autoGenerate) {
131
                case Configuration::AUTOGENERATE_NEVER:
132
                    require $fileName;
133
                    break;
134
135
                case Configuration::AUTOGENERATE_ALWAYS:
136 177
                    $this->generateHydratorClass($class, $hydratorClassName, $fileName);
0 ignored issues
show
$class of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ODM\Mong...\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
137 177
                    require $fileName;
138 177
                    break;
139
140
                case Configuration::AUTOGENERATE_FILE_NOT_EXISTS:
141
                    if (! file_exists($fileName)) {
142
                        $this->generateHydratorClass($class, $hydratorClassName, $fileName);
0 ignored issues
show
$class of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ODM\Mong...\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
143
                    }
144
                    require $fileName;
145
                    break;
146
147
                case Configuration::AUTOGENERATE_EVAL:
148
                    $this->generateHydratorClass($class, $hydratorClassName, null);
0 ignored issues
show
$class of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ODM\Mong...\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
149
                    break;
150
            }
151
        }
152 385
        $this->hydrators[$className] = new $fqn($this->dm, $this->unitOfWork, $class);
153 385
        return $this->hydrators[$className];
154
    }
155
156
    /**
157
     * Generates hydrator classes for all given classes.
158
     *
159
     * @param array  $classes The classes (ClassMetadata instances) for which to generate hydrators.
160
     * @param string $toDir   The target directory of the hydrator classes. If not specified, the
161
     *                        directory configured on the Configuration of the DocumentManager used
162
     *                        by this factory is used.
163
     */
164
    public function generateHydratorClasses(array $classes, ?string $toDir = null) : void
165
    {
166
        $hydratorDir = $toDir ?: $this->hydratorDir;
167
        $hydratorDir = rtrim($hydratorDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
168
        foreach ($classes as $class) {
169
            $hydratorClassName = str_replace('\\', '', $class->name) . 'Hydrator';
170
            $hydratorFileName  = $hydratorDir . $hydratorClassName . '.php';
171
            $this->generateHydratorClass($class, $hydratorClassName, $hydratorFileName);
172
        }
173
    }
174
175 177
    private function generateHydratorClass(ClassMetadata $class, string $hydratorClassName, ?string $fileName) : void
176
    {
177 177
        $code = '';
178
179 177
        foreach ($class->fieldMappings as $fieldName => $mapping) {
180 177
            if (isset($mapping['alsoLoadFields'])) {
181 5
                foreach ($mapping['alsoLoadFields'] as $name) {
182 5
                    $code .= sprintf(
183
                        <<<EOF
184
185 5
        /** @AlsoLoad("$name") */
186 5
        if (!array_key_exists('%1\$s', \$data) && array_key_exists('$name', \$data)) {
187 5
            \$data['%1\$s'] = \$data['$name'];
188
        }
189
190
EOF
191
                        ,
192 5
                        $mapping['name']
193
                    );
194
                }
195
            }
196
197 177
            if ($mapping['type'] === 'date') {
198 9
                $code .= sprintf(
199
                    <<<EOF
200
201
        /** @Field(type="date") */
202
        if (isset(\$data['%1\$s'])) {
203
            \$value = \$data['%1\$s'];
204
            %3\$s
205
            \$this->class->reflFields['%2\$s']->setValue(\$document, clone \$return);
206
            \$hydratedData['%2\$s'] = \$return;
207
        }
208
209
EOF
210
                    ,
211 9
                    $mapping['name'],
212 9
                    $mapping['fieldName'],
213 9
                    Type::getType($mapping['type'])->closureToPHP()
214
                );
215 177
            } elseif (! isset($mapping['association'])) {
216 176
                $code .= sprintf(
217
                    <<<EOF
218
219 176
        /** @Field(type="{$mapping['type']}") */
220
        if (isset(\$data['%1\$s']) || (! empty(\$this->class->fieldMappings['%2\$s']['nullable']) && array_key_exists('%1\$s', \$data))) {
221
            \$value = \$data['%1\$s'];
222
            if (\$value !== null) {
223
                \$typeIdentifier = \$this->class->fieldMappings['%2\$s']['type'];
224
                %3\$s
225
            } else {
226
                \$return = null;
227
            }
228
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
229
            \$hydratedData['%2\$s'] = \$return;
230
        }
231
232
EOF
233
                    ,
234 176
                    $mapping['name'],
235 176
                    $mapping['fieldName'],
236 176
                    Type::getType($mapping['type'])->closureToPHP()
237
                );
238 118
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isOwningSide']) {
239 50
                $code .= sprintf(
240
                    <<<EOF
241
242
        /** @ReferenceOne */
243
        if (isset(\$data['%1\$s'])) {
244
            \$reference = \$data['%1\$s'];
245
            \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$reference);
246
            \$identifier = ClassMetadata::getReferenceId(\$reference, \$this->class->fieldMappings['%2\$s']['storeAs']);
247
            \$targetMetadata = \$this->dm->getClassMetadata(\$className);
248
            \$id = \$targetMetadata->getPHPIdentifierValue(\$identifier);
249
            \$return = \$this->dm->getReference(\$className, \$id);
250
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
251
            \$hydratedData['%2\$s'] = \$return;
252
        }
253
254
EOF
255
                    ,
256 50
                    $mapping['name'],
257 50
                    $mapping['fieldName']
258
                );
259 100
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isInverseSide']) {
260 6
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
261 1
                    $code .= sprintf(
262
                        <<<EOF
263
264
        \$className = \$this->class->fieldMappings['%2\$s']['targetDocument'];
265
        \$return = \$this->dm->getRepository(\$className)->%3\$s(\$document);
266
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
267
        \$hydratedData['%2\$s'] = \$return;
268
269
EOF
270
                        ,
271 1
                        $mapping['name'],
272 1
                        $mapping['fieldName'],
273 1
                        $mapping['repositoryMethod']
274
                    );
275
                } else {
276 6
                    $code .= sprintf(
277
                        <<<EOF
278
279
        \$mapping = \$this->class->fieldMappings['%2\$s'];
280
        \$className = \$mapping['targetDocument'];
281
        \$targetClass = \$this->dm->getClassMetadata(\$mapping['targetDocument']);
282
        \$mappedByMapping = \$targetClass->fieldMappings[\$mapping['mappedBy']];
283
        \$mappedByFieldName = ClassMetadata::getReferenceFieldName(\$mappedByMapping['storeAs'], \$mapping['mappedBy']);
284
        \$criteria = array_merge(
285
            array(\$mappedByFieldName => \$data['_id']),
286
            isset(\$this->class->fieldMappings['%2\$s']['criteria']) ? \$this->class->fieldMappings['%2\$s']['criteria'] : array()
287
        );
288
        \$sort = isset(\$this->class->fieldMappings['%2\$s']['sort']) ? \$this->class->fieldMappings['%2\$s']['sort'] : array();
289
        \$return = \$this->unitOfWork->getDocumentPersister(\$className)->load(\$criteria, null, array(), 0, \$sort);
290
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
291
        \$hydratedData['%2\$s'] = \$return;
292
293
EOF
294
                        ,
295 6
                        $mapping['name'],
296 6
                        $mapping['fieldName']
297
                    );
298
                }
299 99
            } elseif ($mapping['association'] === ClassMetadata::REFERENCE_MANY || $mapping['association'] === ClassMetadata::EMBED_MANY) {
300 79
                $code .= sprintf(
301
                    <<<EOF
302
303
        /** @Many */
304
        \$mongoData = isset(\$data['%1\$s']) ? \$data['%1\$s'] : null;
305
        \$return = \$this->dm->getConfiguration()->getPersistentCollectionFactory()->create(\$this->dm, \$this->class->fieldMappings['%2\$s']);
306
        \$return->setHints(\$hints);
307
        \$return->setOwner(\$document, \$this->class->fieldMappings['%2\$s']);
308
        \$return->setInitialized(false);
309
        if (\$mongoData) {
310
            \$return->setMongoData(\$mongoData);
311
        }
312
        \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
313
        \$hydratedData['%2\$s'] = \$return;
314
315
EOF
316
                    ,
317 79
                    $mapping['name'],
318 79
                    $mapping['fieldName']
319
                );
320 44
            } elseif ($mapping['association'] === ClassMetadata::EMBED_ONE) {
321 44
                $code .= sprintf(
322
                    <<<EOF
323
324
        /** @EmbedOne */
325
        if (isset(\$data['%1\$s'])) {
326
            \$embeddedDocument = \$data['%1\$s'];
327
            \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$embeddedDocument);
328
            \$embeddedMetadata = \$this->dm->getClassMetadata(\$className);
329
            \$return = \$embeddedMetadata->newInstance();
330
331
            \$this->unitOfWork->setParentAssociation(\$return, \$this->class->fieldMappings['%2\$s'], \$document, '%1\$s');
332
333
            \$embeddedData = \$this->dm->getHydratorFactory()->hydrate(\$return, \$embeddedDocument, \$hints);
334
            \$embeddedId = \$embeddedMetadata->identifier && isset(\$embeddedData[\$embeddedMetadata->identifier]) ? \$embeddedData[\$embeddedMetadata->identifier] : null;
335
336
            if (empty(\$hints[Query::HINT_READ_ONLY])) {
337
                \$this->unitOfWork->registerManaged(\$return, \$embeddedId, \$embeddedData);
338
            }
339
340
            \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
341
            \$hydratedData['%2\$s'] = \$return;
342
        }
343
344
EOF
345
                    ,
346 44
                    $mapping['name'],
347 44
                    $mapping['fieldName']
348
                );
349
            }
350
        }
351
352 177
        $namespace = $this->hydratorNamespace;
353 177
        $code      = sprintf(
354
            <<<EOF
355
<?php
356
357 177
namespace $namespace;
358
359
use Doctrine\ODM\MongoDB\DocumentManager;
360
use Doctrine\ODM\MongoDB\Hydrator\HydratorInterface;
361
use Doctrine\ODM\MongoDB\Query\Query;
362
use Doctrine\ODM\MongoDB\UnitOfWork;
363
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
364
365
/**
366
 * THIS CLASS WAS GENERATED BY THE DOCTRINE ODM. DO NOT EDIT THIS FILE.
367
 */
368 177
class $hydratorClassName implements HydratorInterface
369
{
370
    private \$dm;
371
    private \$unitOfWork;
372
    private \$class;
373
374
    public function __construct(DocumentManager \$dm, UnitOfWork \$uow, ClassMetadata \$class)
375
    {
376
        \$this->dm = \$dm;
377
        \$this->unitOfWork = \$uow;
378
        \$this->class = \$class;
379
    }
380
381
    public function hydrate(object \$document, array \$data, array \$hints = array()): array
382
    {
383
        \$hydratedData = array();
384
%s        return \$hydratedData;
385
    }
386
}
387
EOF
388
            ,
389 177
            $code
390
        );
391
392 177
        if ($fileName === null) {
393
            if (! class_exists($namespace . '\\' . $hydratorClassName)) {
394
                eval(substr($code, 5));
395
            }
396
397
            return;
398
        }
399
400 177
        $parentDirectory = dirname($fileName);
401
402 177
        if (! is_dir($parentDirectory) && (@mkdir($parentDirectory, 0775, true) === false)) {
403
            throw HydratorException::hydratorDirectoryNotWritable();
404
        }
405
406 177
        if (! is_writable($parentDirectory)) {
407
            throw HydratorException::hydratorDirectoryNotWritable();
408
        }
409
410 177
        $tmpFileName = $fileName . '.' . uniqid('', true);
411 177
        file_put_contents($tmpFileName, $code);
412 177
        rename($tmpFileName, $fileName);
413 177
        chmod($fileName, 0664);
414 177
    }
415
416
    /**
417
     * Hydrate array of MongoDB document data into the given document object.
418
     *
419
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
420
     */
421 385
    public function hydrate(object $document, array $data, array $hints = []) : array
422
    {
423 385
        $metadata = $this->dm->getClassMetadata(get_class($document));
424
        // Invoke preLoad lifecycle events and listeners
425 385
        if (! empty($metadata->lifecycleCallbacks[Events::preLoad])) {
0 ignored issues
show
Accessing lifecycleCallbacks on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata 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...
426 14
            $args = [new PreLoadEventArgs($document, $this->dm, $data)];
427 14
            $metadata->invokeLifecycleCallbacks(Events::preLoad, $document, $args);
428
        }
429 385
        if ($this->evm->hasListeners(Events::preLoad)) {
430 3
            $this->evm->dispatchEvent(Events::preLoad, new PreLoadEventArgs($document, $this->dm, $data));
431
        }
432
433
        // alsoLoadMethods may transform the document before hydration
434 385
        if (! empty($metadata->alsoLoadMethods)) {
0 ignored issues
show
Accessing alsoLoadMethods on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata 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...
435 12
            foreach ($metadata->alsoLoadMethods as $method => $fieldNames) {
0 ignored issues
show
Accessing alsoLoadMethods on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata 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...
436 12
                foreach ($fieldNames as $fieldName) {
437
                    // Invoke the method only once for the first field we find
438 12
                    if (array_key_exists($fieldName, $data)) {
439 8
                        $document->$method($data[$fieldName]);
440 8
                        continue 2;
441
                    }
442
                }
443
            }
444
        }
445
446 385
        if ($document instanceof GhostObjectInterface) {
447 73
            $document->setProxyInitializer(null);
448
        }
449
450 385
        $data = $this->getHydratorFor($metadata->name)->hydrate($document, $data, $hints);
0 ignored issues
show
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata 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...
451
452
        // Invoke the postLoad lifecycle callbacks and listeners
453 385
        if (! empty($metadata->lifecycleCallbacks[Events::postLoad])) {
0 ignored issues
show
Accessing lifecycleCallbacks on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata 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...
454 13
            $metadata->invokeLifecycleCallbacks(Events::postLoad, $document, [new LifecycleEventArgs($document, $this->dm)]);
455
        }
456 385
        if ($this->evm->hasListeners(Events::postLoad)) {
457 4
            $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($document, $this->dm));
458
        }
459
460 385
        return $data;
461
    }
462
}
463