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