1<?php
2
3declare(strict_types=1);
4
5namespace JMS\Serializer;
6
7use JMS\Serializer\Exception\LogicException;
8use JMS\Serializer\Exception\NotAcceptableException;
9use JMS\Serializer\Exception\RuntimeException;
10use JMS\Serializer\Metadata\ClassMetadata;
11use JMS\Serializer\Metadata\PropertyMetadata;
12use JMS\Serializer\Visitor\DeserializationVisitorInterface;
13
14final class JsonDeserializationVisitor extends AbstractVisitor implements DeserializationVisitorInterface
15{
16    /**
17     * @var int
18     */
19    private $options = 0;
20
21    /**
22     * @var int
23     */
24    private $depth = 512;
25
26    /**
27     * @var \SplStack
28     */
29    private $objectStack;
30
31    /**
32     * @var object|null
33     */
34    private $currentObject;
35
36    public function __construct(
37        int $options = 0,
38        int $depth = 512
39    ) {
40        $this->objectStack = new \SplStack();
41        $this->options = $options;
42        $this->depth = $depth;
43    }
44
45    /**
46     * {@inheritdoc}
47     */
48    public function visitNull($data, array $type): void
49    {
50    }
51
52    /**
53     * {@inheritdoc}
54     */
55    public function visitString($data, array $type): string
56    {
57        return (string) $data;
58    }
59
60    /**
61     * {@inheritdoc}
62     */
63    public function visitBoolean($data, array $type): bool
64    {
65        return (bool) $data;
66    }
67
68    /**
69     * {@inheritdoc}
70     */
71    public function visitInteger($data, array $type): int
72    {
73        return (int) $data;
74    }
75
76    /**
77     * {@inheritdoc}
78     */
79    public function visitDouble($data, array $type): float
80    {
81        return (float) $data;
82    }
83
84    /**
85     * {@inheritdoc}
86     */
87    public function visitArray($data, array $type): array
88    {
89        if (!\is_array($data)) {
90            throw new RuntimeException(sprintf('Expected array, but got %s: %s', \gettype($data), json_encode($data)));
91        }
92
93        // If no further parameters were given, keys/values are just passed as is.
94        if (!$type['params']) {
95            return $data;
96        }
97
98        switch (\count($type['params'])) {
99            case 1: // Array is a list.
100                $listType = $type['params'][0];
101
102                $result = [];
103
104                foreach ($data as $v) {
105                    $result[] = $this->navigator->accept($v, $listType);
106                }
107
108                return $result;
109
110            case 2: // Array is a map.
111                [$keyType, $entryType] = $type['params'];
112
113                $result = [];
114
115                foreach ($data as $k => $v) {
116                    $result[$this->navigator->accept($k, $keyType)] = $this->navigator->accept($v, $entryType);
117                }
118
119                return $result;
120
121            default:
122                throw new RuntimeException(sprintf('Array type cannot have more than 2 parameters, but got %s.', json_encode($type['params'])));
123        }
124    }
125
126    /**
127     * {@inheritdoc}
128     */
129    public function visitDiscriminatorMapProperty($data, ClassMetadata $metadata): string
130    {
131        if (isset($data[$metadata->discriminatorFieldName])) {
132            return (string) $data[$metadata->discriminatorFieldName];
133        }
134
135        throw new LogicException(sprintf(
136            'The discriminator field name "%s" for base-class "%s" was not found in input data.',
137            $metadata->discriminatorFieldName,
138            $metadata->name
139        ));
140    }
141
142    /**
143     * {@inheritdoc}
144     */
145    public function startVisitingObject(ClassMetadata $metadata, object $object, array $type): void
146    {
147        $this->setCurrentObject($object);
148    }
149
150    /**
151     * {@inheritdoc}
152     */
153    public function visitProperty(PropertyMetadata $metadata, $data)
154    {
155        $name = $metadata->serializedName;
156
157        if (null === $data) {
158            return;
159        }
160
161        if (!\is_array($data)) {
162            throw new RuntimeException(sprintf('Invalid data %s (%s), expected "%s".', json_encode($data), $metadata->type['name'], $metadata->class));
163        }
164
165        if (true === $metadata->inline) {
166            if (!$metadata->type) {
167                throw new RuntimeException(sprintf(
168                    'You must define a type for %s::$%s.',
169                    $metadata->class,
170                    $metadata->name
171                ));
172            }
173            return $this->navigator->accept($data, $metadata->type);
174        }
175
176        if (!array_key_exists($name, $data)) {
177            throw new NotAcceptableException();
178        }
179
180        if (!$metadata->type) {
181            throw new RuntimeException(sprintf('You must define a type for %s::$%s.', $metadata->class, $metadata->name));
182        }
183
184        return null !== $data[$name] ? $this->navigator->accept($data[$name], $metadata->type) : null;
185    }
186
187    /**
188     * {@inheritdoc}
189     */
190    public function endVisitingObject(ClassMetadata $metadata, $data, array $type): object
191    {
192        $obj = $this->currentObject;
193        $this->revertCurrentObject();
194
195        return $obj;
196    }
197
198    /**
199     * {@inheritdoc}
200     */
201    public function getResult($data)
202    {
203        return $data;
204    }
205
206    public function setCurrentObject(object $object): void
207    {
208        $this->objectStack->push($this->currentObject);
209        $this->currentObject = $object;
210    }
211
212    public function getCurrentObject(): ?object
213    {
214        return $this->currentObject;
215    }
216
217    public function revertCurrentObject(): ?object
218    {
219        return $this->currentObject = $this->objectStack->pop();
220    }
221
222    /**
223     * {@inheritdoc}
224     */
225    public function prepare($str)
226    {
227        $decoded = json_decode($str, true, $this->depth, $this->options);
228
229        switch (json_last_error()) {
230            case JSON_ERROR_NONE:
231                return $decoded;
232
233            case JSON_ERROR_DEPTH:
234                throw new RuntimeException('Could not decode JSON, maximum stack depth exceeded.');
235
236            case JSON_ERROR_STATE_MISMATCH:
237                throw new RuntimeException('Could not decode JSON, underflow or the nodes mismatch.');
238
239            case JSON_ERROR_CTRL_CHAR:
240                throw new RuntimeException('Could not decode JSON, unexpected control character found.');
241
242            case JSON_ERROR_SYNTAX:
243                throw new RuntimeException('Could not decode JSON, syntax error - malformed JSON.');
244
245            case JSON_ERROR_UTF8:
246                throw new RuntimeException('Could not decode JSON, malformed UTF-8 characters (incorrectly encoded?)');
247
248            default:
249                throw new RuntimeException('Could not decode JSON.');
250        }
251    }
252}
253