-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Form] DateTimeImmutable norm data in DateTime form types #25273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
183 changes: 183 additions & 0 deletions
183
...ony/Component/Form/Extension/Core/DataTransformer/DateTimeImmutableToArrayTransformer.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Form\Extension\Core\DataTransformer; | ||
|
||
use Symfony\Component\Form\Exception\TransformationFailedException; | ||
|
||
/** | ||
* Transforms between a normalized time and a localized time string/array. | ||
* | ||
* @author Bernhard Schussek <bschussek@gmail.com> | ||
* @author Florian Eckerstorfer <florian@eckerstorfer.org> | ||
*/ | ||
class DateTimeImmutableToArrayTransformer extends BaseDateTimeTransformer | ||
{ | ||
private $pad; | ||
private $fields; | ||
|
||
/** | ||
* @param string $inputTimezone The input timezone | ||
* @param string $outputTimezone The output timezone | ||
* @param array $fields The date fields | ||
* @param bool $pad Whether to use padding | ||
*/ | ||
public function __construct(string $inputTimezone = null, string $outputTimezone = null, array $fields = null, bool $pad = false) | ||
{ | ||
parent::__construct($inputTimezone, $outputTimezone); | ||
|
||
if (null === $fields) { | ||
$fields = array('year', 'month', 'day', 'hour', 'minute', 'second'); | ||
} | ||
|
||
$this->fields = $fields; | ||
$this->pad = $pad; | ||
} | ||
|
||
/** | ||
* Transforms a normalized date into a localized date. | ||
* | ||
* @param \DateTimeImmutable $dateTime A DateTimeImmutable object | ||
* | ||
* @return array Localized date | ||
* | ||
* @throws TransformationFailedException If the given value is not a \DateTimeImmutable | ||
*/ | ||
public function transform($dateTime): array | ||
{ | ||
if (null === $dateTime) { | ||
return array_intersect_key(array( | ||
'year' => '', | ||
'month' => '', | ||
'day' => '', | ||
'hour' => '', | ||
'minute' => '', | ||
'second' => '', | ||
), array_flip($this->fields)); | ||
} | ||
|
||
if (!$dateTime instanceof \DateTimeImmutable) { | ||
throw new TransformationFailedException('Expected a \DateTimeImmutable.'); | ||
} | ||
|
||
if ($this->inputTimezone !== $this->outputTimezone) { | ||
$dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone)); | ||
} | ||
|
||
$result = array_intersect_key(array( | ||
'year' => $dateTime->format('Y'), | ||
'month' => $dateTime->format('m'), | ||
'day' => $dateTime->format('d'), | ||
'hour' => $dateTime->format('H'), | ||
'minute' => $dateTime->format('i'), | ||
'second' => $dateTime->format('s'), | ||
), array_flip($this->fields)); | ||
|
||
if (!$this->pad) { | ||
foreach ($result as &$entry) { | ||
// remove leading zeros | ||
$entry = (string) (int) $entry; | ||
} | ||
// unset reference to keep scope clear | ||
unset($entry); | ||
} | ||
|
||
return $result; | ||
} | ||
|
||
/** | ||
* Transforms a localized date into a normalized date. | ||
* | ||
* @param array $value Localized date | ||
* | ||
* @return \DateTimeImmutable Normalized date | ||
* | ||
* @throws TransformationFailedException If the given value is not an array, | ||
* if the value could not be transformed | ||
*/ | ||
public function reverseTransform($value): ?\DateTimeImmutable | ||
{ | ||
if (null === $value) { | ||
return null; | ||
} | ||
|
||
if (!is_array($value)) { | ||
throw new TransformationFailedException('Expected an array.'); | ||
} | ||
|
||
if ('' === implode('', $value)) { | ||
return null; | ||
} | ||
|
||
$emptyFields = array(); | ||
|
||
foreach ($this->fields as $field) { | ||
if (!isset($value[$field])) { | ||
$emptyFields[] = $field; | ||
} | ||
} | ||
|
||
if (count($emptyFields) > 0) { | ||
throw new TransformationFailedException(sprintf('The fields "%s" should not be empty', implode('", "', $emptyFields))); | ||
} | ||
|
||
if (isset($value['month']) && !ctype_digit((string) $value['month'])) { | ||
throw new TransformationFailedException('This month is invalid'); | ||
} | ||
|
||
if (isset($value['day']) && !ctype_digit((string) $value['day'])) { | ||
throw new TransformationFailedException('This day is invalid'); | ||
} | ||
|
||
if (isset($value['year']) && !ctype_digit((string) $value['year'])) { | ||
throw new TransformationFailedException('This year is invalid'); | ||
} | ||
|
||
if (!empty($value['month']) && !empty($value['day']) && !empty($value['year']) && false === checkdate($value['month'], $value['day'], $value['year'])) { | ||
throw new TransformationFailedException('This is an invalid date'); | ||
} | ||
|
||
if (isset($value['hour']) && !ctype_digit((string) $value['hour'])) { | ||
throw new TransformationFailedException('This hour is invalid'); | ||
} | ||
|
||
if (isset($value['minute']) && !ctype_digit((string) $value['minute'])) { | ||
throw new TransformationFailedException('This minute is invalid'); | ||
} | ||
|
||
if (isset($value['second']) && !ctype_digit((string) $value['second'])) { | ||
throw new TransformationFailedException('This second is invalid'); | ||
} | ||
|
||
try { | ||
$dateTime = new \DateTimeImmutable( | ||
sprintf( | ||
'%s-%s-%s %s:%s:%s', | ||
empty($value['year']) ? '1970' : $value['year'], | ||
empty($value['month']) ? '1' : $value['month'], | ||
empty($value['day']) ? '1' : $value['day'], | ||
empty($value['hour']) ? '0' : $value['hour'], | ||
empty($value['minute']) ? '0' : $value['minute'], | ||
empty($value['second']) ? '0' : $value['second'] | ||
), | ||
new \DateTimeZone($this->outputTimezone) | ||
); | ||
|
||
if ($this->inputTimezone !== $this->outputTimezone) { | ||
$dateTime = $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone)); | ||
} | ||
} catch (\Exception $e) { | ||
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e); | ||
} | ||
|
||
return $dateTime; | ||
} | ||
} |
199 changes: 199 additions & 0 deletions
199
...ent/Form/Extension/Core/DataTransformer/DateTimeImmutableToLocalizedStringTransformer.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Form\Extension\Core\DataTransformer; | ||
|
||
use Symfony\Component\Form\Exception\TransformationFailedException; | ||
use Symfony\Component\Form\Exception\UnexpectedTypeException; | ||
|
||
/** | ||
* Transforms between a normalized time and a localized time string. | ||
* | ||
* @author Bernhard Schussek <bschussek@gmail.com> | ||
* @author Florian Eckerstorfer <florian@eckerstorfer.org> | ||
*/ | ||
class DateTimeImmutableToLocalizedStringTransformer extends BaseDateTimeTransformer | ||
{ | ||
private $dateFormat; | ||
private $timeFormat; | ||
private $pattern; | ||
private $calendar; | ||
|
||
/** | ||
* @see BaseDateTimeTransformer::formats for available format options | ||
* | ||
* @param string $inputTimezone The name of the input timezone | ||
* @param string $outputTimezone The name of the output timezone | ||
* @param int $dateFormat The date format | ||
* @param int $timeFormat The time format | ||
* @param int $calendar One of the \IntlDateFormatter calendar constants | ||
* @param string $pattern A pattern to pass to \IntlDateFormatter | ||
* | ||
* @throws UnexpectedTypeException If a format is not supported or if a timezone is not a string | ||
*/ | ||
public function __construct(string $inputTimezone = null, string $outputTimezone = null, int $dateFormat = null, int $timeFormat = null, int $calendar = \IntlDateFormatter::GREGORIAN, string $pattern = null) | ||
{ | ||
parent::__construct($inputTimezone, $outputTimezone); | ||
|
||
if (null === $dateFormat) { | ||
$dateFormat = \IntlDateFormatter::MEDIUM; | ||
} | ||
|
||
if (null === $timeFormat) { | ||
$timeFormat = \IntlDateFormatter::SHORT; | ||
} | ||
|
||
if (!in_array($dateFormat, self::$formats, true)) { | ||
throw new UnexpectedTypeException($dateFormat, implode('", "', self::$formats)); | ||
} | ||
|
||
if (!in_array($timeFormat, self::$formats, true)) { | ||
throw new UnexpectedTypeException($timeFormat, implode('", "', self::$formats)); | ||
} | ||
|
||
$this->dateFormat = $dateFormat; | ||
$this->timeFormat = $timeFormat; | ||
$this->calendar = $calendar; | ||
$this->pattern = $pattern; | ||
} | ||
|
||
/** | ||
* Transforms a normalized date into a localized date string/array. | ||
* | ||
* @param \DateTimeImmutable $dateTime A DateTimeImmutable object | ||
* | ||
* @return string Localized date string | ||
* | ||
* @throws TransformationFailedException if the given value is not a \DateTimeImmutable | ||
* or if the date could not be transformed | ||
*/ | ||
public function transform($dateTime): string | ||
{ | ||
if (null === $dateTime) { | ||
return ''; | ||
} | ||
|
||
if (!$dateTime instanceof \DateTimeImmutable) { | ||
throw new TransformationFailedException('Expected a \DateTimeImmutable.'); | ||
} | ||
|
||
$value = $this->getIntlDateFormatter()->format($dateTime->getTimestamp()); | ||
|
||
if (0 != intl_get_error_code()) { | ||
throw new TransformationFailedException(intl_get_error_message()); | ||
} | ||
|
||
return $value; | ||
} | ||
|
||
/** | ||
* Transforms a localized date string/array into a normalized date. | ||
* | ||
* @param string|array $value Localized date string/array | ||
* | ||
* @return \DateTimeImmutable Normalized date | ||
* | ||
* @throws TransformationFailedException if the given value is not a string, | ||
* if the date could not be parsed | ||
*/ | ||
public function reverseTransform($value): ?\DateTimeImmutable | ||
{ | ||
if (!is_string($value)) { | ||
throw new TransformationFailedException('Expected a string.'); | ||
} | ||
|
||
if ('' === $value) { | ||
return null; | ||
} | ||
|
||
// date-only patterns require parsing to be done in UTC, as midnight might not exist in the local timezone due | ||
// to DST changes | ||
$dateOnly = $this->isPatternDateOnly(); | ||
|
||
$timestamp = $this->getIntlDateFormatter($dateOnly)->parse($value); | ||
|
||
if (0 != intl_get_error_code()) { | ||
throw new TransformationFailedException(intl_get_error_message()); | ||
} | ||
|
||
try { | ||
if ($dateOnly) { | ||
// we only care about year-month-date, which has been delivered as a timestamp pointing to UTC midnight | ||
$dateTime = new \DateTimeImmutable(gmdate('Y-m-d', $timestamp), new \DateTimeZone($this->outputTimezone)); | ||
} else { | ||
// read timestamp into DateTimeImmutable object - the formatter delivers a timestamp | ||
$dateTime = new \DateTimeImmutable(sprintf('@%s', $timestamp)); | ||
} | ||
// set timezone separately, as it would be ignored if set via the constructor, | ||
// see http://php.net/manual/en/datetime.construct.php | ||
$dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone)); | ||
} catch (\Exception $e) { | ||
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e); | ||
} | ||
|
||
if ($this->outputTimezone !== $this->inputTimezone) { | ||
$dateTime = $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone)); | ||
} | ||
|
||
return $dateTime; | ||
} | ||
|
||
/** | ||
* Returns a preconfigured IntlDateFormatter instance. | ||
* | ||
* @param bool $ignoreTimezone Use UTC regardless of the configured timezone | ||
* | ||
* @return \IntlDateFormatter | ||
* | ||
* @throws TransformationFailedException in case the date formatter can not be constructed | ||
*/ | ||
protected function getIntlDateFormatter(bool $ignoreTimezone = false): \IntlDateFormatter | ||
{ | ||
$dateFormat = $this->dateFormat; | ||
$timeFormat = $this->timeFormat; | ||
$timezone = $ignoreTimezone ? 'UTC' : $this->outputTimezone; | ||
if (class_exists('IntlTimeZone', false)) { | ||
// see https://bugs.php.net/bug.php?id=66323 | ||
$timezone = \IntlTimeZone::createTimeZone($timezone); | ||
} | ||
$calendar = $this->calendar; | ||
$pattern = $this->pattern; | ||
|
||
$intlDateFormatter = new \IntlDateFormatter(\Locale::getDefault(), $dateFormat, $timeFormat, $timezone, $calendar, $pattern); | ||
|
||
// new \intlDateFormatter may return null instead of false in case of failure, see https://bugs.php.net/bug.php?id=66323 | ||
if (!$intlDateFormatter) { | ||
throw new TransformationFailedException(intl_get_error_message(), intl_get_error_code()); | ||
} | ||
|
||
$intlDateFormatter->setLenient(false); | ||
|
||
return $intlDateFormatter; | ||
} | ||
|
||
/** | ||
* Checks if the pattern contains only a date. | ||
* | ||
* @return bool | ||
*/ | ||
protected function isPatternDateOnly(): bool | ||
{ | ||
if (null === $this->pattern) { | ||
return false; | ||
} | ||
|
||
// strip escaped text | ||
$pattern = preg_replace("#'(.*?)'#", '', $this->pattern); | ||
|
||
// check for the absence of time-related placeholders | ||
return 0 === preg_match('#[ahHkKmsSAzZOvVxX]#', $pattern); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need all of this.