Completed
Push — authenticator-refactor ( fcc98b...4aec3f )
by Sam
05:51
created

DateField::frontendToInternal()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 9

Duplication

Lines 13
Ratio 100 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 4
nop 1
dl 13
loc 13
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use IntlDateFormatter;
6
use SilverStripe\i18n\i18n;
7
use InvalidArgumentException;
8
use SilverStripe\ORM\FieldType\DBDate;
9
use SilverStripe\ORM\FieldType\DBDatetime;
10
use SilverStripe\ORM\ValidationResult;
11
12
/**
13
 * Form used for editing a date stirng
14
 *
15
 * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
16
 * since the required frontend dependencies are included through CMS bundling.
17
 *
18
 * # Localization
19
 *
20
 * Date formatting can be controlled in the below order of priority:
21
 *  - Format set via setDateFormat()
22
 *  - Format generated from current locale set by setLocale() and setDateLength()
23
 *  - Format generated from current locale in i18n
24
 *
25
 * You can also specify a setClientLocale() to set the javascript to a specific locale
26
 * on the frontend. However, this will not override the date format string.
27
 *
28
 * See http://doc.silverstripe.org/framework/en/topics/i18n for more information about localizing form fields.
29
 *
30
 * # Usage
31
 *
32
 * ## Example: Field localised with german date format
33
 *
34
 *   $f = new DateField('MyDate');
35
 *   $f->setLocale('de_DE');
36
 *
37
 * # Validation
38
 *
39
 * Caution: JavaScript validation is only supported for the 'en_NZ' locale at the moment,
40
 * it will be disabled automatically for all other locales.
41
 *
42
 * # Formats
43
 *
44
 * All format strings should follow the CLDR standard as per
45
 * http://userguide.icu-project.org/formatparse/datetime. These will be converted
46
 * automatically to jquery UI format.
47
 *
48
 * The value of this field in PHP will be ISO 8601 standard (e.g. 2004-02-12), and
49
 * stores this as a timestamp internally.
50
 *
51
 * Note: Do NOT use php date format strings. Date format strings follow the date
52
 * field symbol table as below.
53
 *
54
 * @see http://userguide.icu-project.org/formatparse/datetime
55
 * @see http://api.jqueryui.com/datepicker/#utility-formatDate
56
 */
57
class DateField extends TextField
58
{
59
    protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_DATE;
60
61
    /**
62
     * Override locale. If empty will default to current locale
63
     *
64
     * @var string
65
     */
66
    protected $locale = null;
67
68
    /**
69
     * Override date format. If empty will default to that used by the current locale.
70
     *
71
     * @var null
72
     */
73
    protected $dateFormat = null;
74
75
    /**
76
     * Set if js calendar should popup
77
     *
78
     * @var bool
79
     */
80
    protected $showCalendar = false;
81
82
    /**
83
     * Length of this date (full, short, etc).
84
     *
85
     * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
86
     * @var int
87
     */
88
    protected $dateLength = null;
89
90
    /**
91
     * Override locale for client side.
92
     *
93
     * @var string
94
     */
95
    protected $clientLocale = null;
96
97
    /**
98
     * Min date
99
     *
100
     * @var string ISO 8601 date for min date
101
     */
102
    protected $minDate = null;
103
104
    /**
105
     * Max date
106
     *
107
     * @var string ISO 860 date for max date
108
     */
109
    protected $maxDate = null;
110
111
    /**
112
     * Unparsed value, used exclusively for comparing with internal value
113
     * to detect invalid values.
114
     *
115
     * @var mixed
116
     */
117
    protected $rawValue = null;
118
119
    /**
120
     * Use HTML5-based input fields (and force ISO 8601 date formats).
121
     *
122
     * @var bool
123
     */
124
    protected $html5 = true;
125
126
    /**
127
     * @return bool
128
     */
129
    public function getHTML5()
130
    {
131
        return $this->html5;
132
    }
133
134
    /**
135
     * @param boolean $bool
136
     * @return $this
137
     */
138
    public function setHTML5($bool)
139
    {
140
        $this->html5 = $bool;
141
        return $this;
142
    }
143
144
    /**
145
     * Get length of the date format to use. One of:
146
     *
147
     *  - IntlDateFormatter::SHORT
148
     *  - IntlDateFormatter::MEDIUM
149
     *  - IntlDateFormatter::LONG
150
     *  - IntlDateFormatter::FULL
151
     *
152
     * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
153
     * @return int
154
     */
155
    public function getDateLength()
156
    {
157
        if ($this->dateLength) {
158
            return $this->dateLength;
159
        }
160
        return IntlDateFormatter::MEDIUM;
161
    }
162
163
    /**
164
     * Get length of the date format to use.
165
     * Only applicable with {@link setHTML5(false)}.
166
     *
167
     * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
168
     *
169
     * @param int $length
170
     * @return $this
171
     */
172
    public function setDateLength($length)
173
    {
174
        $this->dateLength = $length;
175
        return $this;
176
    }
177
178
    /**
179
     * Get date format in CLDR standard format
180
     *
181
     * This can be set explicitly. If not, this will be generated from the current locale
182
     * with the current date length.
183
     *
184
     * @see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Field-Symbol-Table
185
     */
186
    public function getDateFormat()
187
    {
188
        if ($this->getHTML5()) {
189
            // Browsers expect ISO 8601 dates, localisation is handled on the client
190
            $this->setDateFormat(DBDate::ISO_DATE);
191
        }
192
193
        if ($this->dateFormat) {
194
            return $this->dateFormat;
195
        }
196
197
        // Get from locale
198
        return $this->getFrontendFormatter()->getPattern();
199
    }
200
201
    /**
202
     * Set date format in CLDR standard format.
203
     * Only applicable with {@link setHTML5(false)}.
204
     *
205
     * @see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Field-Symbol-Table
206
     * @param string $format
207
     * @return $this
208
     */
209
    public function setDateFormat($format)
210
    {
211
        $this->dateFormat = $format;
0 ignored issues
show
Documentation Bug introduced by
It seems like $format of type string is incompatible with the declared type null of property $dateFormat.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
212
        return $this;
213
    }
214
215
    /**
216
     * Get date formatter with the standard locale / date format
217
     *
218
     * @throws \LogicException
219
     * @return IntlDateFormatter
220
     */
221 View Code Duplication
    protected function getFrontendFormatter()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
222
    {
223
        if ($this->getHTML5() && $this->dateFormat && $this->dateFormat !== DBDate::ISO_DATE) {
224
            throw new \LogicException(
225
                'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setDateFormat()'
226
            );
227
        }
228
229
        if ($this->getHTML5() && $this->dateLength) {
230
            throw new \LogicException(
231
                'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setDateLength()'
232
            );
233
        }
234
235
        if ($this->getHTML5() && $this->locale) {
236
            throw new \LogicException(
237
                'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setLocale()'
238
            );
239
        }
240
241
        $formatter = IntlDateFormatter::create(
242
            $this->getLocale(),
243
            $this->getDateLength(),
244
            IntlDateFormatter::NONE
245
        );
246
247
        if ($this->getHTML5()) {
248
            // Browsers expect ISO 8601 dates, localisation is handled on the client
249
            $formatter->setPattern(DBDate::ISO_DATE);
250
        } elseif ($this->dateFormat) {
251
            // Don't invoke getDateFormat() directly to avoid infinite loop
252
            $ok = $formatter->setPattern($this->dateFormat);
253
            if (!$ok) {
254
                throw new InvalidArgumentException("Invalid date format {$this->dateFormat}");
255
            }
256
        }
257
        return $formatter;
258
    }
259
260
    /**
261
     * Get a date formatter for the ISO 8601 format
262
     *
263
     * @return IntlDateFormatter
264
     */
265 View Code Duplication
    protected function getInternalFormatter()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
266
    {
267
        $locale = i18n::config()->uninherited('default_locale');
0 ignored issues
show
Unused Code introduced by
$locale is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
268
        $formatter = IntlDateFormatter::create(
269
            i18n::config()->uninherited('default_locale'),
270
            IntlDateFormatter::MEDIUM,
271
            IntlDateFormatter::NONE
272
        );
273
        $formatter->setLenient(false);
274
        // CLDR ISO 8601 date.
275
        $formatter->setPattern(DBDate::ISO_DATE);
276
        return $formatter;
277
    }
278
279 View Code Duplication
    public function getAttributes()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
280
    {
281
        $attributes = parent::getAttributes();
282
283
        $attributes['lang'] = i18n::convert_rfc1766($this->getLocale());
284
285
        if ($this->getHTML5()) {
286
            $attributes['type'] = 'date';
287
            $attributes['min'] = $this->getMinDate();
288
            $attributes['max'] = $this->getMaxDate();
289
        }
290
291
        return $attributes;
292
    }
293
294 View Code Duplication
    public function getSchemaDataDefaults()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
295
    {
296
        $defaults = parent::getSchemaDataDefaults();
297
        return array_merge($defaults, [
298
            'lang' => i18n::convert_rfc1766($this->getLocale()),
299
            'data' => array_merge($defaults['data'], [
300
                'html5' => $this->getHTML5(),
301
                'min' => $this->getMinDate(),
302
                'max' => $this->getMaxDate()
303
            ])
304
        ]);
305
    }
306
307
    public function Type()
308
    {
309
        return 'date text';
310
    }
311
312
    /**
313
     * Assign value posted from form submission
314
     *
315
     * @param mixed $value
316
     * @param mixed $data
317
     * @return $this
318
     */
319 View Code Duplication
    public function setSubmittedValue($value, $data = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
320
    {
321
        // Save raw value for later validation
322
        $this->rawValue = $value;
323
324
        // Null case
325
        if (!$value) {
326
            $this->value = null;
327
            return $this;
328
        }
329
330
        // Parse from submitted value
331
        $this->value = $this->frontendToInternal($value);
332
        return $this;
333
    }
334
335
    /**
336
     * Assign value based on {@link $datetimeFormat}, which might be localised.
337
     *
338
     * When $html5=true, assign value from ISO 8601 string.
339
     *
340
     * @param mixed $value
341
     * @param mixed $data
342
     * @return $this
343
     */
344 View Code Duplication
    public function setValue($value, $data = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
345
    {
346
        // Save raw value for later validation
347
        $this->rawValue = $value;
348
349
        // Null case
350
        if (!$value) {
351
            $this->value = null;
352
            return $this;
353
        }
354
355
        // Re-run through formatter to tidy up (e.g. remove time component)
356
        $this->value = $this->tidyInternal($value);
357
        return $this;
358
    }
359
360
    public function Value()
361
    {
362
        return $this->internalToFrontend($this->value);
363
    }
364
365
    public function performReadonlyTransformation()
366
    {
367
        $field = $this->castedCopy(DateField_Disabled::class);
368
        $field->setValue($this->dataValue());
369
        $field->setReadonly(true);
370
        return $field;
371
    }
372
373
    /**
374
     * @param Validator $validator
375
     * @return bool
376
     */
377 View Code Duplication
    public function validate($validator)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
378
    {
379
        // Don't validate empty fields
380
        if (empty($this->rawValue)) {
381
            return true;
382
        }
383
384
        // We submitted a value, but it couldn't be parsed
385
        if (empty($this->value)) {
386
            $validator->validationError(
387
                $this->name,
388
                _t(
389
                    'SilverStripe\\Forms\\DateField.VALIDDATEFORMAT2',
390
                    "Please enter a valid date format ({format})",
391
                    ['format' => $this->getDateFormat()]
392
                )
393
            );
394
            return false;
395
        }
396
397
        // Check min date
398
        $min = $this->getMinDate();
399
        if ($min) {
400
            $oops = strtotime($this->value) < strtotime($min);
401
            if ($oops) {
402
                $validator->validationError(
403
                    $this->name,
404
                    _t(
405
                        'SilverStripe\\Forms\\DateField.VALIDDATEMINDATE',
406
                        "Your date has to be newer or matching the minimum allowed date ({date})",
407
                        [
408
                            'date' => sprintf(
409
                                '<time datetime="%s">%s</time>',
410
                                $min,
411
                                $this->internalToFrontend($min)
412
                            )
413
                        ]
414
                    ),
415
                    ValidationResult::TYPE_ERROR,
416
                    ValidationResult::CAST_HTML
417
                );
418
                return false;
419
            }
420
        }
421
422
        // Check max date
423
        $max = $this->getMaxDate();
424
        if ($max) {
425
            $oops = strtotime($this->value) > strtotime($max);
426
            if ($oops) {
427
                $validator->validationError(
428
                    $this->name,
429
                    _t(
430
                        'SilverStripe\\Forms\\DateField.VALIDDATEMAXDATE',
431
                        "Your date has to be older or matching the maximum allowed date ({date})",
432
                        [
433
                            'date' => sprintf(
434
                                '<time datetime="%s">%s</time>',
435
                                $max,
436
                                $this->internalToFrontend($max)
437
                            )
438
                        ]
439
                    ),
440
                    ValidationResult::TYPE_ERROR,
441
                    ValidationResult::CAST_HTML
442
                );
443
                return false;
444
            }
445
        }
446
447
        return true;
448
    }
449
450
    /**
451
     * Get locale to use for this field
452
     *
453
     * @return string
454
     */
455
    public function getLocale()
456
    {
457
        return $this->locale ?: i18n::get_locale();
458
    }
459
460
    /**
461
     * Determines the presented/processed format based on locale defaults,
462
     * instead of explicitly setting {@link setDateFormat()}.
463
     * Only applicable with {@link setHTML5(false)}.
464
     *
465
     * @param string $locale
466
     * @return $this
467
     */
468
    public function setLocale($locale)
469
    {
470
        $this->locale = $locale;
471
        return $this;
472
    }
473
474
    public function getSchemaValidation()
475
    {
476
        $rules = parent::getSchemaValidation();
477
        $rules['date'] = true;
478
        return $rules;
479
    }
480
481
    /**
482
     * @return string
483
     */
484
    public function getMinDate()
485
    {
486
        return $this->minDate;
487
    }
488
489
    /**
490
     * @param string $minDate
491
     * @return $this
492
     */
493
    public function setMinDate($minDate)
494
    {
495
        $this->minDate = $this->tidyInternal($minDate);
496
        return $this;
497
    }
498
499
    /**
500
     * @return string
501
     */
502
    public function getMaxDate()
503
    {
504
        return $this->maxDate;
505
    }
506
507
    /**
508
     * @param string $maxDate
509
     * @return $this
510
     */
511
    public function setMaxDate($maxDate)
512
    {
513
        $this->maxDate = $this->tidyInternal($maxDate);
514
        return $this;
515
    }
516
517
    /**
518
     * Convert frontend date to the internal representation (ISO 8601).
519
     * The frontend date is also in ISO 8601 when $html5=true.
520
     *
521
     * @param string $date
522
     * @return string The formatted date, or null if not a valid date
523
     */
524 View Code Duplication
    protected function frontendToInternal($date)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
525
    {
526
        if (!$date) {
527
            return null;
528
        }
529
        $fromFormatter = $this->getFrontendFormatter();
530
        $toFormatter = $this->getInternalFormatter();
531
        $timestamp = $fromFormatter->parse($date);
532
        if ($timestamp === false) {
533
            return null;
534
        }
535
        return $toFormatter->format($timestamp) ?: null;
536
    }
537
538
    /**
539
     * Convert the internal date representation (ISO 8601) to a format used by the frontend,
540
     * as defined by {@link $dateFormat}. With $html5=true, the frontend date will also be
541
     * in ISO 8601.
542
     *
543
     * @param string $date
544
     * @return string The formatted date, or null if not a valid date
545
     */
546 View Code Duplication
    protected function internalToFrontend($date)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
547
    {
548
        $date = $this->tidyInternal($date);
549
        if (!$date) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $date of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
550
            return null;
551
        }
552
        $fromFormatter = $this->getInternalFormatter();
553
        $toFormatter = $this->getFrontendFormatter();
554
        $timestamp = $fromFormatter->parse($date);
555
        if ($timestamp === false) {
556
            return null;
557
        }
558
        return $toFormatter->format($timestamp) ?: null;
559
    }
560
561
    /**
562
     * Tidy up the internal date representation (ISO 8601),
563
     * and fall back to strtotime() if there's parsing errors.
564
     *
565
     * @param string $date Date in ISO 8601 or approximate form
566
     * @return string ISO 8601 date, or null if not valid
567
     */
568 View Code Duplication
    protected function tidyInternal($date)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
569
    {
570
        if (!$date) {
571
            return null;
572
        }
573
        // Re-run through formatter to tidy up (e.g. remove time component)
574
        $formatter = $this->getInternalFormatter();
575
        $timestamp = $formatter->parse($date);
576
        if ($timestamp === false) {
577
            // Fallback to strtotime
578
            $timestamp = strtotime($date, DBDatetime::now()->getTimestamp());
579
            if ($timestamp === false) {
580
                return null;
581
            }
582
        }
583
        return $formatter->format($timestamp);
584
    }
585
}
586