Completed
Push — 11139-each-validator-attribute... ( a0e614 )
by Dmitry
08:34
created

DateValidator   B

Complexity

Total Complexity 54

Size/Duplication

Total Lines 332
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 98.28%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 54
c 1
b 0
f 0
lcom 1
cbo 4
dl 0
loc 332
ccs 114
cts 116
cp 0.9828
rs 7.0642

8 Methods

Rating   Name   Duplication   Size   Complexity  
F init() 0 42 17
C validateAttribute() 0 29 12
B validateValue() 0 13 6
A parseDateValue() 0 5 1
A parseDateValueFormat() 0 17 4
B parseDateValueIntl() 0 22 6
B parseDateValuePHP() 0 16 6
A formatTimestamp() 0 13 2

How to fix   Complexity   

Complex Class

Complex classes like DateValidator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DateValidator, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\validators;
9
10
use DateTime;
11
use IntlDateFormatter;
12
use Yii;
13
use yii\base\InvalidConfigException;
14
use yii\helpers\FormatConverter;
15
16
/**
17
 * DateValidator verifies if the attribute represents a date, time or datetime in a proper [[format]].
18
 *
19
 * It can also parse internationalized dates in a specific [[locale]] like e.g. `12 мая 2014` when [[format]]
20
 * is configured to use a time pattern in ICU format.
21
 *
22
 * It is further possible to limit the date within a certain range using [[min]] and [[max]].
23
 *
24
 * Additional to validating the date it can also export the parsed timestamp as a machine readable format
25
 * which can be configured using [[timestampAttribute]].
26
 *
27
 * @author Qiang Xue <[email protected]>
28
 * @author Carsten Brandt <[email protected]>
29
 * @since 2.0
30
 */
31
class DateValidator extends Validator
32
{
33
    /**
34
     * @var string the date format that the value being validated should follow.
35
     * 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).
36
     *
37
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the PHP Datetime class.
38
     * Please refer to <http://php.net/manual/en/datetime.createfromformat.php> on supported formats.
39
     *
40
     * If this property is not set, the default value will be obtained from `Yii::$app->formatter->dateFormat`, see [[\yii\i18n\Formatter::dateFormat]] for details.
41
     *
42
     * Here are some example values:
43
     *
44
     * ```php
45
     * 'MM/dd/yyyy' // date in ICU format
46
     * 'php:m/d/Y' // the same date in PHP format
47
     * 'MM/dd/yyyy HH:mm' // not only dates but also times can be validated
48
     * ```
49
     *
50
     * **Note:** the underlying date parsers being used vary dependent on the format. If you use the ICU format and
51
     * the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed, the [IntlDateFormatter](http://php.net/manual/en/intldateformatter.parse.php)
52
     * is used to parse the input value. In all other cases the PHP [DateTime](http://php.net/manual/en/datetime.createfromformat.php) class
53
     * is used. The IntlDateFormatter has the advantage that it can parse international dates like `12. Mai 2015` or `12 мая 2014`, while the
54
     * PHP parser is limited to English only. The PHP parser however is more strict about the input format as it will not accept
55
     * `12.05.05` for the format `php:d.m.Y`, but the IntlDateFormatter will accept it for the format `dd.MM.yyyy`.
56
     * If you need to use the IntlDateFormatter you can avoid this problem by specifying a [[min|minimum date]].
57
     */
58
    public $format;
59
    /**
60
     * @var string the locale ID that is used to localize the date parsing.
61
     * This is only effective when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
62
     * If not set, the locale of the [[\yii\base\Application::formatter|formatter]] will be used.
63
     * See also [[\yii\i18n\Formatter::locale]].
64
     */
65
    public $locale;
66
    /**
67
     * @var string the timezone to use for parsing date and time values.
68
     * 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)
69
     * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
70
     * Refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available timezones.
71
     * If this property is not set, [[\yii\base\Application::timeZone]] will be used.
72
     */
73
    public $timeZone;
74
    /**
75
     * @var string the name of the attribute to receive the parsing result.
76
     * When this property is not null and the validation is successful, the named attribute will
77
     * receive the parsing result.
78
     *
79
     * This can be the same attribute as the one being validated. If this is the case,
80
     * the original value will be overwritten with the timestamp value after successful validation.
81
     * @see timestampAttributeFormat
82
     * @see timestampAttributeTimeZone
83
     */
84
    public $timestampAttribute;
85
    /**
86
     * @var string the format to use when populating the [[timestampAttribute]].
87
     * The format can be specified in the same way as for [[format]].
88
     *
89
     * If not set, [[timestampAttribute]] will receive a UNIX timestamp.
90
     * If [[timestampAttribute]] is not set, this property will be ignored.
91
     * @see format
92
     * @see timestampAttribute
93
     * @since 2.0.4
94
     */
95
    public $timestampAttributeFormat;
96
    /**
97
     * @var string the timezone to use when populating the [[timestampAttribute]]. Defaults to `UTC`.
98
     *
99
     * 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)
100
     * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
101
     * Refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available timezones.
102
     *
103
     * If [[timestampAttributeFormat]] is not set, this property will be ignored.
104
     * @see timestampAttributeFormat
105
     * @since 2.0.4
106
     */
107
    public $timestampAttributeTimeZone = 'UTC';
108
    /**
109
     * @var integer|string upper limit of the date. Defaults to null, meaning no upper limit.
110
     * This can be a unix timestamp or a string representing a date time value.
111
     * If this property is a string, [[format]] will be used to parse it.
112
     * @see tooBig for the customized message used when the date is too big.
113
     * @since 2.0.4
114
     */
115
    public $max;
116
    /**
117
     * @var integer|string lower limit of the date. Defaults to null, meaning no lower limit.
118
     * This can be a unix timestamp or a string representing a date time value.
119
     * If this property is a string, [[format]] will be used to parse it.
120
     * @see tooSmall for the customized message used when the date is too small.
121
     * @since 2.0.4
122
     */
123
    public $min;
124
    /**
125
     * @var string user-defined error message used when the value is bigger than [[max]].
126
     * @since 2.0.4
127
     */
128
    public $tooBig;
129
    /**
130
     * @var string user-defined error message used when the value is smaller than [[min]].
131
     * @since 2.0.4
132
     */
133
    public $tooSmall;
134
    /**
135
     * @var string user friendly value of upper limit to display in the error message.
136
     * If this property is null, the value of [[max]] will be used (before parsing).
137
     * @since 2.0.4
138
     */
139
    public $maxString;
140
    /**
141
     * @var string user friendly value of lower limit to display in the error message.
142
     * If this property is null, the value of [[min]] will be used (before parsing).
143
     * @since 2.0.4
144
     */
145
    public $minString;
146
147
    /**
148
     * @var array map of short format names to IntlDateFormatter constant values.
149
     */
150
    private $_dateFormats = [
151
        'short'  => 3, // IntlDateFormatter::SHORT,
152
        'medium' => 2, // IntlDateFormatter::MEDIUM,
153
        'long'   => 1, // IntlDateFormatter::LONG,
154
        'full'   => 0, // IntlDateFormatter::FULL,
155
    ];
156
157
158
    /**
159
     * @inheritdoc
160
     */
161 145
    public function init()
162
    {
163 145
        parent::init();
164 145
        if ($this->message === null) {
165 145
            $this->message = Yii::t('yii', 'The format of {attribute} is invalid.');
166 145
        }
167 145
        if ($this->format === null) {
168 4
            $this->format = Yii::$app->formatter->dateFormat;
169 4
        }
170 145
        if ($this->locale === null) {
171 144
            $this->locale = Yii::$app->language;
172 144
        }
173 145
        if ($this->timeZone === null) {
174 25
            $this->timeZone = Yii::$app->timeZone;
175 25
        }
176 145
        if ($this->min !== null && $this->tooSmall === null) {
177 4
            $this->tooSmall = Yii::t('yii', '{attribute} must be no less than {min}.');
178 4
        }
179 145
        if ($this->max !== null && $this->tooBig === null) {
180 4
            $this->tooBig = Yii::t('yii', '{attribute} must be no greater than {max}.');
181 4
        }
182 145
        if ($this->maxString === null) {
183 145
            $this->maxString = (string) $this->max;
184 145
        }
185 145
        if ($this->minString === null) {
186 145
            $this->minString = (string) $this->min;
187 145
        }
188 145
        if ($this->max !== null && is_string($this->max)) {
189 4
            $timestamp = $this->parseDateValue($this->max);
190 4
            if ($timestamp === false) {
191
                throw new InvalidConfigException("Invalid max date value: {$this->max}");
192
            }
193 4
            $this->max = $timestamp;
0 ignored issues
show
Documentation Bug introduced by
It seems like $timestamp can also be of type boolean. However, the property $max is declared as type integer|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
194 4
        }
195 145
        if ($this->min !== null && is_string($this->min)) {
196 4
            $timestamp = $this->parseDateValue($this->min);
197 4
            if ($timestamp === false) {
198
                throw new InvalidConfigException("Invalid min date value: {$this->min}");
199
            }
200 4
            $this->min = $timestamp;
0 ignored issues
show
Documentation Bug introduced by
It seems like $timestamp can also be of type boolean. However, the property $min is declared as type integer|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
201 4
        }
202 145
    }
203
204
    /**
205
     * @inheritdoc
206
     */
207 136
    public function validateAttribute($model, $attribute)
208
    {
209 136
        $value = $model->$attribute;
210 136
        $timestamp = $this->parseDateValue($value);
211 136
        if ($timestamp === false) {
212 13
            if ($this->timestampAttribute === $attribute) {
213 1
                if ($this->timestampAttributeFormat === null) {
214 1
                    if (is_int($value)) {
215 1
                        return;
216
                    }
217 1
                } else {
218 1
                    if ($this->parseDateValueFormat($value, $this->timestampAttributeFormat) !== false) {
219 1
                        return;
220
                    }
221
                }
222 1
            }
223 13
            $this->addError($model, $attribute, $this->message, []);
224 136
        } elseif ($this->min !== null && $timestamp < $this->min) {
225 2
            $this->addError($model, $attribute, $this->tooSmall, ['min' => $this->minString]);
226 135
        } elseif ($this->max !== null && $timestamp > $this->max) {
227 2
            $this->addError($model, $attribute, $this->tooBig, ['max' => $this->maxString]);
228 135
        } elseif ($this->timestampAttribute !== null) {
229 132
            if ($this->timestampAttributeFormat === null) {
230 39
                $model->{$this->timestampAttribute} = $timestamp;
231 39
            } else {
232 102
                $model->{$this->timestampAttribute} = $this->formatTimestamp($timestamp, $this->timestampAttributeFormat);
0 ignored issues
show
Bug introduced by
It seems like $timestamp defined by $this->parseDateValue($value) on line 210 can also be of type boolean; however, yii\validators\DateValidator::formatTimestamp() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
233
            }
234 132
        }
235 136
    }
236
237
    /**
238
     * @inheritdoc
239
     */
240 8
    protected function validateValue($value)
241
    {
242 8
        $timestamp = $this->parseDateValue($value);
243 8
        if ($timestamp === false) {
244 6
            return [$this->message, []];
245 8
        } elseif ($this->min !== null && $timestamp < $this->min) {
246 2
            return [$this->tooSmall, ['min' => $this->minString]];
247 8
        } elseif ($this->max !== null && $timestamp > $this->max) {
248 2
            return [$this->tooBig, ['max' => $this->maxString]];
249
        } else {
250 8
            return null;
251
        }
252
    }
253
254
    /**
255
     * Parses date string into UNIX timestamp
256
     *
257
     * @param string $value string representing date
258
     * @return integer|false a UNIX timestamp or `false` on failure.
259
     */
260 144
    protected function parseDateValue($value)
261
    {
262
        // TODO consider merging these methods into single one at 2.1
263 144
        return $this->parseDateValueFormat($value, $this->format);
264
    }
265
266
    /**
267
     * Parses date string into UNIX timestamp
268
     *
269
     * @param string $value string representing date
270
     * @param string $format expected date format
271
     * @return integer|false a UNIX timestamp or `false` on failure.
272
     */
273 144
    private function parseDateValueFormat($value, $format)
274
    {
275 144
        if (is_array($value)) {
276 12
            return false;
277
        }
278 144
        if (strncmp($format, 'php:', 4) === 0) {
279 16
            $format = substr($format, 4);
280 16
        } else {
281 137
            if (extension_loaded('intl')) {
282 69
                return $this->parseDateValueIntl($value, $format);
283
            } else {
284
                // fallback to PHP if intl is not installed
285 68
                $format = FormatConverter::convertDateIcuToPhp($format, 'date');
286
            }
287
        }
288 78
        return $this->parseDateValuePHP($value, $format);
289
    }
290
291
    /**
292
     * Parses a date value using the IntlDateFormatter::parse()
293
     * @param string $value string representing date
294
     * @param string $format the expected date format
295
     * @return integer|boolean a UNIX timestamp or `false` on failure.
296
     */
297 69
    private function parseDateValueIntl($value, $format)
298
    {
299 69
        if (isset($this->_dateFormats[$format])) {
300 3
            $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, 'UTC');
301 3
        } else {
302
            // if no time was provided in the format string set time to 0 to get a simple date timestamp
303 69
            $hasTimeInfo = (strpbrk($format, 'ahHkKmsSA') !== false);
304 69
            $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $hasTimeInfo ? $this->timeZone : 'UTC', null, $format);
305
        }
306
        // enable strict parsing to avoid getting invalid date values
307 69
        $formatter->setLenient(false);
308
309
        // There should not be a warning thrown by parse() but this seems to be the case on windows so we suppress it here
310
        // See https://github.com/yiisoft/yii2/issues/5962 and https://bugs.php.net/bug.php?id=68528
311 69
        $parsePos = 0;
312 69
        $parsedDate = @$formatter->parse($value, $parsePos);
313 69
        if ($parsedDate === false || $parsePos !== mb_strlen($value, Yii::$app ? Yii::$app->charset : 'UTF-8')) {
314 6
            return false;
315
        }
316
317 69
        return $parsedDate;
318
    }
319
320
    /**
321
     * Parses a date value using the DateTime::createFromFormat()
322
     * @param string $value string representing date
323
     * @param string $format the expected date format
324
     * @return integer|boolean a UNIX timestamp or `false` on failure.
325
     */
326 78
    private function parseDateValuePHP($value, $format)
327
    {
328
        // if no time was provided in the format string set time to 0 to get a simple date timestamp
329 78
        $hasTimeInfo = (strpbrk($format, 'HhGgis') !== false);
330
331 78
        $date = DateTime::createFromFormat($format, $value, new \DateTimeZone($hasTimeInfo ? $this->timeZone : 'UTC'));
332 78
        $errors = DateTime::getLastErrors();
333 78
        if ($date === false || $errors['error_count'] || $errors['warning_count']) {
334 16
            return false;
335
        }
336
337 78
        if (!$hasTimeInfo) {
338 72
            $date->setTime(0, 0, 0);
339 72
        }
340 78
        return $date->getTimestamp();
341
    }
342
343
    /**
344
     * Formats a timestamp using the specified format
345
     * @param integer $timestamp
346
     * @param string $format
347
     * @return string
348
     */
349 102
    private function formatTimestamp($timestamp, $format)
350
    {
351 102
        if (strncmp($format, 'php:', 4) === 0) {
352 63
            $format = substr($format, 4);
353 63
        } else {
354 48
            $format = FormatConverter::convertDateIcuToPhp($format, 'date');
355
        }
356
357 102
        $date = new DateTime();
358 102
        $date->setTimestamp($timestamp);
359 102
        $date->setTimezone(new \DateTimeZone($this->timestampAttributeTimeZone));
360 102
        return $date->format($format);
361 15
    }
362
}
363