Passed
Pull Request — 4 (#8209)
by Ingo
09:07
created

DateField   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 522
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 522
rs 3.6
c 0
b 0
f 0
wmc 60

26 Methods

Rating   Name   Duplication   Size   Complexity  
A getDateLength() 0 6 2
A setMinDate() 0 4 1
A getSchemaDataDefaults() 0 9 1
B validate() 0 71 7
A getHTML5() 0 3 1
A internalToFrontend() 0 13 4
A setHTML5() 0 4 1
A getInternalFormatter() 0 11 1
A getSchemaValidation() 0 5 1
A getLocale() 0 7 3
A performReadonlyTransformation() 0 9 1
C getFrontendFormatter() 0 37 12
A getDateFormat() 0 13 3
A setMaxDate() 0 4 1
A tidyInternal() 0 16 4
A setDateFormat() 0 4 1
A Value() 0 3 1
A setDateLength() 0 4 1
A frontendToInternal() 0 12 4
A getAttributes() 0 14 2
A setSubmittedValue() 0 14 2
A setLocale() 0 4 1
A setValue() 0 14 2
A getMaxDate() 0 3 1
A Type() 0 3 1
A getMinDate() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like DateField 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.

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 DateField, and based on these observations, apply Extract Interface, too.

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
     * Length of this date (full, short, etc).
77
     *
78
     * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
79
     * @var int
80
     */
81
    protected $dateLength = null;
82
83
    protected $inputType = 'date';
84
85
    /**
86
     * Min date
87
     *
88
     * @var string ISO 8601 date for min date
89
     */
90
    protected $minDate = null;
91
92
    /**
93
     * Max date
94
     *
95
     * @var string ISO 860 date for max date
96
     */
97
    protected $maxDate = null;
98
99
    /**
100
     * Unparsed value, used exclusively for comparing with internal value
101
     * to detect invalid values.
102
     *
103
     * @var mixed
104
     */
105
    protected $rawValue = null;
106
107
    /**
108
     * Use HTML5-based input fields (and force ISO 8601 date formats).
109
     *
110
     * @var bool
111
     */
112
    protected $html5 = true;
113
114
    /**
115
     * @return bool
116
     */
117
    public function getHTML5()
118
    {
119
        return $this->html5;
120
    }
121
122
    /**
123
     * @param boolean $bool
124
     * @return $this
125
     */
126
    public function setHTML5($bool)
127
    {
128
        $this->html5 = $bool;
129
        return $this;
130
    }
131
132
    /**
133
     * Get length of the date format to use. One of:
134
     *
135
     *  - IntlDateFormatter::SHORT
136
     *  - IntlDateFormatter::MEDIUM
137
     *  - IntlDateFormatter::LONG
138
     *  - IntlDateFormatter::FULL
139
     *
140
     * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
141
     * @return int
142
     */
143
    public function getDateLength()
144
    {
145
        if ($this->dateLength) {
146
            return $this->dateLength;
147
        }
148
        return IntlDateFormatter::MEDIUM;
149
    }
150
151
    /**
152
     * Get length of the date format to use.
153
     * Only applicable with {@link setHTML5(false)}.
154
     *
155
     * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
156
     *
157
     * @param int $length
158
     * @return $this
159
     */
160
    public function setDateLength($length)
161
    {
162
        $this->dateLength = $length;
163
        return $this;
164
    }
165
166
    /**
167
     * Get date format in CLDR standard format
168
     *
169
     * This can be set explicitly. If not, this will be generated from the current locale
170
     * with the current date length.
171
     *
172
     * @see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Field-Symbol-Table
173
     */
174
    public function getDateFormat()
175
    {
176
        // Browsers expect ISO 8601 dates, localisation is handled on the client
177
        if ($this->getHTML5()) {
178
            return DBDate::ISO_DATE;
179
        }
180
181
        if ($this->dateFormat) {
182
            return $this->dateFormat;
183
        }
184
185
        // Get from locale
186
        return $this->getFrontendFormatter()->getPattern();
187
    }
188
189
    /**
190
     * Set date format in CLDR standard format.
191
     * Only applicable with {@link setHTML5(false)}.
192
     *
193
     * @see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Field-Symbol-Table
194
     * @param string $format
195
     * @return $this
196
     */
197
    public function setDateFormat($format)
198
    {
199
        $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...
200
        return $this;
201
    }
202
203
    /**
204
     * Get date formatter with the standard locale / date format
205
     *
206
     * @throws \LogicException
207
     * @return IntlDateFormatter
208
     */
209
    protected function getFrontendFormatter()
210
    {
211
        if ($this->getHTML5() && $this->dateFormat && $this->dateFormat !== DBDate::ISO_DATE) {
212
            throw new \LogicException(
213
                'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setDateFormat()'
214
            );
215
        }
216
217
        if ($this->getHTML5() && $this->dateLength) {
218
            throw new \LogicException(
219
                'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setDateLength()'
220
            );
221
        }
222
223
        if ($this->getHTML5() && $this->locale && $this->locale !== DBDate::ISO_LOCALE) {
224
            throw new \LogicException(
225
                'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setLocale()'
226
            );
227
        }
228
229
        $formatter = IntlDateFormatter::create(
230
            $this->getLocale(),
231
            $this->getDateLength(),
232
            IntlDateFormatter::NONE
233
        );
234
235
        if ($this->getHTML5()) {
236
            // Browsers expect ISO 8601 dates, localisation is handled on the client
237
            $formatter->setPattern(DBDate::ISO_DATE);
238
        } elseif ($this->dateFormat) {
239
            // Don't invoke getDateFormat() directly to avoid infinite loop
240
            $ok = $formatter->setPattern($this->dateFormat);
241
            if (!$ok) {
242
                throw new InvalidArgumentException("Invalid date format {$this->dateFormat}");
243
            }
244
        }
245
        return $formatter;
246
    }
247
248
    /**
249
     * Get a date formatter for the ISO 8601 format
250
     *
251
     * @return IntlDateFormatter
252
     */
253
    protected function getInternalFormatter()
254
    {
255
        $formatter = IntlDateFormatter::create(
256
            DBDate::ISO_LOCALE,
257
            IntlDateFormatter::MEDIUM,
258
            IntlDateFormatter::NONE
259
        );
260
        $formatter->setLenient(false);
261
        // CLDR ISO 8601 date.
262
        $formatter->setPattern(DBDate::ISO_DATE);
263
        return $formatter;
264
    }
265
266
    public function getAttributes()
267
    {
268
        $attributes = parent::getAttributes();
269
270
        $attributes['lang'] = i18n::convert_rfc1766($this->getLocale());
271
272
        if ($this->getHTML5()) {
273
            $attributes['min'] = $this->getMinDate();
274
            $attributes['max'] = $this->getMaxDate();
275
        } else {
276
            $attributes['type'] = 'text';
277
        }
278
279
        return $attributes;
280
    }
281
282
    public function getSchemaDataDefaults()
283
    {
284
        $defaults = parent::getSchemaDataDefaults();
285
        return array_merge($defaults, [
286
            'lang' => i18n::convert_rfc1766($this->getLocale()),
287
            'data' => array_merge($defaults['data'], [
288
                'html5' => $this->getHTML5(),
289
                'min' => $this->getMinDate(),
290
                'max' => $this->getMaxDate()
291
            ])
292
        ]);
293
    }
294
295
    public function Type()
296
    {
297
        return 'date text';
298
    }
299
300
    /**
301
     * Assign value posted from form submission
302
     *
303
     * @param mixed $value
304
     * @param mixed $data
305
     * @return $this
306
     */
307
    public function setSubmittedValue($value, $data = null)
308
    {
309
        // Save raw value for later validation
310
        $this->rawValue = $value;
311
312
        // Null case
313
        if (!$value) {
314
            $this->value = null;
315
            return $this;
316
        }
317
318
        // Parse from submitted value
319
        $this->value = $this->frontendToInternal($value);
320
        return $this;
321
    }
322
323
    /**
324
     * Assign value based on {@link $datetimeFormat}, which might be localised.
325
     *
326
     * When $html5=true, assign value from ISO 8601 string.
327
     *
328
     * @param mixed $value
329
     * @param mixed $data
330
     * @return $this
331
     */
332
    public function setValue($value, $data = null)
333
    {
334
        // Save raw value for later validation
335
        $this->rawValue = $value;
336
337
        // Null case
338
        if (!$value) {
339
            $this->value = null;
340
            return $this;
341
        }
342
343
        // Re-run through formatter to tidy up (e.g. remove time component)
344
        $this->value = $this->tidyInternal($value);
345
        return $this;
346
    }
347
348
    public function Value()
349
    {
350
        return $this->internalToFrontend($this->value);
351
    }
352
353
    public function performReadonlyTransformation()
354
    {
355
        $field = $this
356
            ->castedCopy(DateField_Disabled::class)
357
            ->setValue($this->dataValue())
358
            ->setLocale($this->getLocale())
359
            ->setReadonly(true);
360
361
        return $field;
362
    }
363
364
    /**
365
     * @param Validator $validator
366
     * @return bool
367
     */
368
    public function validate($validator)
369
    {
370
        // Don't validate empty fields
371
        if (empty($this->rawValue)) {
372
            return true;
373
        }
374
375
        // We submitted a value, but it couldn't be parsed
376
        if (empty($this->value)) {
377
            $validator->validationError(
378
                $this->name,
379
                _t(
380
                    'SilverStripe\\Forms\\DateField.VALIDDATEFORMAT2',
381
                    "Please enter a valid date format ({format})",
382
                    ['format' => $this->getDateFormat()]
383
                )
384
            );
385
            return false;
386
        }
387
388
        // Check min date
389
        $min = $this->getMinDate();
390
        if ($min) {
391
            $oops = strtotime($this->value) < strtotime($min);
392
            if ($oops) {
393
                $validator->validationError(
394
                    $this->name,
395
                    _t(
396
                        'SilverStripe\\Forms\\DateField.VALIDDATEMINDATE',
397
                        "Your date has to be newer or matching the minimum allowed date ({date})",
398
                        [
399
                            'date' => sprintf(
400
                                '<time datetime="%s">%s</time>',
401
                                $min,
402
                                $this->internalToFrontend($min)
403
                            )
404
                        ]
405
                    ),
406
                    ValidationResult::TYPE_ERROR,
407
                    ValidationResult::CAST_HTML
408
                );
409
                return false;
410
            }
411
        }
412
413
        // Check max date
414
        $max = $this->getMaxDate();
415
        if ($max) {
416
            $oops = strtotime($this->value) > strtotime($max);
417
            if ($oops) {
418
                $validator->validationError(
419
                    $this->name,
420
                    _t(
421
                        'SilverStripe\\Forms\\DateField.VALIDDATEMAXDATE',
422
                        "Your date has to be older or matching the maximum allowed date ({date})",
423
                        [
424
                            'date' => sprintf(
425
                                '<time datetime="%s">%s</time>',
426
                                $max,
427
                                $this->internalToFrontend($max)
428
                            )
429
                        ]
430
                    ),
431
                    ValidationResult::TYPE_ERROR,
432
                    ValidationResult::CAST_HTML
433
                );
434
                return false;
435
            }
436
        }
437
438
        return true;
439
    }
440
441
    /**
442
     * Get locale to use for this field
443
     *
444
     * @return string
445
     */
446
    public function getLocale()
447
    {
448
        // Use iso locale for html5
449
        if ($this->getHTML5()) {
450
            return DBDate::ISO_LOCALE;
451
        }
452
        return $this->locale ?: i18n::get_locale();
453
    }
454
455
    /**
456
     * Determines the presented/processed format based on locale defaults,
457
     * instead of explicitly setting {@link setDateFormat()}.
458
     * Only applicable with {@link setHTML5(false)}.
459
     *
460
     * @param string $locale
461
     * @return $this
462
     */
463
    public function setLocale($locale)
464
    {
465
        $this->locale = $locale;
466
        return $this;
467
    }
468
469
    public function getSchemaValidation()
470
    {
471
        $rules = parent::getSchemaValidation();
472
        $rules['date'] = true;
473
        return $rules;
474
    }
475
476
    /**
477
     * @return string
478
     */
479
    public function getMinDate()
480
    {
481
        return $this->minDate;
482
    }
483
484
    /**
485
     * @param string $minDate
486
     * @return $this
487
     */
488
    public function setMinDate($minDate)
489
    {
490
        $this->minDate = $this->tidyInternal($minDate);
491
        return $this;
492
    }
493
494
    /**
495
     * @return string
496
     */
497
    public function getMaxDate()
498
    {
499
        return $this->maxDate;
500
    }
501
502
    /**
503
     * @param string $maxDate
504
     * @return $this
505
     */
506
    public function setMaxDate($maxDate)
507
    {
508
        $this->maxDate = $this->tidyInternal($maxDate);
509
        return $this;
510
    }
511
512
    /**
513
     * Convert frontend date to the internal representation (ISO 8601).
514
     * The frontend date is also in ISO 8601 when $html5=true.
515
     *
516
     * @param string $date
517
     * @return string The formatted date, or null if not a valid date
518
     */
519
    protected function frontendToInternal($date)
520
    {
521
        if (!$date) {
522
            return null;
523
        }
524
        $fromFormatter = $this->getFrontendFormatter();
525
        $toFormatter = $this->getInternalFormatter();
526
        $timestamp = $fromFormatter->parse($date);
527
        if ($timestamp === false) {
528
            return null;
529
        }
530
        return $toFormatter->format($timestamp) ?: null;
531
    }
532
533
    /**
534
     * Convert the internal date representation (ISO 8601) to a format used by the frontend,
535
     * as defined by {@link $dateFormat}. With $html5=true, the frontend date will also be
536
     * in ISO 8601.
537
     *
538
     * @param string $date
539
     * @return string The formatted date, or null if not a valid date
540
     */
541
    protected function internalToFrontend($date)
542
    {
543
        $date = $this->tidyInternal($date);
544
        if (!$date) {
545
            return null;
546
        }
547
        $fromFormatter = $this->getInternalFormatter();
548
        $toFormatter = $this->getFrontendFormatter();
549
        $timestamp = $fromFormatter->parse($date);
550
        if ($timestamp === false) {
551
            return null;
552
        }
553
        return $toFormatter->format($timestamp) ?: null;
554
    }
555
556
    /**
557
     * Tidy up the internal date representation (ISO 8601),
558
     * and fall back to strtotime() if there's parsing errors.
559
     *
560
     * @param string $date Date in ISO 8601 or approximate form
561
     * @return string ISO 8601 date, or null if not valid
562
     */
563
    protected function tidyInternal($date)
564
    {
565
        if (!$date) {
566
            return null;
567
        }
568
        // Re-run through formatter to tidy up (e.g. remove time component)
569
        $formatter = $this->getInternalFormatter();
570
        $timestamp = $formatter->parse($date);
571
        if ($timestamp === false) {
572
            // Fallback to strtotime
573
            $timestamp = strtotime($date, DBDatetime::now()->getTimestamp());
574
            if ($timestamp === false) {
575
                return null;
576
            }
577
        }
578
        return $formatter->format($timestamp);
579
    }
580
}
581