1<?php
2
3declare(strict_types=1);
4
5namespace JMS\Serializer;
6
7use JMS\Serializer\Exception\NotAcceptableException;
8use JMS\Serializer\Exception\RuntimeException;
9use JMS\Serializer\Metadata\ClassMetadata;
10use JMS\Serializer\Metadata\PropertyMetadata;
11use JMS\Serializer\Visitor\SerializationVisitorInterface;
12
13final class JsonSerializationVisitor extends AbstractVisitor implements SerializationVisitorInterface
14{
15    /**
16     * @var int
17     */
18    private $options = JSON_PRESERVE_ZERO_FRACTION;
19
20    /**
21     * @var array
22     */
23    private $dataStack = [];
24    /**
25     * @var \ArrayObject
26     */
27    private $data;
28
29    public function __construct(
30        int $options = JSON_PRESERVE_ZERO_FRACTION
31    ) {
32        $this->dataStack = [];
33        $this->options = $options;
34    }
35
36    /**
37     * {@inheritdoc}
38     */
39    public function visitNull($data, array $type)
40    {
41        return null;
42    }
43
44    /**
45     * {@inheritdoc}
46     */
47    public function visitString(string $data, array $type)
48    {
49        return $data;
50    }
51
52    /**
53     * {@inheritdoc}
54     */
55    public function visitBoolean(bool $data, array $type)
56    {
57        return $data;
58    }
59
60    /**
61     * {@inheritdoc}
62     */
63    public function visitInteger(int $data, array $type)
64    {
65        return $data;
66    }
67
68    /**
69     * {@inheritdoc}
70     */
71    public function visitDouble(float $data, array $type)
72    {
73        return $data;
74    }
75
76    /**
77     * @param array $data
78     * @param array $type
79     *
80     * @return array|\ArrayObject
81     */
82    public function visitArray(array $data, array $type)
83    {
84        \array_push($this->dataStack, $data);
85
86        $rs = isset($type['params'][1]) ? new \ArrayObject() : [];
87
88        $isList = isset($type['params'][0]) && !isset($type['params'][1]);
89
90        $elType = $this->getElementType($type);
91        foreach ($data as $k => $v) {
92            try {
93                $v = $this->navigator->accept($v, $elType);
94            } catch (NotAcceptableException $e) {
95                continue;
96            }
97
98            if ($isList) {
99                $rs[] = $v;
100            } else {
101                $rs[$k] = $v;
102            }
103        }
104
105        \array_pop($this->dataStack);
106        return $rs;
107    }
108
109    public function startVisitingObject(ClassMetadata $metadata, object $data, array $type): void
110    {
111        \array_push($this->dataStack, $this->data);
112        $this->data = true === $metadata->isMap ? new \ArrayObject() : [];
113    }
114
115    /**
116     * @return array|\ArrayObject
117     */
118    public function endVisitingObject(ClassMetadata $metadata, object $data, array $type)
119    {
120        $rs = $this->data;
121        $this->data = \array_pop($this->dataStack);
122
123        if (true !== $metadata->isList && empty($rs)) {
124            return new \ArrayObject();
125        }
126
127        return $rs;
128    }
129
130    /**
131     * {@inheritdoc}
132     */
133    public function visitProperty(PropertyMetadata $metadata, $v): void
134    {
135        try {
136            $v = $this->navigator->accept($v, $metadata->type);
137        } catch (NotAcceptableException $e) {
138            return;
139        }
140
141        if (true === $metadata->skipWhenEmpty && ($v instanceof \ArrayObject || \is_array($v)) && 0 === count($v)) {
142            return;
143        }
144
145        if ($metadata->inline) {
146            if (\is_array($v) || ($v instanceof \ArrayObject)) {
147                // concatenate the two array-like structures
148                // is there anything faster?
149                foreach ($v as $key => $value) {
150                    $this->data[$key] = $value;
151                }
152            }
153        } else {
154            $this->data[$metadata->serializedName] = $v;
155        }
156    }
157
158    /**
159     * @deprecated Will be removed in 3.0
160     *
161     * Checks if some data key exists.
162     */
163    public function hasData(string $key): bool
164    {
165        return isset($this->data[$key]);
166    }
167
168    /**
169     * @deprecated Use visitProperty(new StaticPropertyMetadata(null, 'name', 'value'), null) instead
170     *
171     * Allows you to replace existing data on the current object element.
172     *
173     * @param mixed $value This value must either be a regular scalar, or an array.
174     *                                                       It must not contain any objects anymore.
175     */
176    public function setData(string $key, $value): void
177    {
178        $this->data[$key] = $value;
179    }
180
181    /**
182     * {@inheritdoc}
183     */
184    public function getResult($data)
185    {
186        $result = @json_encode($data, $this->options);
187
188        switch (json_last_error()) {
189            case JSON_ERROR_NONE:
190                return $result;
191
192            case JSON_ERROR_UTF8:
193                throw new RuntimeException('Your data could not be encoded because it contains invalid UTF8 characters.');
194
195            default:
196                throw new RuntimeException(sprintf('An error occurred while encoding your data (error code %d).', json_last_error()));
197        }
198    }
199}
200