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

TimeField::getFrontendFormatter()   C

Complexity

Conditions 11
Paths 7

Size

Total Lines 39
Code Lines 22

Duplication

Lines 39
Ratio 100 %

Importance

Changes 0
Metric Value
cc 11
eloc 22
nc 7
nop 0
dl 39
loc 39
rs 5.2653
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 InvalidArgumentException;
7
use SilverStripe\i18n\i18n;
8
use SilverStripe\ORM\FieldType\DBDatetime;
9
use SilverStripe\ORM\FieldType\DBTime;
10
11
/**
12
 * Form field to display editable time values in an <input type="text"> field.
13
 *
14
 * # Localization
15
 *
16
 * See {@link DateField}
17
 *
18
 * @todo Timezone support
19
 */
20
class TimeField extends TextField
21
{
22
    protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_TIME;
23
24
    /**
25
     * Override locale. If empty will default to current locale
26
     *
27
     * @var string
28
     */
29
    protected $locale = null;
30
31
    /**
32
     * Override time format. If empty will default to that used by the current locale.
33
     *
34
     * @var string
35
     */
36
    protected $timeFormat = null;
37
38
    /**
39
     * Length of this date (full, short, etc).
40
     *
41
     * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
42
     * @var int
43
     */
44
    protected $timeLength = null;
45
46
    /**
47
     * Unparsed value, used exclusively for comparing with internal value
48
     * to detect invalid values.
49
     *
50
     * @var mixed
51
     */
52
    protected $rawValue = null;
53
54
    /**
55
     * Set custom timezone
56
     *
57
     * @var string
58
     */
59
    protected $timezone = null;
60
61
    /**
62
     * Use HTML5-based input fields (and force ISO 8601 time formats).
63
     *
64
     * @var bool
65
     */
66
    protected $html5 = true;
67
68
    /**
69
     * @return bool
70
     */
71
    public function getHTML5()
72
    {
73
        return $this->html5;
74
    }
75
76
    /**
77
     * @param boolean $bool
78
     * @return $this
79
     */
80
    public function setHTML5($bool)
81
    {
82
        $this->html5 = $bool;
83
        return $this;
84
    }
85
86
    /**
87
     * Get time format in CLDR standard format
88
     *
89
     * This can be set explicitly. If not, this will be generated from the current locale
90
     * with the current time length.
91
     *
92
     * @see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Field-Symbol-Table
93
     */
94
    public function getTimeFormat()
95
    {
96
        if ($this->getHTML5()) {
97
            // Browsers expect ISO 8601 times, localisation is handled on the client
98
            $this->setTimeFormat(DBTime::ISO_TIME);
99
        }
100
101
        if ($this->timeFormat) {
102
            return $this->timeFormat;
103
        }
104
105
        // Get from locale
106
        return $this->getFrontendFormatter()->getPattern();
107
    }
108
109
    /**
110
     * Set time format in CLDR standard format.
111
     * Only applicable with {@link setHTML5(false)}.
112
     *
113
     * @see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Field-Symbol-Table
114
     * @param string $format
115
     * @return $this
116
     */
117
    public function setTimeFormat($format)
118
    {
119
        $this->timeFormat = $format;
120
        return $this;
121
    }
122
123
    /**
124
     * Get length of the time format to use. One of:
125
     *
126
     *  - IntlDateFormatter::SHORT E.g. '6:31 PM'
127
     *  - IntlDateFormatter::MEDIUM E.g. '6:30:48 PM'
128
     *  - IntlDateFormatter::LONG E.g. '6:32:09 PM NZDT'
129
     *  - IntlDateFormatter::FULL E.g. '6:32:24 PM New Zealand Daylight Time'
130
     *
131
     * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
132
     * @return int
133
     */
134
    public function getTimeLength()
135
    {
136
        if ($this->timeLength) {
137
            return $this->timeLength;
138
        }
139
        return IntlDateFormatter::MEDIUM;
140
    }
141
142
    /**
143
     * Get length of the time format to use.
144
     * Only applicable with {@link setHTML5(false)}.
145
     *
146
     * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
147
     *
148
     * @param int $length
149
     * @return $this
150
     */
151
    public function setTimeLength($length)
152
    {
153
        $this->timeLength = $length;
154
        return $this;
155
    }
156
157
    /**
158
     * Get time formatter with the standard locale / date format
159
     *
160
     * @return IntlDateFormatter
161
     */
162 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...
163
    {
164
        if ($this->getHTML5() && $this->timeFormat && $this->timeFormat !== DBTime::ISO_TIME) {
165
            throw new \LogicException(
166
                'Please opt-out of HTML5 processing of ISO 8601 times via setHTML5(false) if using setTimeFormat()'
167
            );
168
        }
169
170
        if ($this->getHTML5() && $this->timeLength) {
171
            throw new \LogicException(
172
                'Please opt-out of HTML5 processing of ISO 8601 times via setHTML5(false) if using setTimeLength()'
173
            );
174
        }
175
176
        if ($this->getHTML5() && $this->locale) {
177
            throw new \LogicException(
178
                'Please opt-out of HTML5 processing of ISO 8601 times via setHTML5(false) if using setLocale()'
179
            );
180
        }
181
182
        $formatter =  IntlDateFormatter::create(
183
            $this->getLocale(),
184
            IntlDateFormatter::NONE,
185
            $this->getTimeLength(),
186
            $this->getTimezone()
187
        );
188
189
        if ($this->getHTML5()) {
190
            // Browsers expect ISO 8601 times, localisation is handled on the client
191
            $formatter->setPattern(DBTime::ISO_TIME);
192
            // Don't invoke getTimeFormat() directly to avoid infinite loop
193
        } elseif ($this->timeFormat) {
194
            $ok = $formatter->setPattern($this->timeFormat);
195
            if (!$ok) {
196
                throw new InvalidArgumentException("Invalid time format {$this->timeFormat}");
197
            }
198
        }
199
        return $formatter;
200
    }
201
202
    /**
203
     * Get a time formatter for the ISO 8601 format
204
     *
205
     * @return IntlDateFormatter
206
     */
207 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...
208
    {
209
        $formatter = IntlDateFormatter::create(
210
            i18n::config()->uninherited('default_locale'),
211
            IntlDateFormatter::NONE,
212
            IntlDateFormatter::MEDIUM,
213
            date_default_timezone_get() // Default to server timezone
214
        );
215
        $formatter->setLenient(false);
216
217
        // Note we omit timezone from this format, and we assume server TZ always.
218
        $formatter->setPattern(DBTime::ISO_TIME);
219
220
        return $formatter;
221
    }
222
223
    public function getAttributes()
224
    {
225
        $attributes = parent::getAttributes();
226
227
        if ($this->getHTML5()) {
228
            $attributes['type'] = 'time';
229
        }
230
231
        return $attributes;
232
    }
233
234
    public function getSchemaDataDefaults()
235
    {
236
        $defaults = parent::getSchemaDataDefaults();
237
        return array_merge($defaults, [
238
            'lang' => i18n::convert_rfc1766($this->getLocale()),
239
            'data' => array_merge($defaults['data'], [
240
                'html5' => $this->getHTML5(),
241
            ])
242
        ]);
243
    }
244
245
    public function Type()
246
    {
247
        return 'time text';
248
    }
249
250
    /**
251
     * Assign value posted from form submission
252
     *
253
     * @param mixed $value
254
     * @param mixed $data
255
     * @return $this
256
     */
257
    public function setSubmittedValue($value, $data = null)
258
    {
259
        // Save raw value for later validation
260
        $this->rawValue = $value;
261
262
        // Parse from submitted value
263
        $this->value = $this->frontendToInternal($value);
264
        return $this;
265
    }
266
267
    /**
268
     * Set time assigned from database value
269
     *
270
     * @param mixed $value
271
     * @param mixed $data
272
     * @return $this
273
     */
274 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...
275
    {
276
        // Save raw value for later validation
277
        $this->rawValue = $value;
278
279
        // Null case
280
        if (!$value) {
281
            $this->value = null;
282
            return $this;
283
        }
284
285
        // Re-run through formatter to tidy up (e.g. remove date component)
286
        $this->value = $this->tidyInternal($value);
287
        return $this;
288
    }
289
290
    public function Value()
291
    {
292
        $localised = $this->internalToFrontend($this->value);
293
        if ($localised) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $localised of type null|string is loosely compared to true; 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...
294
            return $localised;
295
        }
296
297
        // Show midnight in localised format
298
        return $this->getMidnight();
299
    }
300
301
    /**
302
     * Show midnight in current format (adjusts for timezone)
303
     *
304
     * @return string
305
     */
306
    public function getMidnight()
307
    {
308
        $formatter = $this->getFrontendFormatter();
309
        $timestamp = $this->withTimezone($this->getTimezone(), function () {
310
            return strtotime('midnight');
311
        });
312
        return $formatter->format($timestamp);
313
    }
314
315
    /**
316
     * Validate this field
317
     *
318
     * @param Validator $validator
319
     * @return bool
320
     */
321
    public function validate($validator)
322
    {
323
        // Don't validate empty fields
324
        if (empty($this->rawValue)) {
325
            return true;
326
        }
327
328
        // We submitted a value, but it couldn't be parsed
329
        if (empty($this->value)) {
330
            $validator->validationError(
331
                $this->name,
332
                _t(
333
                    'SilverStripe\\Forms\\TimeField.VALIDATEFORMAT',
334
                    "Please enter a valid time format ({format})",
335
                    ['format' => $this->getTimeFormat()]
336
                )
337
            );
338
            return false;
339
        }
340
        return true;
341
    }
342
343
    /**
344
     * @return string
345
     */
346
    public function getLocale()
347
    {
348
        return $this->locale ?: i18n::get_locale();
349
    }
350
351
    /**
352
     * Determines the presented/processed format based on locale defaults,
353
     * instead of explicitly setting {@link setTimeFormat()}.
354
     * Only applicable with {@link setHTML5(false)}.
355
     *
356
     * @param string $locale
357
     * @return $this
358
     */
359
    public function setLocale($locale)
360
    {
361
        $this->locale = $locale;
362
        return $this;
363
    }
364
365
    /**
366
     * Creates a new readonly field specified below
367
     *
368
     * @return TimeField_Readonly
369
     */
370
    public function performReadonlyTransformation()
371
    {
372
        /** @var TimeField_Readonly $result */
373
        $result = $this->castedCopy(TimeField_Readonly::class);
374
        return $result;
375
    }
376
377
    /**
378
     * Convert frontend time to the internal representation (ISO 8601).
379
     * The frontend time is also in ISO 8601 when $html5=true.
380
     *
381
     * @param string $time
382
     * @return string The formatted time, or null if not a valid time
383
     */
384 View Code Duplication
    protected function frontendToInternal($time)
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...
385
    {
386
        if (!$time) {
387
            return null;
388
        }
389
        $fromFormatter = $this->getFrontendFormatter();
390
        $toFormatter = $this->getInternalFormatter();
391
        $timestamp = $fromFormatter->parse($time);
392
393
        // Try to parse time without seconds, since that's a valid HTML5 submission format
394
        // See https://html.spec.whatwg.org/multipage/infrastructure.html#times
395
        if ($timestamp === false && $this->getHTML5()) {
396
            $fromFormatter->setPattern(str_replace(':ss', '', DBTime::ISO_TIME));
397
            $timestamp = $fromFormatter->parse($time);
398
        }
399
400
        // If timestamp still can't be detected, we've got an invalid time
401
        if ($timestamp === false) {
402
            return null;
403
        }
404
405
        return $toFormatter->format($timestamp);
406
    }
407
408
    /**
409
     * Convert the internal time representation (ISO 8601) to a format used by the frontend,
410
     * as defined by {@link $timeFormat}. With $html5=true, the frontend time will also be
411
     * in ISO 8601.
412
     *
413
     * @param string $time
414
     * @return string
415
     */
416 View Code Duplication
    protected function internalToFrontend($time)
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...
417
    {
418
        $time = $this->tidyInternal($time);
419
        if (!$time) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $time 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...
420
            return null;
421
        }
422
        $fromFormatter = $this->getInternalFormatter();
423
        $toFormatter = $this->getFrontendFormatter();
424
        $timestamp = $fromFormatter->parse($time);
425
        if ($timestamp === false) {
426
            return null;
427
        }
428
        return $toFormatter->format($timestamp);
429
    }
430
431
432
433
    /**
434
     * Tidy up the internal time representation (ISO 8601),
435
     * and fall back to strtotime() if there's parsing errors.
436
     *
437
     * @param string $time Time in ISO 8601 or approximate form
438
     * @return string ISO 8601 time, or null if not valid
439
     */
440 View Code Duplication
    protected function tidyInternal($time)
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...
441
    {
442
        if (!$time) {
443
            return null;
444
        }
445
        // Re-run through formatter to tidy up (e.g. remove date component)
446
        $formatter = $this->getInternalFormatter();
447
        $timestamp = $formatter->parse($time);
448
        if ($timestamp === false) {
449
            // Fallback to strtotime
450
            $timestamp = strtotime($time, DBDatetime::now()->getTimestamp());
451
            if ($timestamp === false) {
452
                return null;
453
            }
454
        }
455
        return $formatter->format($timestamp);
456
    }
457
458
    /**
459
     * @return string
460
     */
461
    public function getTimezone()
462
    {
463
        return $this->timezone;
464
    }
465
466
    /**
467
     * @param string $timezone
468
     * @return $this
469
     */
470 View Code Duplication
    public function setTimezone($timezone)
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...
471
    {
472
        if ($this->value && $timezone !== $this->timezone) {
473
            throw new \BadMethodCallException("Can't change timezone after setting a value");
474
        }
475
        $this->timezone = $timezone;
476
        return $this;
477
    }
478
479
480
    /**
481
     * Run a callback within a specific timezone
482
     *
483
     * @param string $timezone
484
     * @param callable $callback
485
     */
486
    protected function withTimezone($timezone, $callback)
487
    {
488
        $currentTimezone = date_default_timezone_get();
489
        try {
490
            if ($timezone) {
491
                date_default_timezone_set($timezone);
492
            }
493
            return $callback();
494
        } finally {
495
            // Restore timezone
496
            if ($timezone) {
497
                date_default_timezone_set($currentTimezone);
498
            }
499
        }
500
    }
501
}
502