8000 [Form] DateTimeImmutable norm data in DateTime form types by vudaltsov · Pull Request #25273 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[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
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
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
Copy link
Contributor

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.

* @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);
}
}
Loading
0