Completed
Push — master ( 09b86b...ba0798 )
by Andreas
20:05 queued 12s
created

Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php (1 issue)

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 const E_USER_DEPRECATED;
19
use function array_key_exists;
20
use function chmod;
21
use function class_exists;
22
use function dirname;
23
use function file_exists;
24
use function file_put_contents;
25
use function get_class;
26
use function is_dir;
27
use function is_writable;
28
use function mkdir;
29
use function rename;
30
use function rtrim;
31
use function sprintf;
32
use function str_replace;
33
use function substr;
34
use function trigger_error;
35
use function uniqid;
36
37
/**
38
 * The HydratorFactory class is responsible for instantiating a correct hydrator
39
 * type based on document's ClassMetadata
40
 *
41
 * @final
42
 */
43
class HydratorFactory
44
{
45
    /**
46
     * The DocumentManager this factory is bound to.
47
     *
48
     * @var DocumentManager
49
     */
50
    private $dm;
51
52
    /**
53
     * The UnitOfWork used to coordinate object-level transactions.
54
     *
55
     * @var UnitOfWork
56
     */
57
    private $unitOfWork;
58
59
    /**
60
     * The EventManager associated with this Hydrator
61
     *
62
     * @var EventManager
63
     */
64
    private $evm;
65
66
    /**
67
     * Which algorithm to use to automatically (re)generate hydrator classes.
68
     *
69
     * @var int
70
     */
71
    private $autoGenerate;
72
73
    /**
74
     * The namespace that contains all hydrator classes.
75
     *
76
     * @var string|null
77
     */
78
    private $hydratorNamespace;
79
80
    /**
81
     * The directory that contains all hydrator classes.
82
     *
83
     * @var string
84
     */
85
    private $hydratorDir;
86
87
    /**
88
     * Array of instantiated document hydrators.
89
     *
90
     * @var array
91
     */
92
    private $hydrators = [];
93
94
    /**
95
     * @throws HydratorException
96
     */
97 1656
    public function __construct(DocumentManager $dm, EventManager $evm, ?string $hydratorDir, ?string $hydratorNs, int $autoGenerate)
98
    {
99 1656
        if (self::class !== static::class) {
100
            @trigger_error(sprintf('The class "%s" extends "%s" which will be final in MongoDB ODM 2.0.', static::class, self::class), E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

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