Completed
Push — 4.0 ( b59aea...80f83b )
by Loz
52s queued 21s
created

DateField::getFrontendFormatter()   B

Complexity

Conditions 11
Paths 7

Size

Total Lines 39
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 20
nc 7
nop 0
dl 0
loc 39
rs 7.3166
c 0
b 0
f 0

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
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
        if ($this->getHTML5()) {
177
            // Browsers expect ISO 8601 dates, localisation is handled on the client
178
            $this->setDateFormat(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
224
225
        if ($this->getHTML5() && $this->locale) {
226
            throw new \LogicException(
227
                'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setLocale()'
228
            );
229
        }
230
231
        $formatter = IntlDateFormatter::create(
232
            $this->getLocale(),
233
            $this->getDateLength(),
234
            IntlDateFormatter::NONE
235
        );
236
237
        if ($this->getHTML5()) {
238
            // Browsers expect ISO 8601 dates, localisation is handled on the client
239
            $formatter->setPattern(DBDate::ISO_DATE);
240
        } elseif ($this->dateFormat) {
241
            // Don't invoke getDateFormat() directly to avoid infinite loop
242
            $ok = $formatter->setPattern($this->dateFormat);
243
            if (!$ok) {
244
                throw new InvalidArgumentException("Invalid date format {$this->dateFormat}");
245
            }
246
        }
247
        return $formatter;
248
    }
249
250
    /**
251
     * Get a date formatter for the ISO 8601 format
252
     *
253
     * @return IntlDateFormatter
254
     */
255
    protected function getInternalFormatter()
256
    {
257
        $locale = i18n::config()->uninherited('default_locale');
0 ignored issues
show
Unused Code introduced by
The assignment to $locale is dead and can be removed.
Loading history...
258
        $formatter = IntlDateFormatter::create(
259
            i18n::config()->uninherited('default_locale'),
260
            IntlDateFormatter::MEDIUM,
261
            IntlDateFormatter::NONE
262
        );
263
        $formatter->setLenient(false);
264
        // CLDR ISO 8601 date.
265
        $formatter->setPattern(DBDate::ISO_DATE);
266
        return $formatter;
267
    }
268
269
    public function getAttributes()
270
    {
271
        $attributes = parent::getAttributes();
272
273
        $attributes['lang'] = i18n::convert_rfc1766($this->getLocale());
274
275
        if ($this->getHTML5()) {
276
            $attributes['min'] = $this->getMinDate();
277
            $attributes['max'] = $this->getMaxDate();
278
        } else {
279
            $attributes['type'] = 'text';
280
        }
281
282
        return $attributes;
283
    }
284
285
    public function getSchemaDataDefaults()
286
    {
287
        $defaults = parent::getSchemaDataDefaults();
288
        return array_merge($defaults, [
289
            'lang' => i18n::convert_rfc1766($this->getLocale()),
290
            'data' => array_merge($defaults['data'], [
291
                'html5' => $this->getHTML5(),
292
                'min' => $this->getMinDate(),
293
                'max' => $this->getMaxDate()
294
            ])
295
        ]);
296
    }
297
298
    public function Type()
299
    {
300
        return 'date text';
301
    }
302
303
    /**
304
     * Assign value posted from form submission
305
     *
306
     * @param mixed $value
307
     * @param mixed $data
308
     * @return $this
309
     */
310
    public function setSubmittedValue($value, $data = null)
311
    {
312
        // Save raw value for later validation
313
        $this->rawValue = $value;
314
315
        // Null case
316
        if (!$value) {
317
            $this->value = null;
318
            return $this;
319
        }
320
321
        // Parse from submitted value
322
        $this->value = $this->frontendToInternal($value);
323
        return $this;
324
    }
325
326
    /**
327
     * Assign value based on {@link $datetimeFormat}, which might be localised.
328
     *
329
     * When $html5=true, assign value from ISO 8601 string.
330
     *
331
     * @param mixed $value
332
     * @param mixed $data
333
     * @return $this
334
     */
335
    public function setValue($value, $data = null)
336
    {
337
        // Save raw value for later validation
338
        $this->rawValue = $value;
339
340
        // Null case
341
        if (!$value) {
342
            $this->value = null;
343
            return $this;
344
        }
345
346
        // Re-run through formatter to tidy up (e.g. remove time component)
347
        $this->value = $this->tidyInternal($value);
348
        return $this;
349
    }
350
351
    public function Value()
352
    {
353
        return $this->internalToFrontend($this->value);
354
    }
355
356
    public function performReadonlyTransformation()
357
    {
358
        $field = $this
359
            ->castedCopy(DateField_Disabled::class)
360
            ->setValue($this->dataValue())
361
            ->setLocale($this->getLocale())
362
            ->setReadonly(true);
363
364
        return $field;
365
    }
366
367
    /**
368
     * @param Validator $validator
369
     * @return bool
370
     */
371
    public function validate($validator)
372
    {
373
        // Don't validate empty fields
374
        if (empty($this->rawValue)) {
375
            return true;
376
        }
377
378
        // We submitted a value, but it couldn't be parsed
379
        if (empty($this->value)) {
380
            $validator->validationError(
381
                $this->name,
382
                _t(
383
                    'SilverStripe\\Forms\\DateField.VALIDDATEFORMAT2',
384
                    "Please enter a valid date format ({format})",
385
                    ['format' => $this->getDateFormat()]
386
                )
387
            );
388
            return false;
389
        }
390
391
        // Check min date
392
        $min = $this->getMinDate();
393
        if ($min) {
394
            $oops = strtotime($this->value) < strtotime($min);
395
            if ($oops) {
396
                $validator->validationError(
397
                    $this->name,
398
                    _t(
399
                        'SilverStripe\\Forms\\DateField.VALIDDATEMINDATE',
400
                        "Your date has to be newer or matching the minimum allowed date ({date})",
401
                        [
402
                            'date' => sprintf(
403
                                '<time datetime="%s">%s</time>',
404
                                $min,
405
                                $this->internalToFrontend($min)
406
                            )
407
                        ]
408
                    ),
409
                    ValidationResult::TYPE_ERROR,
410
                    ValidationResult::CAST_HTML
411
                );
412
                return false;
413
            }
414
        }
415
416
        // Check max date
417
        $max = $this->getMaxDate();
418
        if ($max) {
419
            $oops = strtotime($this->value) > strtotime($max);
420
            if ($oops) {
421
                $validator->validationError(
422
                    $this->name,
423
                    _t(
424
                        'SilverStripe\\Forms\\DateField.VALIDDATEMAXDATE',
425
                        "Your date has to be older or matching the maximum allowed date ({date})",
426
                        [
427
                            'date' => sprintf(
428
                                '<time datetime="%s">%s</time>',
429
                                $max,
430
                                $this->internalToFrontend($max)
431
                            )
432
                        ]
433
                    ),
434
                    ValidationResult::TYPE_ERROR,
435
                    ValidationResult::CAST_HTML
436
                );
437
                return false;
438
            }
439
        }
440
441
        return true;
442
    }
443
444
    /**
445
     * Get locale to use for this field
446
     *
447
     * @return string
448
     */
449
    public function getLocale()
450
    {
451
        return $this->locale ?: i18n::get_locale();
452
    }
453
454
    /**
455
     * Determines the presented/processed format based on locale defaults,
456
     * instead of explicitly setting {@link setDateFormat()}.
457
     * Only applicable with {@link setHTML5(false)}.
458
     *
459
     * @param string $locale
460
     * @return $this
461
     */
462
    public function setLocale($locale)
463
    {
464
        $this->locale = $locale;
465
        return $this;
466
    }
467
468
    public function getSchemaValidation()
469
    {
470
        $rules = parent::getSchemaValidation();
471
        $rules['date'] = true;
472
        return $rules;
473
    }
474
475
    /**
476
     * @return string
477
     */
478
    public function getMinDate()
479
    {
480
        return $this->minDate;
481
    }
482
483
    /**
484
     * @param string $minDate
485
     * @return $this
486
     */
487
    public function setMinDate($minDate)
488
    {
489
        $this->minDate = $this->tidyInternal($minDate);
490
        return $this;
491
    }
492
493
    /**
494
     * @return string
495
     */
496
    public function getMaxDate()
497
    {
498
        return $this->maxDate;
499
    }
500
501
    /**
502
     * @param string $maxDate
503
     * @return $this
504
     */
505
    public function setMaxDate($maxDate)
506
    {
507
        $this->maxDate = $this->tidyInternal($maxDate);
508
        return $this;
509
    }
510
511
    /**
512
     * Convert frontend date to the internal representation (ISO 8601).
513
     * The frontend date is also in ISO 8601 when $html5=true.
514
     *
515
     * @param string $date
516
     * @return string The formatted date, or null if not a valid date
517
     */
518
    protected function frontendToInternal($date)
519
    {
520
        if (!$date) {
521
            return null;
522
        }
523
        $fromFormatter = $this->getFrontendFormatter();
524
        $toFormatter = $this->getInternalFormatter();
525
        $timestamp = $fromFormatter->parse($date);
526
        if ($timestamp === false) {
527
            return null;
528
        }
529
        return $toFormatter->format($timestamp) ?: null;
530
    }
531
532
    /**
533
     * Convert the internal date representation (ISO 8601) to a format used by the frontend,
534
     * as defined by {@link $dateFormat}. With $html5=true, the frontend date will also be
535
     * in ISO 8601.
536
     *
537
     * @param string $date
538
     * @return string The formatted date, or null if not a valid date
539
     */
540
    protected function internalToFrontend($date)
541
    {
542
        $date = $this->tidyInternal($date);
543
        if (!$date) {
544
            return null;
545
        }
546
        $fromFormatter = $this->getInternalFormatter();
547
        $toFormatter = $this->getFrontendFormatter();
548
        $timestamp = $fromFormatter->parse($date);
549
        if ($timestamp === false) {
550
            return null;
551
        }
552
        return $toFormatter->format($timestamp) ?: null;
553
    }
554
555
    /**
556
     * Tidy up the internal date representation (ISO 8601),
557
     * and fall back to strtotime() if there's parsing errors.
558
     *
559
     * @param string $date Date in ISO 8601 or approximate form
560
     * @return string ISO 8601 date, or null if not valid
561
     */
562
    protected function tidyInternal($date)
563
    {
564
        if (!$date) {
565
            return null;
566
        }
567
        // Re-run through formatter to tidy up (e.g. remove time component)
568
        $formatter = $this->getInternalFormatter();
569
        $timestamp = $formatter->parse($date);
570
        if ($timestamp === false) {
571
            // Fallback to strtotime
572
            $timestamp = strtotime($date, DBDatetime::now()->getTimestamp());
573
            if ($timestamp === false) {
574
                return null;
575
            }
576
        }
577
        return $formatter->format($timestamp);
578
    }
579
}
580