Passed
Pull Request — master (#25)
by Alexander
14:50
created

Date::validateAttribute()   C

Complexity

Conditions 14
Paths 12

Size

Total Lines 39
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 26
c 0
b 0
f 0
dl 0
loc 39
rs 6.2666
cc 14
nc 12
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace Yiisoft\Validator\Rule;
3
4
use DateTime;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Yiisoft\Validator\Rule\DateTime. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
5
use IntlDateFormatter;
6
use Yiisoft\Validator\FormatConverterHelper;
7
use Yiisoft\Validator\Result;
8
use Yiisoft\Validator\Rule;
9
10
/**
11
 * DateValidator verifies if the attribute represents a date, time or datetime in a proper [[format]].
12
 *
13
 * It can also parse internationalized dates in a specific [[locale]] like e.g. `12 мая 2014` when [[format]]
14
 * is configured to use a time pattern in ICU format.
15
 *
16
 * It is further possible to limit the date within a certain range using [[min]] and [[max]].
17
 *
18
 * Additional to validating the date it can also export the parsed timestamp as a machine readable format
19
 * which can be configured using [[timestampAttribute]]. For values that include time information (not date-only values)
20
 * also the time zone will be adjusted. The time zone of the input value is assumed to be the one specified by the [[timeZone]]
21
 * property and the target timeZone will be UTC when [[timestampAttributeFormat]] is `null` (exporting as UNIX timestamp)
22
 * or [[timestampAttributeTimeZone]] otherwise. If you want to avoid the time zone conversion, make sure that [[timeZone]] and
23
 * [[timestampAttributeTimeZone]] are the same.
24
 */
25
class Date extends Rule
26
{
27
    /**
28
     * Constant for specifying the validation [[type]] as a date value, used for validation with intl short format.
29
     * @see type
30
     *
31
     * TODO: split into 3 separate classes?
32
     */
33
    const TYPE_DATE = 'date';
34
    /**
35
     * Constant for specifying the validation [[type]] as a datetime value, used for validation with intl short format.
36
     * @see type
37
     */
38
    const TYPE_DATETIME = 'datetime';
39
    /**
40
     * Constant for specifying the validation [[type]] as a time value, used for validation with intl short format.
41
     * @see type
42
     */
43
    const TYPE_TIME = 'time';
44
45
    /**
46
     * @var string the type of the validator. Indicates, whether a date, time or datetime value should be validated.
47
     * This property influences the default value of [[format]] and also sets the correct behavior when [[format]] is one of the intl
48
     * short formats, `short`, `medium`, `long`, or `full`.
49
     *
50
     * This is only effective when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
51
     *
52
     * This property can be set to the following values:
53
     *
54
     * - [[TYPE_DATE]] - (default) for validating date values only, that means only values that do not include a time range are valid.
55
     * - [[TYPE_DATETIME]] - for validating datetime values, that contain a date part as well as a time part.
56
     * - [[TYPE_TIME]] - for validating time values, that contain no date information.
57
     *
58
     */
59
    public $type = self::TYPE_DATE;
60
    /**
61
     * @var string the date format that the value being validated should follow.
62
     * This can be a date time pattern as described in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax).
63
     *
64
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the PHP Datetime class.
65
     * Please refer to <http://php.net/manual/en/datetime.createfromformat.php> on supported formats.
66
     *
67
     * If this property is not set, the default value will be obtained from `Yii::getApp()->formatter->dateFormat`, see [[\yii\i18n\Formatter::dateFormat]] for details.
68
     * Since version 2.0.8 the default value will be determined from different formats of the formatter class,
69
     * dependent on the value of [[type]]:
70
     *
71
     * - if type is [[TYPE_DATE]], the default value will be taken from [[\yii\i18n\Formatter::dateFormat]],
72
     * - if type is [[TYPE_DATETIME]], it will be taken from [[\yii\i18n\Formatter::datetimeFormat]],
73
     * - and if type is [[TYPE_TIME]], it will be [[\yii\i18n\Formatter::timeFormat]].
74
     *
75
     * Here are some example values:
76
     *
77
     * ```php
78
     * 'MM/dd/yyyy' // date in ICU format
79
     * 'php:m/d/Y' // the same date in PHP format
80
     * 'MM/dd/yyyy HH:mm' // not only dates but also times can be validated
81
     * ```
82
     *
83
     * **Note:** the underlying date parsers being used vary dependent on the format. If you use the ICU format and
84
     * the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed, the [IntlDateFormatter](http://php.net/manual/en/intldateformatter.parse.php)
85
     * is used to parse the input value. In all other cases the PHP [DateTime](http://php.net/manual/en/datetime.createfromformat.php) class
86
     * is used. The IntlDateFormatter has the advantage that it can parse international dates like `12. Mai 2015` or `12 мая 2014`, while the
87
     * PHP parser is limited to English only. The PHP parser however is more strict about the input format as it will not accept
88
     * `12.05.05` for the format `php:d.m.Y`, but the IntlDateFormatter will accept it for the format `dd.MM.yyyy`.
89
     * If you need to use the IntlDateFormatter you can avoid this problem by specifying a [[min|minimum date]].
90
     */
91
    public $format;
92
    /**
93
     * @var string the locale ID that is used to localize the date parsing.
94
     * This is only effective when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
95
     * If not set, the locale of the [[\yii\base\Application::formatter|formatter]] will be used.
96
     * See also [[\yii\i18n\Formatter::locale]].
97
     */
98
    public $locale;
99
    /**
100
     * @var string the timezone to use for parsing date and time values.
101
     * This can be any value that may be passed to [date_default_timezone_set()](http://www.php.net/manual/en/function.date-default-timezone-set.php)
102
     * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
103
     * Refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available timezones.
104
     * If this property is not set, [[\yii\base\Application::timeZone]] will be used.
105
     */
106
    public $timeZone;
107
    /**
108
     * @var string the name of the attribute to receive the parsing result.
109
     * When this property is not null and the validation is successful, the named attribute will
110
     * receive the parsing result.
111
     *
112
     * This can be the same attribute as the one being validated. If this is the case,
113
     * the original value will be overwritten with the timestamp value after successful validation.
114
     *
115
     * Note, that when using this property, the input value will be converted to a unix timestamp,
116
     * which by definition is in UTC, so a conversion from the [[$timeZone|input time zone]] to UTC
117
     * will be performed. When defining [[$timestampAttributeFormat]] you can control the conversion by
118
     * setting [[$timestampAttributeTimeZone]] to a different value than `'UTC'`.
119
     *
120
     * @see timestampAttributeFormat
121
     * @see timestampAttributeTimeZone
122
     */
123
    public $timestampAttribute;
124
    /**
125
     * @var string the format to use when populating the [[timestampAttribute]].
126
     * The format can be specified in the same way as for [[format]].
127
     *
128
     * If not set, [[timestampAttribute]] will receive a UNIX timestamp.
129
     * If [[timestampAttribute]] is not set, this property will be ignored.
130
     * @see format
131
     * @see timestampAttribute
132
     */
133
    public $timestampAttributeFormat;
134
    /**
135
     * @var string the timezone to use when populating the [[timestampAttribute]]. Defaults to `UTC`.
136
     *
137
     * This can be any value that may be passed to [date_default_timezone_set()](http://www.php.net/manual/en/function.date-default-timezone-set.php)
138
     * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
139
     * Refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available timezones.
140
     *
141
     * If [[timestampAttributeFormat]] is not set, this property will be ignored.
142
     * @see timestampAttributeFormat
143
     */
144
    public $timestampAttributeTimeZone = 'UTC';
145
    /**
146
     * @var int|string upper limit of the date. Defaults to null, meaning no upper limit.
147
     * This can be a unix timestamp or a string representing a date time value.
148
     * If this property is a string, [[format]] will be used to parse it.
149
     * @see tooBig for the customized message used when the date is too big.
150
     */
151
    public $max;
152
    /**
153
     * @var int|string lower limit of the date. Defaults to null, meaning no lower limit.
154
     * This can be a unix timestamp or a string representing a date time value.
155
     * If this property is a string, [[format]] will be used to parse it.
156
     * @see tooSmall for the customized message used when the date is too small.
157
     */
158
    public $min;
159
    /**
160
     * @var string user-defined error message used when the value is bigger than [[max]].
161
     */
162
    public $tooBig;
163
    /**
164
     * @var string user-defined error message used when the value is smaller than [[min]].
165
     */
166
    public $tooSmall;
167
    /**
168
     * @var string user friendly value of upper limit to display in the error message.
169
     * If this property is null, the value of [[max]] will be used (before parsing).
170
     */
171
    public $maxString;
172
    /**
173
     * @var string user friendly value of lower limit to display in the error message.
174
     * If this property is null, the value of [[min]] will be used (before parsing).
175
     */
176
    public $minString;
177
    /**
178
     * @var string user-defined error message used when the value is invalid.
179
     */
180
    private $message;
181
182
    /**
183
     * @var array map of short format names to IntlDateFormatter constant values.
184
     */
185
    private $_dateFormats = [
186
        'short' => 3, // IntlDateFormatter::SHORT,
187
        'medium' => 2, // IntlDateFormatter::MEDIUM,
188
        'long' => 1, // IntlDateFormatter::LONG,
189
        'full' => 0, // IntlDateFormatter::FULL,
190
    ];
191
192
    /**
193
     * Date constructor.
194
     * @param $format string date format
195
     * @param $locale string locale code in ICU format
196
     * @param $timeZone string timezone to use for parsing date and time values
197
     */
198
    public function __construct($format, $locale, $timeZone)
199
    {
200
        if ($this->message === null) {
201
            $this->message = 'The format of {attribute} is invalid.';
202
        }
203
        $this->format = $format;
204
        $this->locale = $locale;
205
        $this->timeZone = $timeZone;
206
207
//        if (!empty($type) && !in_array($type, [self::TYPE_DATE, self::TYPE_DATETIME, self::TYPE_TIME])) {
208
//            throw new InvalidConfigException('Unknown validation type set for DateValidator::$type: ' . $this->type);
209
//        }
210
//        if (!empty($format)) {
211
//            throw new InvalidConfigException('Unknown validation type set for DateValidator::$type: ' . $this->type);
212
//        }
213
214
//        if ($this->format === null) {
215
//            if ($this->type === self::TYPE_DATE) {
216
//                $this->format = Yii::getApp()->formatter->dateFormat;
217
//            } elseif ($this->type === self::TYPE_DATETIME) {
218
//                $this->format = Yii::getApp()->formatter->datetimeFormat;
219
//            } elseif ($this->type === self::TYPE_TIME) {
220
//                $this->format = Yii::getApp()->formatter->timeFormat;
221
//            } else {
222
//
223
//            }
224
//        }
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230
    public function validateAttribute($model, $attribute)
231
    {
232
        $result = new Result();
233
234
        $value = $model->$attribute;
235
        if ($this->isEmpty($value)) {
236
            if ($this->timestampAttribute !== null) {
237
                $model->{$this->timestampAttribute} = null;
238
            }
239
            return $result;
240
        }
241
242
        $timestamp = $this->parseDateValue($value);
243
        if ($timestamp === false) {
244
            if ($this->timestampAttribute === $attribute) {
245
                if ($this->timestampAttributeFormat === null) {
246
                    if (is_int($value)) {
247
                        return $result;
248
                    }
249
                } else {
250
                    if ($this->parseDateValueFormat($value, $this->timestampAttributeFormat) !== false) {
251
                        return $result;
252
                    }
253
                }
254
            }
255
            $result->addError($this->message);
256
        } elseif ($this->min !== null && $timestamp < $this->min) {
257
            $result->addError($this->formatMessage($this->tooSmall, ['min' => $this->minString]));
258
        } elseif ($this->max !== null && $timestamp > $this->max) {
259
            $result->addError($this->formatMessage($this->tooBig, ['max' => $this->maxString]));
260
        } elseif ($this->timestampAttribute !== null) {
261
            if ($this->timestampAttributeFormat === null) {
262
                $model->{$this->timestampAttribute} = $timestamp;
263
            } else {
264
                $model->{$this->timestampAttribute} = $this->formatTimestamp($timestamp, $this->timestampAttributeFormat);
265
            }
266
        }
267
268
        return $result;
269
    }
270
271
    public function validateValue($value): Result
272
    {
273
        $result = new Result();
274
275
        $timestamp = $this->parseDateValue($value);
276
        if ($timestamp === false) {
277
            $result->addError($this->message);
278
        } elseif ($this->min !== null && $timestamp < $this->min) {
279
            $result->addError($this->formatMessage($this->tooSmall, ['min' => $this->minString]));
280
        } elseif ($this->max !== null && $timestamp > $this->max) {
281
            $result->addError($this->formatMessage($this->tooBig, ['max' => $this->maxString]));
282
        }
283
284
        return $result;
285
    }
286
287
    /**
288
     * Parses date string into UNIX timestamp.
289
     *
290
     * @param string $value string representing date
291
     * @return int|false a UNIX timestamp or `false` on failure.
292
     */
293
    protected function parseDateValue($value)
294
    {
295
        // TODO consider merging these methods into single one at 2.1
296
        return $this->parseDateValueFormat($value, $this->format);
297
    }
298
299
    /**
300
     * Parses date string into UNIX timestamp.
301
     *
302
     * @param string $value string representing date
303
     * @param string $format expected date format
304
     * @return int|false a UNIX timestamp or `false` on failure.
305
     */
306
    private function parseDateValueFormat($value, $format)
307
    {
308
        if (is_array($value)) {
0 ignored issues
show
introduced by
The condition is_array($value) is always false.
Loading history...
309
            return false;
310
        }
311
        if (strncmp($format, 'php:', 4) === 0) {
312
            $format = substr($format, 4);
313
        } else {
314
            if (extension_loaded('intl')) {
315
                return $this->parseDateValueIntl($value, $format);
316
            }
317
318
            // fallback to PHP if intl is not installed
319
            $format = FormatConverter::convertDateIcuToPhp($format, 'date');
0 ignored issues
show
Bug introduced by
The type Yiisoft\Validator\Rule\FormatConverter was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
320
        }
321
322
        return $this->parseDateValuePHP($value, $format);
323
    }
324
325
    /**
326
     * Parses a date value using the IntlDateFormatter::parse().
327
     * @param string $value string representing date
328
     * @param string $format the expected date format
329
     * @return int|bool a UNIX timestamp or `false` on failure.
330
     * @throws InvalidConfigException
331
     */
332
    private function parseDateValueIntl($value, $format)
333
    {
334
        if (isset($this->_dateFormats[$format])) {
335
            if ($this->type === self::TYPE_DATE) {
336
                $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, 'UTC');
337
            } elseif ($this->type === self::TYPE_DATETIME) {
338
                $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $this->timeZone);
339
            } elseif ($this->type === self::TYPE_TIME) {
340
                $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $this->timeZone);
341
            } else {
342
                throw new InvalidConfigException('Unknown validation type set for DateValidator::$type: ' . $this->type);
0 ignored issues
show
Bug introduced by
The type Yiisoft\Validator\Rule\InvalidConfigException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
343
            }
344
        } else {
345
            // if no time was provided in the format string set time to 0 to get a simple date timestamp
346
            $hasTimeInfo = (strpbrk($format, 'ahHkKmsSA') !== false);
347
            $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $hasTimeInfo ? $this->timeZone : 'UTC', null, $format);
348
        }
349
        // enable strict parsing to avoid getting invalid date values
350
        $formatter->setLenient(false);
351
352
        // There should not be a warning thrown by parse() but this seems to be the case on windows so we suppress it here
353
        // See https://github.com/yiisoft/yii2/issues/5962 and https://bugs.php.net/bug.php?id=68528
354
        $parsePos = 0;
355
        $parsedDate = @$formatter->parse($value, $parsePos);
356
        if ($parsedDate === false || $parsePos !== mb_strlen($value)) {
357
            return false;
358
        }
359
360
        return $parsedDate;
361
    }
362
363
    /**
364
     * Parses a date value using the DateTime::createFromFormat().
365
     * @param string $value string representing date
366
     * @param string $format the expected date format
367
     * @return int|bool a UNIX timestamp or `false` on failure.
368
     */
369
    private function parseDateValuePHP($value, $format)
370
    {
371
        // if no time was provided in the format string set time to 0 to get a simple date timestamp
372
        $hasTimeInfo = (strpbrk($format, 'HhGgisU') !== false);
373
374
        $date = DateTime::createFromFormat($format, $value, new \DateTimeZone($hasTimeInfo ? $this->timeZone : 'UTC'));
375
        $errors = DateTime::getLastErrors();
376
        if ($date === false || $errors['error_count'] || $errors['warning_count']) {
377
            return false;
378
        }
379
380
        if (!$hasTimeInfo) {
381
            $date->setTime(0, 0, 0);
382
        }
383
384
        return $date->getTimestamp();
385
    }
386
387
    /**
388
     * Formats a timestamp using the specified format.
389
     * @param int $timestamp
390
     * @param string $format
391
     * @return string
392
     */
393
    private function formatTimestamp($timestamp, $format)
394
    {
395
        if (strncmp($format, 'php:', 4) === 0) {
396
            $format = substr($format, 4);
397
        } else {
398
            $format = FormatConverterHelper::convertDateIcuToPhp($format, 'date', $this->locale);
399
        }
400
401
        $date = new DateTime();
402
        $date->setTimestamp($timestamp);
403
        $date->setTimezone(new \DateTimeZone($this->timestampAttributeTimeZone));
404
        return $date->format($format);
405
    }
406
407
    public function getMessage()
408
    {
409
        return $this->message;
410
    }
411
412
    public function type($type): self
413
    {
414
        $this->type = $type;
415
416
        return $this;
417
    }
418
419
    public function timestampAttribute($attribute): self
420
    {
421
        $this->timestampAttribute = $attribute;
422
423
        return $this;
424
    }
425
426
    public function timestampAttributeFormat($format): self
427
    {
428
        $this->timestampAttributeFormat = $format;
429
430
        return $this;
431
    }
432
433
    public function timestampAttributeTimeZone($timezone): self
434
    {
435
        $this->timestampAttributeTimeZone = $timezone;
436
437
        return $this;
438
    }
439
440
    public function min($value): self
441
    {
442
        $this->min = $value;
443
        if ($this->min !== null && $this->tooSmall === null) {
444
            $this->tooSmall = '{attribute} must be no less than {min}.';
445
        }
446
447
        if ($this->minString === null) {
448
            $this->minString = (string) $this->min;
449
        }
450
        if ($this->min !== null && is_string($this->min)) {
451
            $timestamp = $this->parseDateValue($this->min);
452
            if ($timestamp === false) {
453
                throw new InvalidConfigException("Invalid min date value: {$this->min}");
454
            }
455
            $this->min = $timestamp;
456
        }
457
458
        return $this;
459
    }
460
461
    public function max($value): self
462
    {
463
        $this->max = $value;
464
        if ($this->max !== null && $this->tooBig === null) {
465
            $this->tooBig = '{attribute} must be no greater than {max}.';
466
        }
467
        if ($this->maxString === null) {
468
            $this->maxString = (string) $this->max;
469
        }
470
        if ($this->max !== null && is_string($this->max)) {
471
            $timestamp = $this->parseDateValue($this->max);
472
            if ($timestamp === false) {
473
                throw new InvalidConfigException("Invalid max date value: {$this->max}");
474
            }
475
            $this->max = $timestamp;
476
        }
477
478
        return $this;
479
    }
480
481
482
}
483