1<?php
2
3declare(strict_types=1);
4
5namespace JMS\Serializer\Handler;
6
7use JMS\Serializer\Exception\RuntimeException;
8use JMS\Serializer\GraphNavigatorInterface;
9use JMS\Serializer\JsonDeserializationVisitor;
10use JMS\Serializer\SerializationContext;
11use JMS\Serializer\Visitor\SerializationVisitorInterface;
12use JMS\Serializer\XmlDeserializationVisitor;
13use JMS\Serializer\XmlSerializationVisitor;
14
15final class DateHandler implements SubscribingHandlerInterface
16{
17    /**
18     * @var string
19     */
20    private $defaultFormat;
21
22    /**
23     * @var \DateTimeZone
24     */
25    private $defaultTimezone;
26
27    /**
28     * @var bool
29     */
30    private $xmlCData;
31
32    /**
33     * {@inheritdoc}
34     */
35    public static function getSubscribingMethods()
36    {
37        $methods = [];
38        $deserializationTypes = ['DateTime', 'DateTimeImmutable', 'DateInterval'];
39        $serialisationTypes = ['DateTime', 'DateTimeImmutable', 'DateInterval'];
40
41        foreach (['json', 'xml'] as $format) {
42            foreach ($deserializationTypes as $type) {
43                $methods[] = [
44                    'type' => $type,
45                    'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
46                    'format' => $format,
47                ];
48            }
49
50            foreach ($serialisationTypes as $type) {
51                $methods[] = [
52                    'type' => $type,
53                    'format' => $format,
54                    'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
55                    'method' => 'serialize' . $type,
56                ];
57            }
58        }
59
60        return $methods;
61    }
62
63    public function __construct(string $defaultFormat = \DateTime::ATOM, string $defaultTimezone = 'UTC', bool $xmlCData = true)
64    {
65        $this->defaultFormat = $defaultFormat;
66        $this->defaultTimezone = new \DateTimeZone($defaultTimezone);
67        $this->xmlCData = $xmlCData;
68    }
69
70    /**
71     * @return \DOMCdataSection|\DOMText|mixed
72     */
73    private function serializeDateTimeInterface(
74        SerializationVisitorInterface $visitor,
75        \DateTimeInterface $date,
76        array $type,
77        SerializationContext $context
78    ) {
79        if ($visitor instanceof XmlSerializationVisitor && false === $this->xmlCData) {
80            return $visitor->visitSimpleString($date->format($this->getFormat($type)), $type);
81        }
82
83        $format = $this->getFormat($type);
84        if ('U' === $format) {
85            return $visitor->visitInteger((int) $date->format($format), $type);
86        }
87
88        return $visitor->visitString($date->format($this->getFormat($type)), $type);
89    }
90
91    /**
92     * @param array $type
93     *
94     * @return \DOMCdataSection|\DOMText|mixed
95     */
96    public function serializeDateTime(SerializationVisitorInterface $visitor, \DateTime $date, array $type, SerializationContext $context)
97    {
98        return $this->serializeDateTimeInterface($visitor, $date, $type, $context);
99    }
100
101    /**
102     * @param array $type
103     *
104     * @return \DOMCdataSection|\DOMText|mixed
105     */
106    public function serializeDateTimeImmutable(
107        SerializationVisitorInterface $visitor,
108        \DateTimeImmutable $date,
109        array $type,
110        SerializationContext $context
111    ) {
112        return $this->serializeDateTimeInterface($visitor, $date, $type, $context);
113    }
114
115    /**
116     * @param array $type
117     *
118     * @return \DOMCdataSection|\DOMText|mixed
119     */
120    public function serializeDateInterval(SerializationVisitorInterface $visitor, \DateInterval $date, array $type, SerializationContext $context)
121    {
122        $iso8601DateIntervalString = $this->format($date);
123
124        if ($visitor instanceof XmlSerializationVisitor && false === $this->xmlCData) {
125            return $visitor->visitSimpleString($iso8601DateIntervalString, $type);
126        }
127
128        return $visitor->visitString($iso8601DateIntervalString, $type);
129    }
130
131    /**
132     * @param mixed $data
133     */
134    private function isDataXmlNull($data): bool
135    {
136        $attributes = $data->attributes('xsi', true);
137        return isset($attributes['nil'][0]) && 'true' === (string) $attributes['nil'][0];
138    }
139
140    /**
141     * @param mixed $data
142     * @param array $type
143     */
144    public function deserializeDateTimeFromXml(XmlDeserializationVisitor $visitor, $data, array $type): ?\DateTimeInterface
145    {
146        if ($this->isDataXmlNull($data)) {
147            return null;
148        }
149
150        return $this->parseDateTime($data, $type);
151    }
152
153    /**
154     * @param mixed $data
155     * @param array $type
156     */
157    public function deserializeDateTimeImmutableFromXml(XmlDeserializationVisitor $visitor, $data, array $type): ?\DateTimeInterface
158    {
159        if ($this->isDataXmlNull($data)) {
160            return null;
161        }
162
163        return $this->parseDateTime($data, $type, true);
164    }
165
166    /**
167     * @param mixed $data
168     * @param array $type
169     */
170    public function deserializeDateIntervalFromXml(XmlDeserializationVisitor $visitor, $data, array $type): ?\DateInterval
171    {
172        if ($this->isDataXmlNull($data)) {
173            return null;
174        }
175
176        return $this->parseDateInterval((string) $data);
177    }
178
179    /**
180     * @param mixed $data
181     * @param array $type
182     */
183    public function deserializeDateTimeFromJson(JsonDeserializationVisitor $visitor, $data, array $type): ?\DateTimeInterface
184    {
185        if (null === $data) {
186            return null;
187        }
188
189        return $this->parseDateTime($data, $type);
190    }
191
192    /**
193     * @param mixed $data
194     * @param array $type
195     */
196    public function deserializeDateTimeImmutableFromJson(JsonDeserializationVisitor $visitor, $data, array $type): ?\DateTimeInterface
197    {
198        if (null === $data) {
199            return null;
200        }
201
202        return $this->parseDateTime($data, $type, true);
203    }
204
205    /**
206     * @param mixed $data
207     * @param array $type
208     */
209    public function deserializeDateIntervalFromJson(JsonDeserializationVisitor $visitor, $data, array $type): ?\DateInterval
210    {
211        if (null === $data) {
212            return null;
213        }
214
215        return $this->parseDateInterval($data);
216    }
217
218    /**
219     * @param mixed $data
220     * @param array $type
221     */
222    private function parseDateTime($data, array $type, bool $immutable = false): \DateTimeInterface
223    {
224        $timezone = !empty($type['params'][1]) ? new \DateTimeZone($type['params'][1]) : $this->defaultTimezone;
225        $format = $this->getDeserializationFormat($type);
226
227        if ($immutable) {
228            $datetime = \DateTimeImmutable::createFromFormat($format, (string) $data, $timezone);
229        } else {
230            $datetime = \DateTime::createFromFormat($format, (string) $data, $timezone);
231        }
232
233        if (false === $datetime) {
234            throw new RuntimeException(sprintf('Invalid datetime "%s", expected format %s.', $data, $format));
235        }
236
237        if ('U' === $format) {
238            $datetime = $datetime->setTimezone($timezone);
239        }
240
241        return $datetime;
242    }
243
244    private function parseDateInterval(string $data): \DateInterval
245    {
246        $dateInterval = null;
247        try {
248            $dateInterval = new \DateInterval($data);
249        } catch (\Throwable $e) {
250            throw new RuntimeException(sprintf('Invalid dateinterval "%s", expected ISO 8601 format', $data), null, $e);
251        }
252
253        return $dateInterval;
254    }
255
256    /**
257     * @param array $type
258     */
259    private function getDeserializationFormat(array $type): string
260    {
261        if (isset($type['params'][2])) {
262            return $type['params'][2];
263        }
264        if (isset($type['params'][0])) {
265            return $type['params'][0];
266        }
267        return $this->defaultFormat;
268    }
269
270    /**
271     * @param array $type
272     */
273    private function getFormat(array $type): string
274    {
275        return $type['params'][0] ?? $this->defaultFormat;
276    }
277
278    public function format(\DateInterval $dateInterval): string
279    {
280        $format = 'P';
281
282        if (0 < $dateInterval->y) {
283            $format .= $dateInterval->y . 'Y';
284        }
285
286        if (0 < $dateInterval->m) {
287            $format .= $dateInterval->m . 'M';
288        }
289
290        if (0 < $dateInterval->d) {
291            $format .= $dateInterval->d . 'D';
292        }
293
294        if (0 < $dateInterval->h || 0 < $dateInterval->i || 0 < $dateInterval->s) {
295            $format .= 'T';
296        }
297
298        if (0 < $dateInterval->h) {
299            $format .= $dateInterval->h . 'H';
300        }
301
302        if (0 < $dateInterval->i) {
303            $format .= $dateInterval->i . 'M';
304        }
305
306        if (0 < $dateInterval->s) {
307            $format .= $dateInterval->s . 'S';
308        }
309
310        if ('P' === $format) {
311            $format = 'P0DT0S';
312        }
313
314        return $format;
315    }
316}
317