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

Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php (5 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);
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);
143
                    }
144
                    require $fileName;
145
                    break;
146
147
                case Configuration::AUTOGENERATE_EVAL:
148
                    $this->generateHydratorClass($class, $hydratorClassName, null);
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