yiisoft / yii2

Yii 2: The Fast, Secure and Professional PHP Framework
http://www.yiiframework.com
BSD 3-Clause "New" or "Revised" License
14.24k stars 6.91k forks source link

DateValidator incorrectly transforms date value in non-UTC timezone #14795

Closed PowerGamer1 closed 4 years ago

PowerGamer1 commented 7 years ago

When DateValidator is configured to parse an input value in a specific time zone (different from UTC) and output the result (timestampAttribute) in the same time zone the result is different from the input value (should be the same).

The code to blame (https://github.com/yiisoft/yii2/blob/master/framework/validators/DateValidator.php#L407):

$date = DateTime::createFromFormat($format, $value, new \DateTimeZone($hasTimeInfo ? $this->timeZone : 'UTC'));

should be changed to

$date = DateTime::createFromFormat($format, $value, new \DateTimeZone($this->timeZone));

What steps will reproduce the problem?

class TestModel extends \yii\base\Model
{
    public $in;
    public $out;
    public function rules()
    {
        return [
            ['in', \yii\validators\DateValidator::class,
                'format' => 'php:Y-m-d',
                'timeZone' => 'America/Sao_Paulo',
                'timestampAttribute' => 'out',
                'timestampAttributeFormat' => 'php:Y-m-d',
                'timestampAttributeTimeZone'=> 'America/Sao_Paulo',
            ],
        ];
    }
}

$model = new TestModel();
$model->in = '2017-09-11';
$model->validate();
var_dump($model->in);
var_dump($model->out);

What is the expected result?

string(10) "2017-09-11" string(10) "2017-09-11"

What do you get instead?

string(10) "2017-09-11" string(10) "2017-09-10"

Additional info

Q A
Yii version 2.0.12
PHP version 7.0.18
Operating system Windows 10
cebe commented 7 years ago

if the values are date-only, why do you set a timezone?

PowerGamer1 commented 7 years ago

@cebe

if the values are date-only, why do you set a timezone?

  1. Why not?
  2. DateValidator itself does exactly that during conversions.
  3. The date is actually treated as date 00:00:00 or date 23:59:59 in the real code so the timezone is required ('timestampAttributeFormat' => 'php:Y-m-d 00:00:00').
cebe commented 7 years ago

Why not?

because a date-only value has no time information and thus no time zone. What are you trying to achieve with specifying a timezone here?

DateValidator itself does exactly that during conversions.

If a date-only value is parsed, the timezone is set to UTC and the time set to 00:00:00 to be able to convert the date value into a unix timestamp. A unix timestamp is in UTC by definition.

There is no sense in adding timezone info for a date. Remove the timezone info and all should work fine.

PowerGamer1 commented 7 years ago

There is no sense in adding timezone info for a date.

The fact that any specific date without time is the same in any time zone does not mean it makes no sense to speak about a date in a time zone. You stated it yourself that internally the code works even with date-only values within UTC time zone. For your "no sense in adding timezone info for a date" statement to actually have sense, DateValidator shouldn't try to convert a date-only value into a date-time value (since UNIX timestamp is a date-time value) at all - yet it does and once you add a time to a date for any reason you would need a timezone to correctly work with resulting date-time value.

Also, if I want to convert input date into a date-time value like so:

 'timestampAttributeFormat' => 'php:Y-m-d 00:00:00' 

it is only natural to also specify a time zone for a FULL date-time value:

 'timestampAttributeTimeZone'=> 'America/Sao_Paulo'

Anyway, at the very least, if you continue to insist on your "makes no sense" statement, then by your logic setting a time zone for the DateValidator when input value is just a date should have no effect on the result of processing of date value by DateValidator. As you can see from my example it has an effect so what I posted is still a bug.

Also:

    /**
     * @var string the timezone to use for parsing date and time values.
      *  ...
     */
    public $timeZone;

clearly states that timeZone is USED for parsing date-only values too, otherwise the comment would have said something like: "the timezone to use for parsing values with times only."

And:

/**
 * ...
 * If you want to avoid the time zone conversion, make sure that [[timeZone]] and
 * [[timestampAttributeTimeZone]] are the same.
  * ...
*/
class DateValidator extends Validator

EXACTLY what I have done in my example - specified the same INPUT and OUTPUT time zones to have the IDENTICAL input and output value.

Conclusion - implement my fix and:

PowerGamer1 commented 7 years ago

The whole doc for DateValidator should be rewritten to state how EXACTLY this class treats date-only values - it treats them as if "date 00:00:00" values. Since date-only value is converted into date-time value we would need to correctly specify input and output timezones to get correct results. When you view it like that everything makes sense (and "There is no sense in adding timezone info for a date." is no longer relevant).

razorsharpshady commented 6 years ago

Is there any update on this issue? I'm using Yii version 2.0.15.1 and the timestamp is still recorded in UTC timezone even if I set $timeZone & $timestampAttributeTimeZone to something else.

cebe commented 6 years ago

@razorsharpshady did you set $timestampAttributeFormat?

andrii-borysov-me commented 4 years ago

Hi,

I have another use case as argument to use timezone in date-only parsing or to provide an option to alter this behavior.

So, I have a table with models with a creation timestamp field, and I have a separate search form with a 'created from' date field. This date field in the search should include only those models, which creation timestamp is greater or equal to the field value. The creation timestamp is stored as UNIX timestamp in the database. In the table the creation timestamp is formatted to full date and time format and it uses current time zone ("America/Los_Angeles", or any timezone with negative offset).

Test scenario:

  1. App configuration:
    $config = [
    'timeZone' => 'America/Los_Angeles',
    ];
  2. Configuration of DateValidator in the search model:
    'createdAtFromParseDate' => ['created_at_from', 'date',
    'timestampAttribute' => 'created_at_from',
    'min' => '01-01-1990',
    'format' => 'dd-MM-yyyy',
    ],
  3. Enter the date search field value '27-01-2020'
  4. Field created_at_from has value 1580083200 (which is in UTC - Monday, 27 January 2020, 00:00:00)
  5. The value is applied in the ActiveQuery in the way like:
    $query->andFilterWhere(['>=', 'created_at', $this->created_at_from]);
  6. The value is printed in the search form as date with specified format, like this:
    <?= $form->field($model, 'created_at_from')->textInput([
    'value' => Yii::$app->formatter->asDate($model->created_at_from, 'dd-MM-yyyy'),
    ]) ?>
  7. As result, the web page displays '26-01-2020' in the printed search form.

So, we have few problems here: 1) Inconsistency between builtin DateValidator and builtin Formatter when using non-UTC timezone, UNIX timestamp and date format. The builtin formatter treats the timestamp as "has time info" and applies the given Formatter.timeZone (instead of Formatter.defaultTimeZone) for date-only formatting.

2) UTC timestamp has wrong time component, and thus the search result will include models, which creation timestamp will be printed like '26-01-2020 18:00:00', which is not expected.

P.S. The test cases in #17018 don't use time zones with negative offset.

bizley commented 4 years ago

With the recent change introduced in #18358 is it possible to finally close this 3 YO issue?