mirror of
				https://github.com/yiisoft/yii2.git
				synced 2025-11-04 14:46:19 +08:00 
			
		
		
		
	Merge pull request #6178 from yiisoft/date-timezone-issue
[WIP] first draft of a fix for #5448
This commit is contained in:
		@ -8,6 +8,7 @@ Yii Framework 2 Change Log
 | 
			
		||||
- Bug #4823: `yii message` accuracy and error handling were improved (samdark)
 | 
			
		||||
- Bug #4889: Application was getting into redirect loop when user wasn't allowed accessing login page. Now shows 403 (samdark)
 | 
			
		||||
- Bug #5402: Debugger was not loading when there were closures in asset classes (samdark)
 | 
			
		||||
- Bug #5448: Date formatter was doing timezone conversion on date only values resulting in different date displayed than provided (cebe)
 | 
			
		||||
- Bug #5452: Errors occurring after the response is sent are not displayed (qiangxue) 
 | 
			
		||||
- Bug #5521: Fixed `yii\console\controllers\AssetController` breaks CSS URLs, which start from '/' (klimov-paul)
 | 
			
		||||
- Bug #5570: `yii\bootstrap\Tabs` would throw an exception if `content` is not set for one of its `items` (RomeroMsk)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										97
									
								
								framework/i18n/DateTimeExtended.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								framework/i18n/DateTimeExtended.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,97 @@
 | 
			
		||||
<?php
 | 
			
		||||
/**
 | 
			
		||||
 * @link http://www.yiiframework.com/
 | 
			
		||||
 * @copyright Copyright (c) 2008 Yii Software LLC
 | 
			
		||||
 * @license http://www.yiiframework.com/license/
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace yii\i18n;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * DateTimeExtended is an extended version of the [PHP DateTime class](http://php.net/manual/en/class.datetime.php).
 | 
			
		||||
 *
 | 
			
		||||
 * It provides more accurate handling of date-only values which can not be converted between timezone as
 | 
			
		||||
 * they do not include any time information.
 | 
			
		||||
 *
 | 
			
		||||
 * **Important Note:** This implementation was created to be used by [[Formatter]] internally, it may not behave
 | 
			
		||||
 * as expected when used directly in your code. Normally you should not need to use this class in your application code.
 | 
			
		||||
 * Use the original PHP [DateTime](http://php.net/manual/en/class.datetime.php) class instead.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Carsten Brandt <mail@cebe.cc>
 | 
			
		||||
 * @since 2.0
 | 
			
		||||
 */
 | 
			
		||||
class DateTimeExtended extends \DateTime
 | 
			
		||||
{
 | 
			
		||||
    private $_isDateOnly = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The DateTimeExtended constructor.
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $time
 | 
			
		||||
     * @param \DateTimeZone $timezone
 | 
			
		||||
     * @return DateTimeExtended
 | 
			
		||||
     * @see http://php.net/manual/en/datetime.construct.php
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct ($time = 'now', \DateTimeZone $timezone = null)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct($time, $timezone);
 | 
			
		||||
 | 
			
		||||
        $info = date_parse($time);
 | 
			
		||||
        if ($info['hour'] === false && $info['minute'] === false && $info['second'] === false) {
 | 
			
		||||
            $this->_isDateOnly = true;
 | 
			
		||||
        } else {
 | 
			
		||||
            $this->_isDateOnly = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse a string into a new DateTime object according to the specified format
 | 
			
		||||
     * @param string $format Format accepted by date().
 | 
			
		||||
     * @param string $time String representing the time.
 | 
			
		||||
     * @param \DateTimeZone $timezone A DateTimeZone object representing the desired time zone.
 | 
			
		||||
     * @return DateTimeExtended
 | 
			
		||||
     * @link http://php.net/manual/en/datetime.createfromformat.php
 | 
			
		||||
     */
 | 
			
		||||
    public static function createFromFormat ($format, $time, $timezone = null)
 | 
			
		||||
    {
 | 
			
		||||
        if (($originalDateTime = parent::createFromFormat($format, $time, $timezone)) === false) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $info = date_parse_from_format($format, $time);
 | 
			
		||||
 | 
			
		||||
        /** @var $dateTime \DateTime */
 | 
			
		||||
        $dateTime = new static;
 | 
			
		||||
        if ($info['hour'] === false && $info['minute'] === false && $info['second'] === false) {
 | 
			
		||||
            $dateTime->_isDateOnly = true;
 | 
			
		||||
        } else {
 | 
			
		||||
            $dateTime->_isDateOnly = false;
 | 
			
		||||
        }
 | 
			
		||||
        $dateTime->setTimezone($originalDateTime->getTimezone());
 | 
			
		||||
        $dateTime->setTimestamp($originalDateTime->getTimestamp());
 | 
			
		||||
 | 
			
		||||
        return $dateTime;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isDateOnly()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->_isDateOnly;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTimezone()
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->_isDateOnly) {
 | 
			
		||||
            return false;
 | 
			
		||||
        } else {
 | 
			
		||||
            return parent::getTimezone();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getOffset()
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->_isDateOnly) {
 | 
			
		||||
            return false;
 | 
			
		||||
        } else {
 | 
			
		||||
            return parent::getOffset();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -532,20 +532,27 @@ class Formatter extends Component
 | 
			
		||||
            return $this->nullDisplay;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // avoid time zone conversion for date-only values
 | 
			
		||||
        if ($type === 'date' && $timestamp->isDateOnly()) {
 | 
			
		||||
            $timeZone = $this->defaultTimeZone;
 | 
			
		||||
        } else {
 | 
			
		||||
            $timeZone = $this->timeZone;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($this->_intlLoaded) {
 | 
			
		||||
            if (strncmp($format, 'php:', 4) === 0) {
 | 
			
		||||
                $format = FormatConverter::convertDatePhpToIcu(substr($format, 4));
 | 
			
		||||
            }
 | 
			
		||||
            if (isset($this->_dateFormats[$format])) {
 | 
			
		||||
                if ($type === 'date') {
 | 
			
		||||
                    $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, $this->timeZone);
 | 
			
		||||
                    $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, $timeZone);
 | 
			
		||||
                } elseif ($type === 'time') {
 | 
			
		||||
                    $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $this->timeZone);
 | 
			
		||||
                    $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $timeZone);
 | 
			
		||||
                } else {
 | 
			
		||||
                    $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $this->timeZone);
 | 
			
		||||
                    $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $timeZone);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $this->timeZone, null, $format);
 | 
			
		||||
                $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $timeZone, null, $format);
 | 
			
		||||
            }
 | 
			
		||||
            if ($formatter === null) {
 | 
			
		||||
                throw new InvalidConfigException(intl_get_error_message());
 | 
			
		||||
@ -557,8 +564,8 @@ class Formatter extends Component
 | 
			
		||||
            } else {
 | 
			
		||||
                $format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale);
 | 
			
		||||
            }
 | 
			
		||||
            if ($this->timeZone != null) {
 | 
			
		||||
                $timestamp->setTimezone(new DateTimeZone($this->timeZone));
 | 
			
		||||
            if ($timeZone != null) {
 | 
			
		||||
                $timestamp->setTimezone(new DateTimeZone($timeZone));
 | 
			
		||||
            }
 | 
			
		||||
            return $timestamp->format($format);
 | 
			
		||||
        }
 | 
			
		||||
@ -575,7 +582,7 @@ class Formatter extends Component
 | 
			
		||||
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
 | 
			
		||||
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
 | 
			
		||||
     *
 | 
			
		||||
     * @return DateTime the normalized datetime value
 | 
			
		||||
     * @return DateTimeExtended the normalized datetime value
 | 
			
		||||
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
 | 
			
		||||
     */
 | 
			
		||||
    protected function normalizeDatetimeValue($value)
 | 
			
		||||
@ -589,17 +596,17 @@ class Formatter extends Component
 | 
			
		||||
        }
 | 
			
		||||
        try {
 | 
			
		||||
            if (is_numeric($value)) { // process as unix timestamp, which is always in UTC
 | 
			
		||||
                if (($timestamp = DateTime::createFromFormat('U', $value, new DateTimeZone('UTC'))) === false) {
 | 
			
		||||
                if (($timestamp = DateTimeExtended::createFromFormat('U', $value, new DateTimeZone('UTC'))) === false) {
 | 
			
		||||
                    throw new InvalidParamException("Failed to parse '$value' as a UNIX timestamp.");
 | 
			
		||||
                }
 | 
			
		||||
                return $timestamp;
 | 
			
		||||
            } elseif (($timestamp = DateTime::createFromFormat('Y-m-d', $value, new DateTimeZone($this->defaultTimeZone))) !== false) { // try Y-m-d format (support invalid dates like 2012-13-01)
 | 
			
		||||
            } elseif (($timestamp = DateTimeExtended::createFromFormat('Y-m-d', $value, new DateTimeZone($this->defaultTimeZone))) !== false) { // try Y-m-d format (support invalid dates like 2012-13-01)
 | 
			
		||||
                return $timestamp;
 | 
			
		||||
            } elseif (($timestamp = DateTime::createFromFormat('Y-m-d H:i:s', $value, new DateTimeZone($this->defaultTimeZone))) !== false) { // try Y-m-d H:i:s format (support invalid dates like 2012-13-01 12:63:12)
 | 
			
		||||
            } elseif (($timestamp = DateTimeExtended::createFromFormat('Y-m-d H:i:s', $value, new DateTimeZone($this->defaultTimeZone))) !== false) { // try Y-m-d H:i:s format (support invalid dates like 2012-13-01 12:63:12)
 | 
			
		||||
                return $timestamp;
 | 
			
		||||
            }
 | 
			
		||||
            // finally try to create a DateTime object with the value
 | 
			
		||||
            $timestamp = new DateTime($value, new DateTimeZone($this->defaultTimeZone));
 | 
			
		||||
            $timestamp = new DateTimeExtended($value, new DateTimeZone($this->defaultTimeZone));
 | 
			
		||||
            return $timestamp;
 | 
			
		||||
        } catch(\Exception $e) {
 | 
			
		||||
            throw new InvalidParamException("'$value' is not a valid date time value: " . $e->getMessage()
 | 
			
		||||
 | 
			
		||||
@ -629,6 +629,18 @@ class FormatterTest extends TestCase
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public function testDateOnlyValues()
 | 
			
		||||
    {
 | 
			
		||||
        date_default_timezone_set('Pacific/Kiritimati');
 | 
			
		||||
        // timzones with exactly 24h difference, ensure this test does not fail on a certain time
 | 
			
		||||
        $this->formatter->defaultTimeZone = 'Pacific/Kiritimati'; // always UTC+14
 | 
			
		||||
        $this->formatter->timeZone = 'Pacific/Honolulu'; // always UTC-10
 | 
			
		||||
 | 
			
		||||
        // when timezone conversion is made on this date, it will result in 2014-07-31 to be returned.
 | 
			
		||||
        // ensure this does not happen on date only values
 | 
			
		||||
        $this->assertSame('2014-08-01', $this->formatter->asDate('2014-08-01', 'yyyy-MM-dd'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // number format
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user