vendor/symfony/form/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php line 129

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  11. use Symfony\Component\Form\Exception\TransformationFailedException;
  12. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  13. /**
  14. * Transforms between a normalized time and a localized time string.
  15. *
  16. * @author Bernhard Schussek <bschussek@gmail.com>
  17. * @author Florian Eckerstorfer <florian@eckerstorfer.org>
  18. */
  19. class DateTimeToLocalizedStringTransformer extends BaseDateTimeTransformer
  20. {
  21. private $dateFormat;
  22. private $timeFormat;
  23. private $pattern;
  24. private $calendar;
  25. /**
  26. * @see BaseDateTimeTransformer::formats for available format options
  27. *
  28. * @param string|null $inputTimezone The name of the input timezone
  29. * @param string|null $outputTimezone The name of the output timezone
  30. * @param int|null $dateFormat The date format
  31. * @param int|null $timeFormat The time format
  32. * @param int $calendar One of the \IntlDateFormatter calendar constants
  33. * @param string|null $pattern A pattern to pass to \IntlDateFormatter
  34. *
  35. * @throws UnexpectedTypeException If a format is not supported or if a timezone is not a string
  36. */
  37. public function __construct(?string $inputTimezone = null, ?string $outputTimezone = null, ?int $dateFormat = null, ?int $timeFormat = null, int $calendar = \IntlDateFormatter::GREGORIAN, ?string $pattern = null)
  38. {
  39. parent::__construct($inputTimezone, $outputTimezone);
  40. if (null === $dateFormat) {
  41. $dateFormat = \IntlDateFormatter::MEDIUM;
  42. }
  43. if (null === $timeFormat) {
  44. $timeFormat = \IntlDateFormatter::SHORT;
  45. }
  46. if (!\in_array($dateFormat, self::$formats, true)) {
  47. throw new UnexpectedTypeException($dateFormat, implode('", "', self::$formats));
  48. }
  49. if (!\in_array($timeFormat, self::$formats, true)) {
  50. throw new UnexpectedTypeException($timeFormat, implode('", "', self::$formats));
  51. }
  52. $this->dateFormat = $dateFormat;
  53. $this->timeFormat = $timeFormat;
  54. $this->calendar = $calendar;
  55. $this->pattern = $pattern;
  56. }
  57. /**
  58. * Transforms a normalized date into a localized date string/array.
  59. *
  60. * @param \DateTimeInterface $dateTime A DateTimeInterface object
  61. *
  62. * @return string
  63. *
  64. * @throws TransformationFailedException if the given value is not a \DateTimeInterface
  65. * or if the date could not be transformed
  66. */
  67. public function transform($dateTime)
  68. {
  69. if (null === $dateTime) {
  70. return '';
  71. }
  72. if (!$dateTime instanceof \DateTimeInterface) {
  73. throw new TransformationFailedException('Expected a \DateTimeInterface.');
  74. }
  75. $value = $this->getIntlDateFormatter()->format($dateTime->getTimestamp());
  76. if (0 != intl_get_error_code()) {
  77. throw new TransformationFailedException(intl_get_error_message());
  78. }
  79. return $value;
  80. }
  81. /**
  82. * Transforms a localized date string/array into a normalized date.
  83. *
  84. * @param string|array $value Localized date string/array
  85. *
  86. * @return \DateTime|null
  87. *
  88. * @throws TransformationFailedException if the given value is not a string,
  89. * if the date could not be parsed
  90. */
  91. public function reverseTransform($value)
  92. {
  93. if (!\is_string($value)) {
  94. throw new TransformationFailedException('Expected a string.');
  95. }
  96. if ('' === $value) {
  97. return null;
  98. }
  99. // date-only patterns require parsing to be done in UTC, as midnight might not exist in the local timezone due
  100. // to DST changes
  101. $dateOnly = $this->isPatternDateOnly();
  102. $dateFormatter = $this->getIntlDateFormatter($dateOnly);
  103. try {
  104. $timestamp = @$dateFormatter->parse($value);
  105. } catch (\IntlException $e) {
  106. throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
  107. }
  108. if (0 != intl_get_error_code()) {
  109. throw new TransformationFailedException(intl_get_error_message(), intl_get_error_code());
  110. } elseif ($timestamp > 253402214400) {
  111. // This timestamp represents UTC midnight of 9999-12-31 to prevent 5+ digit years
  112. throw new TransformationFailedException('Years beyond 9999 are not supported.');
  113. } elseif (false === $timestamp) {
  114. // the value couldn't be parsed but the Intl extension didn't report an error code, this
  115. // could be the case when the Intl polyfill is used which always returns 0 as the error code
  116. throw new TransformationFailedException(sprintf('"%s" could not be parsed as a date.', $value));
  117. }
  118. try {
  119. if ($dateOnly) {
  120. // we only care about year-month-date, which has been delivered as a timestamp pointing to UTC midnight
  121. $dateTime = new \DateTime(gmdate('Y-m-d', $timestamp), new \DateTimeZone($this->outputTimezone));
  122. } else {
  123. // read timestamp into DateTime object - the formatter delivers a timestamp
  124. $dateTime = new \DateTime(sprintf('@%s', $timestamp));
  125. }
  126. // set timezone separately, as it would be ignored if set via the constructor,
  127. // see https://php.net/datetime.construct
  128. $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
  129. } catch (\Exception $e) {
  130. throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
  131. }
  132. if ($this->outputTimezone !== $this->inputTimezone) {
  133. $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
  134. }
  135. return $dateTime;
  136. }
  137. /**
  138. * Returns a preconfigured IntlDateFormatter instance.
  139. *
  140. * @param bool $ignoreTimezone Use UTC regardless of the configured timezone
  141. *
  142. * @return \IntlDateFormatter
  143. *
  144. * @throws TransformationFailedException in case the date formatter cannot be constructed
  145. */
  146. protected function getIntlDateFormatter(bool $ignoreTimezone = false)
  147. {
  148. $dateFormat = $this->dateFormat;
  149. $timeFormat = $this->timeFormat;
  150. $timezone = new \DateTimeZone($ignoreTimezone ? 'UTC' : $this->outputTimezone);
  151. $calendar = $this->calendar;
  152. $pattern = $this->pattern;
  153. $intlDateFormatter = new \IntlDateFormatter(\Locale::getDefault(), $dateFormat, $timeFormat, $timezone, $calendar, $pattern ?? '');
  154. // new \intlDateFormatter may return null instead of false in case of failure, see https://bugs.php.net/66323
  155. if (!$intlDateFormatter) {
  156. throw new TransformationFailedException(intl_get_error_message(), intl_get_error_code());
  157. }
  158. $intlDateFormatter->setLenient(false);
  159. return $intlDateFormatter;
  160. }
  161. /**
  162. * Checks if the pattern contains only a date.
  163. *
  164. * @return bool
  165. */
  166. protected function isPatternDateOnly()
  167. {
  168. if (null === $this->pattern) {
  169. return false;
  170. }
  171. // strip escaped text
  172. $pattern = preg_replace("#'(.*?)'#", '', $this->pattern);
  173. // check for the absence of time-related placeholders
  174. return 0 === preg_match('#[ahHkKmsSAzZOvVxX]#', $pattern);
  175. }
  176. }