Completed
Push — readme-redesign ( e2fd40...17eb05 )
by Alexander
108:51 queued 68:52
created

Formatter::asImage()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 2
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\i18n;
9
10
use DateInterval;
11
use DateTime;
12
use DateTimeInterface;
13
use DateTimeZone;
14
use IntlDateFormatter;
15
use NumberFormatter;
16
use Yii;
17
use yii\base\Component;
18
use yii\base\InvalidConfigException;
19
use yii\base\InvalidParamException;
20
use yii\helpers\FormatConverter;
21
use yii\helpers\HtmlPurifier;
22
use yii\helpers\Html;
23
24
/**
25
 * Formatter provides a set of commonly used data formatting methods.
26
 *
27
 * The formatting methods provided by Formatter are all named in the form of `asXyz()`.
28
 * The behavior of some of them may be configured via the properties of Formatter. For example,
29
 * by configuring [[dateFormat]], one may control how [[asDate()]] formats the value into a date string.
30
 *
31
 * Formatter is configured as an application component in [[\yii\base\Application]] by default.
32
 * You can access that instance via `Yii::$app->formatter`.
33
 *
34
 * The Formatter class is designed to format values according to a [[locale]]. For this feature to work
35
 * the [PHP intl extension](http://php.net/manual/en/book.intl.php) has to be installed.
36
 * Most of the methods however work also if the PHP intl extension is not installed by providing
37
 * a fallback implementation. Without intl month and day names are in English only.
38
 * Note that even if the intl extension is installed, formatting date and time values for years >=2038 or <=1901
39
 * on 32bit systems will fall back to the PHP implementation because intl uses a 32bit UNIX timestamp internally.
40
 * On a 64bit system the intl formatter is used in all cases if installed.
41
 *
42
 * @author Qiang Xue <[email protected]>
43
 * @author Enrica Ruedin <[email protected]>
44
 * @author Carsten Brandt <[email protected]>
45
 * @since 2.0
46
 */
47
class Formatter extends Component
48
{
49
    /**
50
     * @var string the text to be displayed when formatting a `null` value.
51
     * Defaults to `'<span class="not-set">(not set)</span>'`, where `(not set)`
52
     * will be translated according to [[locale]].
53
     */
54
    public $nullDisplay;
55
    /**
56
     * @var array the text to be displayed when formatting a boolean value. The first element corresponds
57
     * to the text displayed for `false`, the second element for `true`.
58
     * Defaults to `['No', 'Yes']`, where `Yes` and `No`
59
     * will be translated according to [[locale]].
60
     */
61
    public $booleanFormat;
62
    /**
63
     * @var string the locale ID that is used to localize the date and number formatting.
64
     * For number and date formatting this is only effective when the
65
     * [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
66
     * If not set, [[\yii\base\Application::language]] will be used.
67
     */
68
    public $locale;
69
    /**
70
     * @var string the time zone to use for formatting time and date values.
71
     *
72
     * This can be any value that may be passed to [date_default_timezone_set()](http://www.php.net/manual/en/function.date-default-timezone-set.php)
73
     * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
74
     * Refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available time zones.
75
     * If this property is not set, [[\yii\base\Application::timeZone]] will be used.
76
     *
77
     * Note that the default time zone for input data is assumed to be UTC by default if no time zone is included in the input date value.
78
     * If you store your data in a different time zone in the database, you have to adjust [[defaultTimeZone]] accordingly.
79
     */
80
    public $timeZone;
81
    /**
82
     * @var string the time zone that is assumed for input values if they do not include a time zone explicitly.
83
     *
84
     * The value must be a valid time zone identifier, e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
85
     * Please refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available time zones.
86
     *
87
     * It defaults to `UTC` so you only have to adjust this value if you store datetime values in another time zone in your database.
88
     *
89
     * @since 2.0.1
90
     */
91
    public $defaultTimeZone = 'UTC';
92
    /**
93
     * @var string the default format string to be used to format a [[asDate()|date]].
94
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
95
     *
96
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax).
97
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
98
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
99
     *
100
     * For example:
101
     *
102
     * ```php
103
     * 'MM/dd/yyyy' // date in ICU format
104
     * 'php:m/d/Y' // the same date in PHP format
105
     * ```
106
     */
107
    public $dateFormat = 'medium';
108
    /**
109
     * @var string the default format string to be used to format a [[asTime()|time]].
110
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
111
     *
112
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax).
113
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
114
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
115
     *
116
     * For example:
117
     *
118
     * ```php
119
     * 'HH:mm:ss' // time in ICU format
120
     * 'php:H:i:s' // the same time in PHP format
121
     * ```
122
     */
123
    public $timeFormat = 'medium';
124
    /**
125
     * @var string the default format string to be used to format a [[asDatetime()|date and time]].
126
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
127
     *
128
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax).
129
     *
130
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
131
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
132
     *
133
     * For example:
134
     *
135
     * ```php
136
     * 'MM/dd/yyyy HH:mm:ss' // date and time in ICU format
137
     * 'php:m/d/Y H:i:s' // the same date and time in PHP format
138
     * ```
139
     */
140
    public $datetimeFormat = 'medium';
141
    /**
142
     * @var \IntlCalendar|int|null the calendar to be used for date formatting. The value of this property will be directly
143
     * passed to the [constructor of the `IntlDateFormatter` class](http://php.net/manual/en/intldateformatter.create.php).
144
     *
145
     * Defaults to `null`, which means the Gregorian calendar will be used. You may also explicitly pass the constant
146
     * `\IntlDateFormatter::GREGORIAN` for Gregorian calendar.
147
     *
148
     * To use an alternative calendar like for example the [Jalali calendar](https://en.wikipedia.org/wiki/Jalali_calendar),
149
     * set this property to `\IntlDateFormatter::TRADITIONAL`.
150
     * The calendar must then be specified in the [[locale]], for example for the persian calendar the configuration for the formatter would be:
151
     *
152
     * ```php
153
     * 'formatter' => [
154
     *     'locale' => 'fa_IR@calendar=persian',
155
     *     'calendar' => \IntlDateFormatter::TRADITIONAL,
156
     * ],
157
     * ```
158
     *
159
     * Available calendar names can be found in the [ICU manual](http://userguide.icu-project.org/datetime/calendar).
160
     *
161
     * Since PHP 5.5 you may also use an instance of the [[\IntlCalendar]] class.
162
     * Check the [PHP manual](http://php.net/manual/en/intldateformatter.create.php) for more details.
163
     *
164
     * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, setting this property will have no effect.
165
     *
166
     * @see http://php.net/manual/en/intldateformatter.create.php
167
     * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants.calendartypes
168
     * @see http://php.net/manual/en/class.intlcalendar.php
169
     * @since 2.0.7
170
     */
171
    public $calendar;
172
    /**
173
     * @var string the character displayed as the decimal point when formatting a number.
174
     * If not set, the decimal separator corresponding to [[locale]] will be used.
175
     * If [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is '.'.
176
     */
177
    public $decimalSeparator;
178
    /**
179
     * @var string the character displayed as the thousands separator (also called grouping separator) character when formatting a number.
180
     * If not set, the thousand separator corresponding to [[locale]] will be used.
181
     * If [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is ','.
182
     */
183
    public $thousandSeparator;
184
    /**
185
     * @var array a list of name value pairs that are passed to the
186
     * intl [Numberformatter::setAttribute()](http://php.net/manual/en/numberformatter.setattribute.php) method of all
187
     * the number formatter objects created by [[createNumberFormatter()]].
188
     * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
189
     *
190
     * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatattribute)
191
     * for the possible options.
192
     *
193
     * For example to adjust the maximum and minimum value of fraction digits you can configure this property like the following:
194
     *
195
     * ```php
196
     * [
197
     *     NumberFormatter::MIN_FRACTION_DIGITS => 0,
198
     *     NumberFormatter::MAX_FRACTION_DIGITS => 2,
199
     * ]
200
     * ```
201
     */
202
    public $numberFormatterOptions = [];
203
    /**
204
     * @var array a list of name value pairs that are passed to the
205
     * intl [Numberformatter::setTextAttribute()](http://php.net/manual/en/numberformatter.settextattribute.php) method of all
206
     * the number formatter objects created by [[createNumberFormatter()]].
207
     * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
208
     *
209
     * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformattextattribute)
210
     * for the possible options.
211
     *
212
     * For example to change the minus sign for negative numbers you can configure this property like the following:
213
     *
214
     * ```php
215
     * [
216
     *     NumberFormatter::NEGATIVE_PREFIX => 'MINUS',
217
     * ]
218
     * ```
219
     */
220
    public $numberFormatterTextOptions = [];
221
    /**
222
     * @var array a list of name value pairs that are passed to the
223
     * intl [Numberformatter::setSymbol()](http://php.net/manual/en/numberformatter.setsymbol.php) method of all
224
     * the number formatter objects created by [[createNumberFormatter()]].
225
     * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
226
     *
227
     * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatsymbol)
228
     * for the possible options.
229
     *
230
     * For example to choose a custom currency symbol, e.g. [U+20BD](http://unicode-table.com/en/20BD/) instead of `руб.` for Russian Ruble:
231
     *
232
     * ```php
233
     * [
234
     *     NumberFormatter::CURRENCY_SYMBOL => '₽',
235
     * ]
236
     * ```
237
     *
238
     * @since 2.0.4
239
     */
240
    public $numberFormatterSymbols = [];
241
    /**
242
     * @var string the 3-letter ISO 4217 currency code indicating the default currency to use for [[asCurrency]].
243
     * If not set, the currency code corresponding to [[locale]] will be used.
244
     * Note that in this case the [[locale]] has to be specified with a country code, e.g. `en-US` otherwise it
245
     * is not possible to determine the default currency.
246
     */
247
    public $currencyCode;
248
    /**
249
     * @var int the base at which a kilobyte is calculated (1000 or 1024 bytes per kilobyte), used by [[asSize]] and [[asShortSize]].
250
     * Defaults to 1024.
251
     */
252
    public $sizeFormatBase = 1024;
253
254
    /**
255
     * @var bool whether the [PHP intl extension](http://php.net/manual/en/book.intl.php) is loaded.
256
     */
257
    private $_intlLoaded = false;
258
259
260
    /**
261
     * @inheritdoc
262
     */
263
    public function init()
264
    {
265
        if ($this->timeZone === null) {
266
            $this->timeZone = Yii::$app->timeZone;
267
        }
268
        if ($this->locale === null) {
269
            $this->locale = Yii::$app->language;
270
        }
271
        if ($this->booleanFormat === null) {
272
            $this->booleanFormat = [Yii::t('yii', 'No', [], $this->locale), Yii::t('yii', 'Yes', [], $this->locale)];
273
        }
274
        if ($this->nullDisplay === null) {
275
            $this->nullDisplay = '<span class="not-set">' . Yii::t('yii', '(not set)', [], $this->locale) . '</span>';
276
        }
277
        $this->_intlLoaded = extension_loaded('intl');
278
        if (!$this->_intlLoaded) {
279
            if ($this->decimalSeparator === null) {
280
                $this->decimalSeparator = '.';
281
            }
282
            if ($this->thousandSeparator === null) {
283
                $this->thousandSeparator = ',';
284
            }
285
        }
286
    }
287
288
    /**
289
     * Formats the value based on the given format type.
290
     * This method will call one of the "as" methods available in this class to do the formatting.
291
     * For type "xyz", the method "asXyz" will be used. For example, if the format is "html",
292
     * then [[asHtml()]] will be used. Format names are case insensitive.
293
     * @param mixed $value the value to be formatted.
294
     * @param string|array $format the format of the value, e.g., "html", "text". To specify additional
295
     * parameters of the formatting method, you may use an array. The first element of the array
296
     * specifies the format name, while the rest of the elements will be used as the parameters to the formatting
297
     * method. For example, a format of `['date', 'Y-m-d']` will cause the invocation of `asDate($value, 'Y-m-d')`.
298
     * @return string the formatting result.
299
     * @throws InvalidParamException if the format type is not supported by this class.
300
     */
301
    public function format($value, $format)
302
    {
303
        if (is_array($format)) {
304
            if (!isset($format[0])) {
305
                throw new InvalidParamException('The $format array must contain at least one element.');
306
            }
307
            $f = $format[0];
308
            $format[0] = $value;
309
            $params = $format;
310
            $format = $f;
311
        } else {
312
            $params = [$value];
313
        }
314
        $method = 'as' . $format;
315
        if ($this->hasMethod($method)) {
316
            return call_user_func_array([$this, $method], $params);
317
        } else {
318
            throw new InvalidParamException("Unknown format type: $format");
319
        }
320
    }
321
322
323
    // simple formats
324
325
326
    /**
327
     * Formats the value as is without any formatting.
328
     * This method simply returns back the parameter without any format.
329
     * The only exception is a `null` value which will be formatted using [[nullDisplay]].
330
     * @param mixed $value the value to be formatted.
331
     * @return string the formatted result.
332
     */
333
    public function asRaw($value)
334
    {
335
        if ($value === null) {
336
            return $this->nullDisplay;
337
        }
338
        return $value;
339
    }
340
341
    /**
342
     * Formats the value as an HTML-encoded plain text.
343
     * @param string $value the value to be formatted.
344
     * @return string the formatted result.
345
     */
346
    public function asText($value)
347
    {
348
        if ($value === null) {
349
            return $this->nullDisplay;
350
        }
351
        return Html::encode($value);
352
    }
353
354
    /**
355
     * Formats the value as an HTML-encoded plain text with newlines converted into breaks.
356
     * @param string $value the value to be formatted.
357
     * @return string the formatted result.
358
     */
359
    public function asNtext($value)
360
    {
361
        if ($value === null) {
362
            return $this->nullDisplay;
363
        }
364
        return nl2br(Html::encode($value));
365
    }
366
367
    /**
368
     * Formats the value as HTML-encoded text paragraphs.
369
     * Each text paragraph is enclosed within a `<p>` tag.
370
     * One or multiple consecutive empty lines divide two paragraphs.
371
     * @param string $value the value to be formatted.
372
     * @return string the formatted result.
373
     */
374
    public function asParagraphs($value)
375
    {
376
        if ($value === null) {
377
            return $this->nullDisplay;
378
        }
379
        return str_replace('<p></p>', '', '<p>' . preg_replace('/\R{2,}/u', "</p>\n<p>", Html::encode($value)) . '</p>');
380
    }
381
382
    /**
383
     * Formats the value as HTML text.
384
     * The value will be purified using [[HtmlPurifier]] to avoid XSS attacks.
385
     * Use [[asRaw()]] if you do not want any purification of the value.
386
     * @param string $value the value to be formatted.
387
     * @param array|null $config the configuration for the HTMLPurifier class.
388
     * @return string the formatted result.
389
     */
390
    public function asHtml($value, $config = null)
391
    {
392
        if ($value === null) {
393
            return $this->nullDisplay;
394
        }
395
        return HtmlPurifier::process($value, $config);
396
    }
397
398
    /**
399
     * Formats the value as a mailto link.
400
     * @param string $value the value to be formatted.
401
     * @param array $options the tag options in terms of name-value pairs. See [[Html::mailto()]].
402
     * @return string the formatted result.
403
     */
404
    public function asEmail($value, $options = [])
405
    {
406
        if ($value === null) {
407
            return $this->nullDisplay;
408
        }
409
        return Html::mailto(Html::encode($value), $value, $options);
410
    }
411
412
    /**
413
     * Formats the value as an image tag.
414
     * @param mixed $value the value to be formatted.
415
     * @param array $options the tag options in terms of name-value pairs. See [[Html::img()]].
416
     * @return string the formatted result.
417
     */
418
    public function asImage($value, $options = [])
419
    {
420
        if ($value === null) {
421
            return $this->nullDisplay;
422
        }
423
        return Html::img($value, $options);
424
    }
425
426
    /**
427
     * Formats the value as a hyperlink.
428
     * @param mixed $value the value to be formatted.
429
     * @param array $options the tag options in terms of name-value pairs. See [[Html::a()]].
430
     * @return string the formatted result.
431
     */
432
    public function asUrl($value, $options = [])
433
    {
434
        if ($value === null) {
435
            return $this->nullDisplay;
436
        }
437
        $url = $value;
438
        if (strpos($url, '://') === false) {
439
            $url = 'http://' . $url;
440
        }
441
442
        return Html::a(Html::encode($value), $url, $options);
443
    }
444
445
    /**
446
     * Formats the value as a boolean.
447
     * @param mixed $value the value to be formatted.
448
     * @return string the formatted result.
449
     * @see booleanFormat
450
     */
451
    public function asBoolean($value)
452
    {
453
        if ($value === null) {
454
            return $this->nullDisplay;
455
        }
456
457
        return $value ? $this->booleanFormat[1] : $this->booleanFormat[0];
458
    }
459
460
461
    // date and time formats
462
463
464
    /**
465
     * Formats the value as a date.
466
     * @param int|string|DateTime $value the value to be formatted. The following
467
     * types of value are supported:
468
     *
469
     * - an integer representing a UNIX timestamp
470
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
471
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
472
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
473
     *
474
     * @param string $format the format used to convert the value into a date string.
475
     * If null, [[dateFormat]] will be used.
476
     *
477
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
478
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
479
     *
480
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
481
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
482
     *
483
     * @return string the formatted result.
484
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
485
     * @throws InvalidConfigException if the date format is invalid.
486
     * @see dateFormat
487
     */
488
    public function asDate($value, $format = null)
489
    {
490
        if ($format === null) {
491
            $format = $this->dateFormat;
492
        }
493
        return $this->formatDateTimeValue($value, $format, 'date');
494
    }
495
496
    /**
497
     * Formats the value as a time.
498
     * @param int|string|DateTime $value the value to be formatted. The following
499
     * types of value are supported:
500
     *
501
     * - an integer representing a UNIX timestamp
502
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
503
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
504
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
505
     *
506
     * @param string $format the format used to convert the value into a date string.
507
     * If null, [[timeFormat]] will be used.
508
     *
509
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
510
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
511
     *
512
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
513
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
514
     *
515
     * @return string the formatted result.
516
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
517
     * @throws InvalidConfigException if the date format is invalid.
518
     * @see timeFormat
519
     */
520
    public function asTime($value, $format = null)
521
    {
522
        if ($format === null) {
523
            $format = $this->timeFormat;
524
        }
525
        return $this->formatDateTimeValue($value, $format, 'time');
526
    }
527
528
    /**
529
     * Formats the value as a datetime.
530
     * @param int|string|DateTime $value the value to be formatted. The following
531
     * types of value are supported:
532
     *
533
     * - an integer representing a UNIX timestamp
534
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
535
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
536
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
537
     *
538
     * @param string $format the format used to convert the value into a date string.
539
     * If null, [[dateFormat]] will be used.
540
     *
541
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
542
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
543
     *
544
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
545
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
546
     *
547
     * @return string the formatted result.
548
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
549
     * @throws InvalidConfigException if the date format is invalid.
550
     * @see datetimeFormat
551
     */
552
    public function asDatetime($value, $format = null)
553
    {
554
        if ($format === null) {
555
            $format = $this->datetimeFormat;
556
        }
557
        return $this->formatDateTimeValue($value, $format, 'datetime');
558
    }
559
560
    /**
561
     * @var array map of short format names to IntlDateFormatter constant values.
562
     */
563
    private $_dateFormats = [
564
        'short'  => 3, // IntlDateFormatter::SHORT,
565
        'medium' => 2, // IntlDateFormatter::MEDIUM,
566
        'long'   => 1, // IntlDateFormatter::LONG,
567
        'full'   => 0, // IntlDateFormatter::FULL,
568
    ];
569
570
    /**
571
     * @param int|string|DateTime $value the value to be formatted. The following
572
     * types of value are supported:
573
     *
574
     * - an integer representing a UNIX timestamp
575
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
576
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
577
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
578
     *
579
     * @param string $format the format used to convert the value into a date string.
580
     * @param string $type 'date', 'time', or 'datetime'.
581
     * @throws InvalidConfigException if the date format is invalid.
582
     * @return string the formatted result.
583
     */
584
    private function formatDateTimeValue($value, $format, $type)
585
    {
586
        $timeZone = $this->timeZone;
587
        // avoid time zone conversion for date-only and time-only values
588
        if ($type === 'date' || $type === 'time') {
589
            list($timestamp, $hasTimeInfo, $hasDateInfo) = $this->normalizeDatetimeValue($value, true);
590
            if ($type === 'date' && !$hasTimeInfo || $type === 'time' && !$hasDateInfo) {
591
                $timeZone = $this->defaultTimeZone;
592
            }
593
        } else {
594
            $timestamp = $this->normalizeDatetimeValue($value);
595
        }
596
        if ($timestamp === null) {
597
            return $this->nullDisplay;
598
        }
599
600
        // intl does not work with dates >=2038 or <=1901 on 32bit machines, fall back to PHP
601
        $year = $timestamp->format('Y');
602
        if ($this->_intlLoaded && !(PHP_INT_SIZE === 4 && ($year <= 1901 || $year >= 2038))) {
603
            if (strncmp($format, 'php:', 4) === 0) {
604
                $format = FormatConverter::convertDatePhpToIcu(substr($format, 4));
605
            }
606
            if (isset($this->_dateFormats[$format])) {
607
                if ($type === 'date') {
608
                    $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, $timeZone, $this->calendar);
609
                } elseif ($type === 'time') {
610
                    $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $timeZone, $this->calendar);
611
                } else {
612
                    $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $timeZone, $this->calendar);
613
                }
614
            } else {
615
                $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $timeZone, $this->calendar, $format);
616
            }
617
            if ($formatter === null) {
618
                throw new InvalidConfigException(intl_get_error_message());
619
            }
620
            // make IntlDateFormatter work with DateTimeImmutable
621
            if ($timestamp instanceof \DateTimeImmutable) {
0 ignored issues
show
Bug introduced by
The class DateTimeImmutable does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
622
                $timestamp = new DateTime($timestamp->format(DateTime::ISO8601), $timestamp->getTimezone());
623
            }
624
            return $formatter->format($timestamp);
625
        } else {
626
            if (strncmp($format, 'php:', 4) === 0) {
627
                $format = substr($format, 4);
628
            } else {
629
                $format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale);
630
            }
631
            if ($timeZone != null) {
632
                if ($timestamp instanceof \DateTimeImmutable) {
0 ignored issues
show
Bug introduced by
The class DateTimeImmutable does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
633
                    $timestamp = $timestamp->setTimezone(new DateTimeZone($timeZone));
634
                } else {
635
                    $timestamp->setTimezone(new DateTimeZone($timeZone));
636
                }
637
            }
638
            return $timestamp->format($format);
639
        }
640
    }
641
642
    /**
643
     * Normalizes the given datetime value as a DateTime object that can be taken by various date/time formatting methods.
644
     *
645
     * @param int|string|DateTime $value the datetime value to be normalized. The following
646
     * types of value are supported:
647
     *
648
     * - an integer representing a UNIX timestamp
649
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
650
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
651
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
652
     *
653
     * @param bool $checkDateTimeInfo whether to also check if the date/time value has some time and date information attached.
654
     * Defaults to `false`. If `true`, the method will then return an array with the first element being the normalized
655
     * timestamp, the second a boolean indicating whether the timestamp has time information and third a boolean indicating
656
     * whether the timestamp has date information.
657
     * This parameter is available since version 2.0.1.
658
     * @return DateTime|array the normalized datetime value.
659
     * Since version 2.0.1 this may also return an array if `$checkTimeInfo` is true.
660
     * The first element of the array is the normalized timestamp and the second is a boolean indicating whether
661
     * the timestamp has time information or it is just a date value.
662
     * Since version 2.0.12 the array has third boolean element indicating whether the timestamp has date information
663
     * or it is just a time value.
664
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
665
     */
666
    protected function normalizeDatetimeValue($value, $checkDateTimeInfo = false)
667
    {
668
        // checking for DateTime and DateTimeInterface is not redundant, DateTimeInterface is only in PHP>5.5
669
        if ($value === null || $value instanceof DateTime || $value instanceof DateTimeInterface) {
0 ignored issues
show
Bug introduced by
The class DateTimeInterface does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
670
            // skip any processing
671
            return $checkDateTimeInfo ? [$value, true, true] : $value;
672
        }
673
        if (empty($value)) {
674
            $value = 0;
675
        }
676
        try {
677
            if (is_numeric($value)) { // process as unix timestamp, which is always in UTC
678
                $timestamp = new DateTime('@' . (int)$value, new DateTimeZone('UTC'));
679
                return $checkDateTimeInfo ? [$timestamp, true, true] : $timestamp;
680
            } elseif (($timestamp = DateTime::createFromFormat('Y-m-d', $value, new DateTimeZone($this->defaultTimeZone))) !== false) { // try Y-m-d format (support invalid dates like 2012-13-01)
681
                return $checkDateTimeInfo ? [$timestamp, false, true] : $timestamp;
682
            } elseif (($timestamp = DateTime::createFromFormat('Y-m-d H:i:s', $value, new DateTimeZone($this->defaultTimeZone))) !== false) { // try Y-m-d H:i:s format (support invalid dates like 2012-13-01 12:63:12)
683
                return $checkDateTimeInfo ? [$timestamp, true, true] : $timestamp;
684
            }
685
            // finally try to create a DateTime object with the value
686
            if ($checkDateTimeInfo) {
687
                $timestamp = new DateTime($value, new DateTimeZone($this->defaultTimeZone));
688
                $info = date_parse($value);
689
                return [
690
                    $timestamp,
691
                    !($info['hour'] === false && $info['minute'] === false && $info['second'] === false),
692
                    !($info['year'] === false && $info['month'] === false && $info['day'] === false)
693
                ];
694
            } else {
695
                return new DateTime($value, new DateTimeZone($this->defaultTimeZone));
696
            }
697
        } catch (\Exception $e) {
698
            throw new InvalidParamException("'$value' is not a valid date time value: " . $e->getMessage()
699
                . "\n" . print_r(DateTime::getLastErrors(), true), $e->getCode(), $e);
700
        }
701
    }
702
703
    /**
704
     * Formats a date, time or datetime in a float number as UNIX timestamp (seconds since 01-01-1970).
705
     * @param int|string|DateTime $value the value to be formatted. The following
706
     * types of value are supported:
707
     *
708
     * - an integer representing a UNIX timestamp
709
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
710
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
711
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
712
     *
713
     * @return string the formatted result.
714
     */
715
    public function asTimestamp($value)
716
    {
717
        if ($value === null) {
718
            return $this->nullDisplay;
719
        }
720
        $timestamp = $this->normalizeDatetimeValue($value);
721
        return number_format($timestamp->format('U'), 0, '.', '');
722
    }
723
724
    /**
725
     * Formats the value as the time interval between a date and now in human readable form.
726
     *
727
     * This method can be used in three different ways:
728
     *
729
     * 1. Using a timestamp that is relative to `now`.
730
     * 2. Using a timestamp that is relative to the `$referenceTime`.
731
     * 3. Using a `DateInterval` object.
732
     *
733
     * @param int|string|DateTime|DateInterval $value the value to be formatted. The following
734
     * types of value are supported:
735
     *
736
     * - an integer representing a UNIX timestamp
737
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
738
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
739
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
740
     * - a PHP DateInterval object (a positive time interval will refer to the past, a negative one to the future)
741
     *
742
     * @param int|string|DateTime $referenceTime if specified the value is used as a reference time instead of `now`
743
     * when `$value` is not a `DateInterval` object.
744
     * @return string the formatted result.
745
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
746
     */
747
    public function asRelativeTime($value, $referenceTime = null)
748
    {
749
        if ($value === null) {
750
            return $this->nullDisplay;
751
        }
752
753
        if ($value instanceof DateInterval) {
754
            $interval = $value;
755
        } else {
756
            $timestamp = $this->normalizeDatetimeValue($value);
757
758
            if ($timestamp === false) {
759
                // $value is not a valid date/time value, so we try
760
                // to create a DateInterval with it
761
                try {
762
                    $interval = new DateInterval($value);
763
                } catch (\Exception $e) {
764
                    // invalid date/time and invalid interval
765
                    return $this->nullDisplay;
766
                }
767
            } else {
768
                $timeZone = new DateTimeZone($this->timeZone);
769
770
                if ($referenceTime === null) {
771
                    $dateNow = new DateTime('now', $timeZone);
772
                } else {
773
                    $dateNow = $this->normalizeDatetimeValue($referenceTime);
774
                    $dateNow->setTimezone($timeZone);
775
                }
776
777
                $dateThen = $timestamp->setTimezone($timeZone);
778
779
                $interval = $dateThen->diff($dateNow);
780
            }
781
        }
782
783
        if ($interval->invert) {
784
            if ($interval->y >= 1) {
785
                return Yii::t('yii', 'in {delta, plural, =1{a year} other{# years}}', ['delta' => $interval->y], $this->locale);
786
            }
787
            if ($interval->m >= 1) {
788
                return Yii::t('yii', 'in {delta, plural, =1{a month} other{# months}}', ['delta' => $interval->m], $this->locale);
789
            }
790
            if ($interval->d >= 1) {
791
                return Yii::t('yii', 'in {delta, plural, =1{a day} other{# days}}', ['delta' => $interval->d], $this->locale);
792
            }
793
            if ($interval->h >= 1) {
794
                return Yii::t('yii', 'in {delta, plural, =1{an hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
795
            }
796
            if ($interval->i >= 1) {
797
                return Yii::t('yii', 'in {delta, plural, =1{a minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
798
            }
799
            if ($interval->s == 0) {
800
                return Yii::t('yii', 'just now', [], $this->locale);
801
            }
802
            return Yii::t('yii', 'in {delta, plural, =1{a second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
803
        } else {
804
            if ($interval->y >= 1) {
805
                return Yii::t('yii', '{delta, plural, =1{a year} other{# years}} ago', ['delta' => $interval->y], $this->locale);
806
            }
807
            if ($interval->m >= 1) {
808
                return Yii::t('yii', '{delta, plural, =1{a month} other{# months}} ago', ['delta' => $interval->m], $this->locale);
809
            }
810
            if ($interval->d >= 1) {
811
                return Yii::t('yii', '{delta, plural, =1{a day} other{# days}} ago', ['delta' => $interval->d], $this->locale);
812
            }
813
            if ($interval->h >= 1) {
814
                return Yii::t('yii', '{delta, plural, =1{an hour} other{# hours}} ago', ['delta' => $interval->h], $this->locale);
815
            }
816
            if ($interval->i >= 1) {
817
                return Yii::t('yii', '{delta, plural, =1{a minute} other{# minutes}} ago', ['delta' => $interval->i], $this->locale);
818
            }
819
            if ($interval->s == 0) {
820
                return Yii::t('yii', 'just now', [], $this->locale);
821
            }
822
            return Yii::t('yii', '{delta, plural, =1{a second} other{# seconds}} ago', ['delta' => $interval->s], $this->locale);
823
        }
824
    }
825
826
    /**
827
     * Represents the value as duration in human readable format.
828
     *
829
     * @param DateInterval|string|int $value the value to be formatted. Acceptable formats:
830
     *  - [DateInterval object](http://php.net/manual/ru/class.dateinterval.php)
831
     *  - integer - number of seconds. For example: value `131` represents `2 minutes, 11 seconds`
832
     *  - ISO8601 duration format. For example, all of these values represent `1 day, 2 hours, 30 minutes` duration:
833
     *    `2015-01-01T13:00:00Z/2015-01-02T13:30:00Z` - between two datetime values
834
     *    `2015-01-01T13:00:00Z/P1D2H30M` - time interval after datetime value
835
     *    `P1D2H30M/2015-01-02T13:30:00Z` - time interval before datetime value
836
     *    `P1D2H30M` - simply a date interval
837
     *    `P-1D2H30M` - a negative date interval (`-1 day, 2 hours, 30 minutes`)
838
     *
839
     * @param string $implodeString will be used to concatenate duration parts. Defaults to `, `.
840
     * @param string $negativeSign will be prefixed to the formatted duration, when it is negative. Defaults to `-`.
841
     * @return string the formatted duration.
842
     * @since 2.0.7
843
     */
844
    public function asDuration($value, $implodeString = ', ', $negativeSign = '-')
845
    {
846
        if ($value === null) {
847
            return $this->nullDisplay;
848
        }
849
850
        if ($value instanceof DateInterval) {
851
            $isNegative = $value->invert;
852
            $interval = $value;
853
        } elseif (is_numeric($value)) {
854
            $isNegative = $value < 0;
855
            $zeroDateTime = (new DateTime())->setTimestamp(0);
856
            $valueDateTime = (new DateTime())->setTimestamp(abs($value));
857
            $interval = $valueDateTime->diff($zeroDateTime);
858
        } elseif (strpos($value, 'P-') === 0) {
859
            $interval = new DateInterval('P'.substr($value, 2));
860
            $isNegative = true;
861
        } else {
862
            $interval = new DateInterval($value);
863
            $isNegative = $interval->invert;
864
        }
865
866
        if ($interval->y > 0) {
867
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 year} other{# years}}', ['delta' => $interval->y], $this->locale);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$parts was never initialized. Although not strictly required by PHP, it is generally a good practice to add $parts = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
868
        }
869
        if ($interval->m > 0) {
870
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 month} other{# months}}', ['delta' => $interval->m], $this->locale);
0 ignored issues
show
Bug introduced by
The variable $parts does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
871
        }
872
        if ($interval->d > 0) {
873
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 day} other{# days}}', ['delta' => $interval->d], $this->locale);
874
        }
875
        if ($interval->h > 0) {
876
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
877
        }
878
        if ($interval->i > 0) {
879
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
880
        }
881
        if ($interval->s > 0) {
882
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
883
        }
884
        if ($interval->s === 0 && empty($parts)) {
885
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
886
            $isNegative = false;
887
        }
888
889
        return empty($parts) ? $this->nullDisplay : (($isNegative ? $negativeSign : '') . implode($implodeString, $parts));
890
    }
891
892
893
    // number formats
894
895
896
    /**
897
     * Formats the value as an integer number by removing any decimal digits without rounding.
898
     *
899
     * @param mixed $value the value to be formatted.
900
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
901
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
902
     * @return string the formatted result.
903
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
904
     */
905
    public function asInteger($value, $options = [], $textOptions = [])
906
    {
907
        if ($value === null) {
908
            return $this->nullDisplay;
909
        }
910
        $value = $this->normalizeNumericValue($value);
911
        if ($this->_intlLoaded) {
912
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, null, $options, $textOptions);
913
            $f->setAttribute(NumberFormatter::FRACTION_DIGITS, 0);
914
            if (($result = $f->format($value, NumberFormatter::TYPE_INT64)) === false) {
915
                throw new InvalidParamException('Formatting integer value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
916
            }
917
            return $result;
918
        } else {
919
            return number_format((int) $value, 0, $this->decimalSeparator, $this->thousandSeparator);
920
        }
921
    }
922
923
    /**
924
     * Formats the value as a decimal number.
925
     *
926
     * Property [[decimalSeparator]] will be used to represent the decimal point. The
927
     * value is rounded automatically to the defined decimal digits.
928
     *
929
     * @param mixed $value the value to be formatted.
930
     * @param int $decimals the number of digits after the decimal point.
931
     * If not given, the number of digits depends in the input value and is determined based on
932
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
933
     * using [[$numberFormatterOptions]].
934
     * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is `2`.
935
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
936
     * specify a value here.
937
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
938
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
939
     * @return string the formatted result.
940
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
941
     * @see decimalSeparator
942
     * @see thousandSeparator
943
     */
944
    public function asDecimal($value, $decimals = null, $options = [], $textOptions = [])
945
    {
946
        if ($value === null) {
947
            return $this->nullDisplay;
948
        }
949
        $value = $this->normalizeNumericValue($value);
950
951
        if ($this->_intlLoaded) {
952
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, $decimals, $options, $textOptions);
953
            if (($result = $f->format($value)) === false) {
954
                throw new InvalidParamException('Formatting decimal value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
955
            }
956
            return $result;
957
        } else {
958
            if ($decimals === null) {
959
                $decimals = 2;
960
            }
961
            return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator);
962
        }
963
    }
964
965
966
    /**
967
     * Formats the value as a percent number with "%" sign.
968
     *
969
     * @param mixed $value the value to be formatted. It must be a factor e.g. `0.75` will result in `75%`.
970
     * @param int $decimals the number of digits after the decimal point.
971
     * If not given, the number of digits depends in the input value and is determined based on
972
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
973
     * using [[$numberFormatterOptions]].
974
     * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is `0`.
975
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
976
     * specify a value here.
977
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
978
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
979
     * @return string the formatted result.
980
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
981
     */
982
    public function asPercent($value, $decimals = null, $options = [], $textOptions = [])
983
    {
984
        if ($value === null) {
985
            return $this->nullDisplay;
986
        }
987
        $value = $this->normalizeNumericValue($value);
988
989
        if ($this->_intlLoaded) {
990
            $f = $this->createNumberFormatter(NumberFormatter::PERCENT, $decimals, $options, $textOptions);
991
            if (($result = $f->format($value)) === false) {
992
                throw new InvalidParamException('Formatting percent value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
993
            }
994
            return $result;
995
        } else {
996
            if ($decimals === null) {
997
                $decimals = 0;
998
            }
999
            $value *= 100;
1000
            return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator) . '%';
1001
        }
1002
    }
1003
1004
    /**
1005
     * Formats the value as a scientific number.
1006
     *
1007
     * @param mixed $value the value to be formatted.
1008
     * @param int $decimals the number of digits after the decimal point.
1009
     * If not given, the number of digits depends in the input value and is determined based on
1010
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
1011
     * using [[$numberFormatterOptions]].
1012
     * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value depends on your PHP configuration.
1013
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
1014
     * specify a value here.
1015
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1016
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1017
     * @return string the formatted result.
1018
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1019
     */
1020
    public function asScientific($value, $decimals = null, $options = [], $textOptions = [])
1021
    {
1022
        if ($value === null) {
1023
            return $this->nullDisplay;
1024
        }
1025
        $value = $this->normalizeNumericValue($value);
1026
1027
        if ($this->_intlLoaded) {
1028
            $f = $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $decimals, $options, $textOptions);
1029
            if (($result = $f->format($value)) === false) {
1030
                throw new InvalidParamException('Formatting scientific number value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1031
            }
1032
            return $result;
1033
        } else {
1034
            if ($decimals !== null) {
1035
                return sprintf("%.{$decimals}E", $value);
1036
            } else {
1037
                return sprintf('%.E', $value);
1038
            }
1039
        }
1040
    }
1041
1042
    /**
1043
     * Formats the value as a currency number.
1044
     *
1045
     * This function does not require the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed
1046
     * to work, but it is highly recommended to install it to get good formatting results.
1047
     *
1048
     * @param mixed $value the value to be formatted.
1049
     * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use.
1050
     * If null, [[currencyCode]] will be used.
1051
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1052
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1053
     * @return string the formatted result.
1054
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1055
     * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined.
1056
     */
1057
    public function asCurrency($value, $currency = null, $options = [], $textOptions = [])
1058
    {
1059
        if ($value === null) {
1060
            return $this->nullDisplay;
1061
        }
1062
        $value = $this->normalizeNumericValue($value);
1063
1064
        if ($this->_intlLoaded) {
1065
            $currency = $currency ?: $this->currencyCode;
1066
            // currency code must be set before fraction digits
1067
            // http://php.net/manual/en/numberformatter.formatcurrency.php#114376
1068
            if ($currency && !isset($textOptions[NumberFormatter::CURRENCY_CODE])) {
1069
                $textOptions[NumberFormatter::CURRENCY_CODE] = $currency;
1070
            }
1071
            $formatter = $this->createNumberFormatter(NumberFormatter::CURRENCY, null, $options, $textOptions);
1072
            if ($currency === null) {
1073
                $result = $formatter->format($value);
1074
            } else {
1075
                $result = $formatter->formatCurrency($value, $currency);
1076
            }
1077
            if ($result === false) {
1078
                throw new InvalidParamException('Formatting currency value failed: ' . $formatter->getErrorCode() . ' ' . $formatter->getErrorMessage());
1079
            }
1080
            return $result;
1081
        } else {
1082
            if ($currency === null) {
1083
                if ($this->currencyCode === null) {
1084
                    throw new InvalidConfigException('The default currency code for the formatter is not defined and the php intl extension is not installed which could take the default currency from the locale.');
1085
                }
1086
                $currency = $this->currencyCode;
1087
            }
1088
            return $currency . ' ' . $this->asDecimal($value, 2, $options, $textOptions);
1089
        }
1090
    }
1091
1092
    /**
1093
     * Formats the value as a number spellout.
1094
     *
1095
     * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
1096
     *
1097
     * @param mixed $value the value to be formatted
1098
     * @return string the formatted result.
1099
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1100
     * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
1101
     */
1102
    public function asSpellout($value)
1103
    {
1104
        if ($value === null) {
1105
            return $this->nullDisplay;
1106
        }
1107
        $value = $this->normalizeNumericValue($value);
1108
        if ($this->_intlLoaded) {
1109
            $f = $this->createNumberFormatter(NumberFormatter::SPELLOUT);
1110
            if (($result = $f->format($value)) === false) {
1111
                throw new InvalidParamException('Formatting number as spellout failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1112
            }
1113
            return $result;
1114
        } else {
1115
            throw new InvalidConfigException('Format as Spellout is only supported when PHP intl extension is installed.');
1116
        }
1117
    }
1118
1119
    /**
1120
     * Formats the value as a ordinal value of a number.
1121
     *
1122
     * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
1123
     *
1124
     * @param mixed $value the value to be formatted
1125
     * @return string the formatted result.
1126
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1127
     * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
1128
     */
1129
    public function asOrdinal($value)
1130
    {
1131
        if ($value === null) {
1132
            return $this->nullDisplay;
1133
        }
1134
        $value = $this->normalizeNumericValue($value);
1135
        if ($this->_intlLoaded) {
1136
            $f = $this->createNumberFormatter(NumberFormatter::ORDINAL);
1137
            if (($result = $f->format($value)) === false) {
1138
                throw new InvalidParamException('Formatting number as ordinal failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1139
            }
1140
            return $result;
1141
        } else {
1142
            throw new InvalidConfigException('Format as Ordinal is only supported when PHP intl extension is installed.');
1143
        }
1144
    }
1145
1146
    /**
1147
     * Formats the value in bytes as a size in human readable form for example `12 KB`.
1148
     *
1149
     * This is the short form of [[asSize]].
1150
     *
1151
     * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
1152
     * are used in the formatting result.
1153
     *
1154
     * @param string|int|float $value value in bytes to be formatted.
1155
     * @param int $decimals the number of digits after the decimal point.
1156
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1157
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1158
     * @return string the formatted result.
1159
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1160
     * @see sizeFormatBase
1161
     * @see asSize
1162
     */
1163
    public function asShortSize($value, $decimals = null, $options = [], $textOptions = [])
1164
    {
1165
        if ($value === null) {
1166
            return $this->nullDisplay;
1167
        }
1168
1169
        list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
1170
1171
        if ($this->sizeFormatBase == 1024) {
1172
            switch ($position) {
1173
                case 0:
1174
                    return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
1175
                case 1:
1176
                    return Yii::t('yii', '{nFormatted} KiB', $params, $this->locale);
1177
                case 2:
1178
                    return Yii::t('yii', '{nFormatted} MiB', $params, $this->locale);
1179
                case 3:
1180
                    return Yii::t('yii', '{nFormatted} GiB', $params, $this->locale);
1181
                case 4:
1182
                    return Yii::t('yii', '{nFormatted} TiB', $params, $this->locale);
1183
                default:
1184
                    return Yii::t('yii', '{nFormatted} PiB', $params, $this->locale);
1185
            }
1186
        } else {
1187
            switch ($position) {
1188
                case 0:
1189
                    return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
1190
                case 1:
1191
                    return Yii::t('yii', '{nFormatted} KB', $params, $this->locale);
1192
                case 2:
1193
                    return Yii::t('yii', '{nFormatted} MB', $params, $this->locale);
1194
                case 3:
1195
                    return Yii::t('yii', '{nFormatted} GB', $params, $this->locale);
1196
                case 4:
1197
                    return Yii::t('yii', '{nFormatted} TB', $params, $this->locale);
1198
                default:
1199
                    return Yii::t('yii', '{nFormatted} PB', $params, $this->locale);
1200
            }
1201
        }
1202
    }
1203
1204
    /**
1205
     * Formats the value in bytes as a size in human readable form, for example `12 kilobytes`.
1206
     *
1207
     * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
1208
     * are used in the formatting result.
1209
     *
1210
     * @param string|int|float $value value in bytes to be formatted.
1211
     * @param int $decimals the number of digits after the decimal point.
1212
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1213
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1214
     * @return string the formatted result.
1215
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1216
     * @see sizeFormatBase
1217
     * @see asShortSize
1218
     */
1219
    public function asSize($value, $decimals = null, $options = [], $textOptions = [])
1220
    {
1221
        if ($value === null) {
1222
            return $this->nullDisplay;
1223
        }
1224
1225
        list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
1226
1227
        if ($this->sizeFormatBase == 1024) {
1228
            switch ($position) {
1229
                case 0:
1230
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
1231
                case 1:
1232
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}', $params, $this->locale);
1233
                case 2:
1234
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}', $params, $this->locale);
1235
                case 3:
1236
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', $params, $this->locale);
1237
                case 4:
1238
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}', $params, $this->locale);
1239
                default:
1240
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}', $params, $this->locale);
1241
            }
1242
        } else {
1243
            switch ($position) {
1244
                case 0:
1245
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
1246
                case 1:
1247
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}', $params, $this->locale);
1248
                case 2:
1249
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}', $params, $this->locale);
1250
                case 3:
1251
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', $params, $this->locale);
1252
                case 4:
1253
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', $params, $this->locale);
1254
                default:
1255
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', $params, $this->locale);
1256
            }
1257
        }
1258
    }
1259
1260
1261
    /**
1262
     * Given the value in bytes formats number part of the human readable form.
1263
     *
1264
     * @param string|int|float $value value in bytes to be formatted.
1265
     * @param int $decimals the number of digits after the decimal point
1266
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1267
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1268
     * @return array [parameters for Yii::t containing formatted number, internal position of size unit]
1269
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1270
     */
1271
    private function formatSizeNumber($value, $decimals, $options, $textOptions)
1272
    {
1273
        $value = $this->normalizeNumericValue($value);
1274
1275
        $position = 0;
1276
        do {
1277
            if (abs($value) < $this->sizeFormatBase) {
1278
                break;
1279
            }
1280
            $value /= $this->sizeFormatBase;
1281
            $position++;
1282
        } while ($position < 5);
1283
1284
        // no decimals for bytes
1285
        if ($position === 0) {
1286
            $decimals = 0;
1287
        } elseif ($decimals !== null) {
1288
            $value = round($value, $decimals);
1289
        }
1290
        // disable grouping for edge cases like 1023 to get 1023 B instead of 1,023 B
1291
        $oldThousandSeparator = $this->thousandSeparator;
1292
        $this->thousandSeparator = '';
1293
        if ($this->_intlLoaded) {
1294
            $options[NumberFormatter::GROUPING_USED] = false;
1295
        }
1296
        // format the size value
1297
        $params = [
1298
            // this is the unformatted number used for the plural rule
1299
            // abs() to make sure the plural rules work correctly on negative numbers, intl does not cover this
1300
            // http://english.stackexchange.com/questions/9735/is-1-singular-or-plural
1301
            'n' => abs($value),
1302
            // this is the formatted number used for display
1303
            'nFormatted' => $this->asDecimal($value, $decimals, $options, $textOptions),
1304
        ];
1305
        $this->thousandSeparator = $oldThousandSeparator;
1306
1307
        return [$params, $position];
1308
    }
1309
1310
    /**
1311
     * Normalizes a numeric input value
1312
     *
1313
     * - everything [empty](http://php.net/manual/en/function.empty.php) will result in `0`
1314
     * - a [numeric](http://php.net/manual/en/function.is-numeric.php) string will be casted to float
1315
     * - everything else will be returned if it is [numeric](http://php.net/manual/en/function.is-numeric.php),
1316
     *   otherwise an exception is thrown.
1317
     *
1318
     * @param mixed $value the input value
1319
     * @return float|int the normalized number value
1320
     * @throws InvalidParamException if the input value is not numeric.
1321
     */
1322
    protected function normalizeNumericValue($value)
1323
    {
1324
        if (empty($value)) {
1325
            return 0;
1326
        }
1327
        if (is_string($value) && is_numeric($value)) {
1328
            $value = (float) $value;
1329
        }
1330
        if (!is_numeric($value)) {
1331
            throw new InvalidParamException("'$value' is not a numeric value.");
1332
        }
1333
        return $value;
1334
    }
1335
1336
    /**
1337
     * Creates a number formatter based on the given type and format.
1338
     *
1339
     * You may override this method to create a number formatter based on patterns.
1340
     *
1341
     * @param int $style the type of the number formatter.
1342
     * Values: NumberFormatter::DECIMAL, ::CURRENCY, ::PERCENT, ::SCIENTIFIC, ::SPELLOUT, ::ORDINAL
1343
     * ::DURATION, ::PATTERN_RULEBASED, ::DEFAULT_STYLE, ::IGNORE
1344
     * @param int $decimals the number of digits after the decimal point.
1345
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1346
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1347
     * @return NumberFormatter the created formatter instance
1348
     */
1349
    protected function createNumberFormatter($style, $decimals = null, $options = [], $textOptions = [])
1350
    {
1351
        $formatter = new NumberFormatter($this->locale, $style);
1352
1353
        // set text attributes
1354
        foreach ($this->numberFormatterTextOptions as $name => $attribute) {
1355
            $formatter->setTextAttribute($name, $attribute);
1356
        }
1357
        foreach ($textOptions as $name => $attribute) {
1358
            $formatter->setTextAttribute($name, $attribute);
1359
        }
1360
1361
        // set attributes
1362
        foreach ($this->numberFormatterOptions as $name => $value) {
1363
            $formatter->setAttribute($name, $value);
1364
        }
1365
        foreach ($options as $name => $value) {
1366
            $formatter->setAttribute($name, $value);
1367
        }
1368
        if ($decimals !== null) {
1369
            $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimals);
1370
            $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimals);
1371
        }
1372
1373
        // set symbols
1374
        if ($this->decimalSeparator !== null) {
1375
            $formatter->setSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $this->decimalSeparator);
1376
        }
1377
        if ($this->thousandSeparator !== null) {
1378
            $formatter->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1379
            $formatter->setSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1380
        }
1381
        foreach ($this->numberFormatterSymbols as $name => $symbol) {
1382
            $formatter->setSymbol($name, $symbol);
1383
        }
1384
1385
        return $formatter;
1386
    }
1387
}
1388