Completed
Pull Request — master (#6766)
by Ingo
08:55
created

DateField::setHTML5()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
rs 9.4285
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
11
/**
12
 * Form used for editing a date stirng
13
 *
14
 * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
15
 * since the required frontend dependencies are included through CMS bundling.
16
 *
17
 * # Localization
18
 *
19
 * Date formatting can be controlled in the below order of priority:
20
 *  - Format set via setDateFormat()
21
 *  - Format generated from current locale set by setLocale() and setDateLength()
22
 *  - Format generated from current locale in i18n
23
 *
24
 * You can also specify a setClientLocale() to set the javascript to a specific locale
25
 * on the frontend. However, this will not override the date format string.
26
 *
27
 * See http://doc.silverstripe.org/framework/en/topics/i18n for more information about localizing form fields.
28
 *
29
 * # Usage
30
 *
31
 * ## Example: Field localised with german date format
32
 *
33
 *   $f = new DateField('MyDate');
34
 *   $f->setLocale('de_DE');
35
 *
36
 * # Validation
37
 *
38
 * Caution: JavaScript validation is only supported for the 'en_NZ' locale at the moment,
39
 * it will be disabled automatically for all other locales.
40
 *
41
 * # Formats
42
 *
43
 * All format strings should follow the CLDR standard as per
44
 * http://userguide.icu-project.org/formatparse/datetime. These will be converted
45
 * automatically to jquery UI format.
46
 *
47
 * The value of this field in PHP will be ISO 8601 standard (e.g. 2004-02-12), and
48
 * stores this as a timestamp internally.
49
 *
50
 * Note: Do NOT use php date format strings. Date format strings follow the date
51
 * field symbol table as below.
52
 *
53
 * @see http://userguide.icu-project.org/formatparse/datetime
54
 * @see http://api.jqueryui.com/datepicker/#utility-formatDate
55
 */
56
class DateField extends TextField
57
{
58
    protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_DATE;
59
60
    /**
61
     * Override locale. If empty will default to current locale
62
     *
63
     * @var string
64
     */
65
    protected $locale = null;
66
67
    /**
68
     * Override date format. If empty will default to that used by the current locale.
69
     *
70
     * @var null
71
     */
72
    protected $dateFormat = null;
73
74
    /**
75
     * Set if js calendar should popup
76
     *
77
     * @var bool
78
     */
79
    protected $showCalendar = false;
80
81
    /**
82
     * Length of this date (full, short, etc).
83
     *
84
     * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
85
     * @var int
86
     */
87
    protected $dateLength = null;
88
89
    /**
90
     * Override locale for client side.
91
     *
92
     * @var string
93
     */
94
    protected $clientLocale = null;
95
96
    /**
97
     * Min date
98
     *
99
     * @var string ISO 8601 date for min date
100
     */
101
    protected $minDate = null;
102
103
    /**
104
     * Max date
105
     *
106
     * @var string ISO 860 date for max date
107
     */
108
    protected $maxDate = null;
109
110
    /**
111
     * Unparsed value, used exclusively for comparing with internal value
112
     * to detect invalid values.
113
     *
114
     * @var mixed
115
     */
116
    protected $rawValue = null;
117
118
    /**
119
     * Use HTML5-based input fields (and force ISO 8601 date formats).
120
     *
121
     * @var bool
122
     */
123
    protected $html5 = true;
124
125
    /**
126
     * @return bool
127
     */
128
    public function getHTML5()
129
    {
130
        return $this->html5;
131
    }
132
133
    /**
134
     * @param boolean $bool
135
     * @return $this
136
     */
137
    public function setHTML5($bool)
138
    {
139
        $this->html5 = $bool;
140
        return $this;
141
    }
142
143
    /**
144
     * Get length of the date format to use. One of:
145
     *
146
     *  - IntlDateFormatter::SHORT
147
     *  - IntlDateFormatter::MEDIUM
148
     *  - IntlDateFormatter::LONG
149
     *  - IntlDateFormatter::FULL
150
     *
151
     * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
152
     * @return int
153
     */
154
    public function getDateLength()
155
    {
156
        if ($this->dateLength) {
157
            return $this->dateLength;
158
        }
159
        return IntlDateFormatter::MEDIUM;
160
    }
161
162
    /**
163
     * Get length of the date format to use.
164
     * Only applicable with {@link setHTML5(false)}.
165
     *
166
     * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
167
     *
168
     * @param int $length
169
     * @return $this
170
     */
171
    public function setDateLength($length)
172
    {
173
        $this->dateLength = $length;
174
        return $this;
175
    }
176
177
    /**
178
     * Get date format in CLDR standard format
179
     *
180
     * This can be set explicitly. If not, this will be generated from the current locale
181
     * with the current date length.
182
     *
183
     * @see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Field-Symbol-Table
184
     */
185
    public function getDateFormat()
186
    {
187
        if ($this->getHTML5()) {
188
            // Browsers expect ISO 8601 dates, localisation is handled on the client
189
            $this->setDateFormat(DBDate::ISO_DATE);
190
        }
191
192
        if ($this->dateFormat) {
193
            return $this->dateFormat;
194
        }
195
196
        // Get from locale
197
        return $this->getFormatter()->getPattern();
198
    }
199
200
    /**
201
     * Set date format in CLDR standard format.
202
     * Only applicable with {@link setHTML5(false)}.
203
     *
204
     * @see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Field-Symbol-Table
205
     * @param string $format
206
     * @return $this
207
     */
208
    public function setDateFormat($format)
209
    {
210
        $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...
211
        return $this;
212
    }
213
214
    /**
215
     * Get date formatter with the standard locale / date format
216
     *
217
     * @throws \LogicException
218
     * @return IntlDateFormatter
219
     */
220
    protected function getFormatter()
221
    {
222
        if ($this->getHTML5() && $this->dateFormat && $this->dateFormat !== DBDate::ISO_DATE) {
223
            throw new \LogicException(
224
                'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setDateFormat()'
225
            );
226
        }
227
228
        if ($this->getHTML5() && $this->dateLength) {
229
            throw new \LogicException(
230
                'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setDateLength()'
231
            );
232
        }
233
234
        if ($this->getHTML5() && $this->locale) {
235
            throw new \LogicException(
236
                'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setLocale()'
237
            );
238
        }
239
240
        $formatter = IntlDateFormatter::create(
241
            $this->getLocale(),
242
            $this->getDateLength(),
243
            IntlDateFormatter::NONE
244
        );
245
246
        if ($this->getHTML5()) {
247
            // Browsers expect ISO 8601 dates, localisation is handled on the client
248
            $formatter->setPattern(DBDate::ISO_DATE);
249
        } elseif ($this->dateFormat) {
250
            // Don't invoke getDateFormat() directly to avoid infinite loop
251
            $ok = $formatter->setPattern($this->dateFormat);
252
            if (!$ok) {
253
                throw new InvalidArgumentException("Invalid date format {$this->dateFormat}");
254
            }
255
        }
256
        return $formatter;
257
    }
258
259
    /**
260
     * Get a date formatter for the ISO 8601 format
261
     *
262
     * @return IntlDateFormatter
263
     */
264
    protected function getISO8601Formatter()
265
    {
266
        $locale = i18n::config()->uninherited('default_locale');
267
        $formatter = IntlDateFormatter::create(
268
            i18n::config()->uninherited('default_locale'),
269
            IntlDateFormatter::MEDIUM,
270
            IntlDateFormatter::NONE
271
        );
272
        $formatter->setLenient(false);
273
        // CLDR ISO 8601 date.
274
        $formatter->setPattern(DBDate::ISO_DATE);
275
        return $formatter;
276
    }
277
278
    public function getAttributes()
279
    {
280
        $attributes = parent::getAttributes();
281
282
        $attributes['lang'] = i18n::convert_rfc1766($this->getLocale());
283
284
        if ($this->getHTML5()) {
285
            $attributes['type'] = 'date';
286
            $attributes['min'] = $this->getMinDate();
287
            $attributes['max'] = $this->getMaxDate();
288
        }
289
290
        return $attributes;
291
    }
292
293
    public function Type()
294
    {
295
        return 'date text';
296
    }
297
298
    /**
299
     * Assign value posted from form submission
300
     *
301
     * @param mixed $value
302
     * @param mixed $data
303
     * @return $this
304
     */
305
    public function setSubmittedValue($value, $data = null)
306
    {
307
        // Save raw value for later validation
308
        $this->rawValue = $value;
309
310
        // Null case
311
        if (!$value) {
312
            $this->value = null;
313
            return $this;
314
        }
315
316
        // Parse from submitted value
317
        $this->value = $this->localisedToISO8601($value);
318
        return $this;
319
    }
320
321
    public function setValue($value, $data = null)
322
    {
323
        // Save raw value for later validation
324
        $this->rawValue = $value;
325
326
        // Null case
327
        if (!$value) {
328
            $this->value = null;
329
            return $this;
330
        }
331
332
        if (is_array($value)) {
333
            throw new InvalidArgumentException("Use setSubmittedValue to assign by array");
334
        }
335
336
        // Re-run through formatter to tidy up (e.g. remove time component)
337
        $this->value = $this->tidyISO8601($value);
338
        return $this;
339
    }
340
341
    public function Value()
342
    {
343
        return $this->iso8601ToLocalised($this->value);
344
    }
345
346
    public function performReadonlyTransformation()
347
    {
348
        $field = $this->castedCopy(DateField_Disabled::class);
349
        $field->setValue($this->dataValue());
350
        $field->setReadonly(true);
351
        return $field;
352
    }
353
354
    /**
355
     * @param Validator $validator
356
     * @return bool
357
     */
358
    public function validate($validator)
359
    {
360
        // Don't validate empty fields
361
        if (empty($this->rawValue)) {
362
            return true;
363
        }
364
365
        // We submitted a value, but it couldn't be parsed
366
        if (empty($this->value)) {
367
            $validator->validationError(
368
                $this->name,
369
                _t(
370
                    'DateField.VALIDDATEFORMAT2',
371
                    "Please enter a valid date format ({format})",
372
                    ['format' => $this->getDateFormat()]
373
                )
374
            );
375
            return false;
376
        }
377
378
        // Check min date
379
        $min = $this->getMinDate();
380
        if ($min) {
381
            $oops = strtotime($this->value) < strtotime($min);
382
            if ($oops) {
383
                $validator->validationError(
384
                    $this->name,
385
                    _t(
386
                        'DateField.VALIDDATEMINDATE',
387
                        "Your date has to be newer or matching the minimum allowed date ({date})",
388
                        ['date' => $this->iso8601ToLocalised($min)]
389
                    )
390
                );
391
                return false;
392
            }
393
        }
394
395
        // Check max date
396
        $max = $this->getMaxDate();
397
        if ($max) {
398
            $oops = strtotime($this->value) > strtotime($max);
399
            if ($oops) {
400
                $validator->validationError(
401
                    $this->name,
402
                    _t(
403
                        'DateField.VALIDDATEMAXDATE',
404
                        "Your date has to be older or matching the maximum allowed date ({date})",
405
                        ['date' => $this->iso8601ToLocalised($max)]
406
                    )
407
                );
408
                return false;
409
            }
410
        }
411
412
        return true;
413
    }
414
415
    /**
416
     * Get locale to use for this field
417
     *
418
     * @return string
419
     */
420
    public function getLocale()
421
    {
422
        return $this->locale ?: i18n::get_locale();
423
    }
424
425
    /**
426
     * Determines the presented/processed format based on locale defaults,
427
     * instead of explicitly setting {@link setDateFormat()}.
428
     * Only applicable with {@link setHTML5(false)}.
429
     *
430
     * @param string $locale
431
     * @return $this
432
     */
433
    public function setLocale($locale)
434
    {
435
        $this->locale = $locale;
436
        return $this;
437
    }
438
439
    public function getSchemaValidation()
440
    {
441
        $rules = parent::getSchemaValidation();
442
        $rules['date'] = true;
443
        return $rules;
444
    }
445
446
    /**
447
     * @return string
448
     */
449
    public function getMinDate()
450
    {
451
        return $this->minDate;
452
    }
453
454
    /**
455
     * @param string $minDate
456
     * @return $this
457
     */
458
    public function setMinDate($minDate)
459
    {
460
        $this->minDate = $this->tidyISO8601($minDate);
461
        return $this;
462
    }
463
464
    /**
465
     * @return string
466
     */
467
    public function getMaxDate()
468
    {
469
        return $this->maxDate;
470
    }
471
472
    /**
473
     * @param string $maxDate
474
     * @return $this
475
     */
476
    public function setMaxDate($maxDate)
477
    {
478
        $this->maxDate = $this->tidyISO8601($maxDate);
479
        return $this;
480
    }
481
482
    /**
483
     * Convert date localised in the current locale to ISO 8601 date
484
     *
485
     * @param string $date
486
     * @return string The formatted date, or null if not a valid date
487
     */
488
    public function localisedToISO8601($date)
489
    {
490
        if (!$date) {
491
            return null;
492
        }
493
        $fromFormatter = $this->getFormatter();
494
        $toFormatter = $this->getISO8601Formatter();
495
        $timestamp = $fromFormatter->parse($date);
496
        if ($timestamp === false) {
497
            return null;
498
        }
499
        return $toFormatter->format($timestamp) ?: null;
500
    }
501
502
    /**
503
     * Convert an ISO 8601 localised date into the format specified by the
504
     * current date format.
505
     *
506
     * @param string $date
507
     * @return string The formatted date, or null if not a valid date
508
     */
509
    public function iso8601ToLocalised($date)
510
    {
511
        $date = $this->tidyISO8601($date);
512
        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...
513
            return null;
514
        }
515
        $fromFormatter = $this->getISO8601Formatter();
516
        $toFormatter = $this->getFormatter();
517
        $timestamp = $fromFormatter->parse($date);
518
        if ($timestamp === false) {
519
            return null;
520
        }
521
        return $toFormatter->format($timestamp) ?: null;
522
    }
523
524
    /**
525
     * Tidy up iso8601-ish date, or approximation
526
     *
527
     * @param string $date Date in iso8601 or approximate form
528
     * @return string iso8601 date, or null if not valid
529
     */
530
    public function tidyISO8601($date)
531
    {
532
        if (!$date) {
533
            return null;
534
        }
535
        // Re-run through formatter to tidy up (e.g. remove time component)
536
        $formatter = $this->getISO8601Formatter();
537
        $timestamp = $formatter->parse($date);
538
        if ($timestamp === false) {
539
            // Fallback to strtotime
540
            $timestamp = strtotime($date, DBDatetime::now()->getTimestamp());
541
            if ($timestamp === false) {
542
                return null;
543
            }
544
        }
545
        return $formatter->format($timestamp);
546
    }
547
}
548