1<?php
2
3declare(strict_types=1);
4
5namespace JMS\Serializer\Construction;
6
7use Doctrine\Common\Persistence\ManagerRegistry;
8use JMS\Serializer\DeserializationContext;
9use JMS\Serializer\Exception\InvalidArgumentException;
10use JMS\Serializer\Exception\ObjectConstructionException;
11use JMS\Serializer\Metadata\ClassMetadata;
12use JMS\Serializer\Visitor\DeserializationVisitorInterface;
13
14/**
15 * Doctrine object constructor for new (or existing) objects during deserialization.
16 */
17final class DoctrineObjectConstructor implements ObjectConstructorInterface
18{
19    public const ON_MISSING_NULL = 'null';
20    public const ON_MISSING_EXCEPTION = 'exception';
21    public const ON_MISSING_FALLBACK = 'fallback';
22    /**
23     * @var string
24     */
25    private $fallbackStrategy;
26
27    /**
28     * @var ManagerRegistry
29     */
30    private $managerRegistry;
31
32    /**
33     * @var ObjectConstructorInterface
34     */
35    private $fallbackConstructor;
36
37    /**
38     * @param ManagerRegistry $managerRegistry     Manager registry
39     * @param ObjectConstructorInterface $fallbackConstructor Fallback object constructor
40     */
41    public function __construct(ManagerRegistry $managerRegistry, ObjectConstructorInterface $fallbackConstructor, string $fallbackStrategy = self::ON_MISSING_NULL)
42    {
43        $this->managerRegistry = $managerRegistry;
44        $this->fallbackConstructor = $fallbackConstructor;
45        $this->fallbackStrategy = $fallbackStrategy;
46    }
47
48    /**
49     * {@inheritdoc}
50     */
51    public function construct(DeserializationVisitorInterface $visitor, ClassMetadata $metadata, $data, array $type, DeserializationContext $context): ?object
52    {
53        // Locate possible ObjectManager
54        $objectManager = $this->managerRegistry->getManagerForClass($metadata->name);
55
56        if (!$objectManager) {
57            // No ObjectManager found, proceed with normal deserialization
58            return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
59        }
60
61        // Locate possible ClassMetadata
62        $classMetadataFactory = $objectManager->getMetadataFactory();
63
64        if ($classMetadataFactory->isTransient($metadata->name)) {
65            // No ClassMetadata found, proceed with normal deserialization
66            return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
67        }
68
69        // Managed entity, check for proxy load
70        if (!\is_array($data)) {
71            // Single identifier, load proxy
72            return $objectManager->getReference($metadata->name, $data);
73        }
74
75        // Fallback to default constructor if missing identifier(s)
76        $classMetadata = $objectManager->getClassMetadata($metadata->name);
77        $identifierList = [];
78
79        foreach ($classMetadata->getIdentifierFieldNames() as $name) {
80            if (isset($metadata->propertyMetadata[$name])) {
81                $dataName = $metadata->propertyMetadata[$name]->serializedName;
82            } else {
83                $dataName = $name;
84            }
85
86            if (!array_key_exists($dataName, $data)) {
87                return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
88            }
89            $identifierList[$name] = $data[$dataName];
90        }
91
92        // Entity update, load it from database
93        $object = $objectManager->find($metadata->name, $identifierList);
94
95        if (null === $object) {
96            switch ($this->fallbackStrategy) {
97                case self::ON_MISSING_NULL:
98                    return null;
99                case self::ON_MISSING_EXCEPTION:
100                    throw new ObjectConstructionException(sprintf('Entity %s can not be found', $metadata->name));
101                case self::ON_MISSING_FALLBACK:
102                    return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
103                default:
104                    throw new InvalidArgumentException('The provided fallback strategy for the object constructor is not valid');
105            }
106        }
107
108        $objectManager->initializeObject($object);
109
110        return $object;
111    }
112}
113