GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

MessageFormatter   F
last analyzed

Complexity

Total Complexity 92

Size/Duplication

Total Lines 394
Duplicated Lines 0 %

Test Coverage

Coverage 80.2%

Importance

Changes 0
Metric Value
eloc 198
dl 0
loc 394
ccs 158
cts 197
cp 0.802
rs 2
c 0
b 0
f 0
wmc 92

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getErrorCode() 0 3 1
C replaceNamedArguments() 0 46 14
B parse() 0 51 9
B format() 0 48 7
A getErrorMessage() 0 3 1
C tokenizePattern() 0 40 13
A fallbackFormat() 0 20 5
D parseToken() 0 99 42

How to fix   Complexity   

Complex Class

Complex classes like MessageFormatter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MessageFormatter, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\i18n;
9
10
use Yii;
11
use yii\base\Component;
12
use yii\base\NotSupportedException;
13
14
/**
15
 * MessageFormatter allows formatting messages via [ICU message format](https://unicode-org.github.io/icu/userguide/format_parse/messages/).
16
 *
17
 * This class enhances the message formatter class provided by the PHP intl extension.
18
 *
19
 * The following enhancements are provided:
20
 *
21
 * - It accepts named arguments and mixed numeric and named arguments.
22
 * - Issues no error when an insufficient number of arguments have been provided. Instead, the placeholders will not be
23
 *   substituted.
24
 * - Fixes PHP 5.5 weird placeholder replacement in case no arguments are provided at all (https://bugs.php.net/bug.php?id=65920).
25
 * - Offers limited support for message formatting in case PHP intl extension is not installed.
26
 *   However it is highly recommended that you install [PHP intl extension](https://www.php.net/manual/en/book.intl.php) if you want
27
 *   to use MessageFormatter features.
28
 *
29
 *   The fallback implementation only supports the following message formats:
30
 *   - plural formatting for english ('one' and 'other' selectors)
31
 *   - select format
32
 *   - simple parameters
33
 *   - integer number parameters
34
 *
35
 *   The fallback implementation does NOT support the ['apostrophe-friendly' syntax](https://www.php.net/manual/en/messageformatter.formatmessage.php).
36
 *   Also messages that are working with the fallback implementation are not necessarily compatible with the
37
 *   PHP intl MessageFormatter so do not rely on the fallback if you are able to install intl extension somehow.
38
 *
39
 * @property-read string $errorCode Code of the last error.
40
 * @property-read string $errorMessage Description of the last error.
41
 *
42
 * @author Alexander Makarov <[email protected]>
43
 * @author Carsten Brandt <[email protected]>
44
 * @since 2.0
45
 */
46
class MessageFormatter extends Component
47
{
48
    private $_errorCode = 0;
49
    private $_errorMessage = '';
50
51
52
    /**
53
     * Get the error code from the last operation.
54
     * @link https://www.php.net/manual/en/messageformatter.geterrorcode.php
55
     * @return string Code of the last error.
56
     */
57
    public function getErrorCode()
58
    {
59
        return $this->_errorCode;
60
    }
61
62
    /**
63
     * Get the error text from the last operation.
64
     * @link https://www.php.net/manual/en/messageformatter.geterrormessage.php
65
     * @return string Description of the last error.
66
     */
67 49
    public function getErrorMessage()
68
    {
69 49
        return $this->_errorMessage;
70
    }
71
72
    /**
73
     * Formats a message via [ICU message format](https://unicode-org.github.io/icu/userguide/format_parse/messages/).
74
     *
75
     * It uses the PHP intl extension's [MessageFormatter](https://www.php.net/manual/en/class.messageformatter.php)
76
     * and works around some issues.
77
     * If PHP intl is not installed a fallback will be used that supports a subset of the ICU message format.
78
     *
79
     * @param string $pattern The pattern string to insert parameters into.
80
     * @param array $params The array of name value pairs to insert into the format string.
81
     * @param string $language The locale to use for formatting locale-dependent parts
82
     * @return string|false The formatted pattern string or `false` if an error occurred
83
     */
84 157
    public function format($pattern, $params, $language)
85
    {
86 157
        $this->_errorCode = 0;
87 157
        $this->_errorMessage = '';
88
89 157
        if ($params === []) {
90 4
            return $pattern;
91
        }
92
93 153
        if (!class_exists('MessageFormatter', false)) {
94
            return $this->fallbackFormat($pattern, $params, $language);
95
        }
96
97
        // replace named arguments (https://github.com/yiisoft/yii2/issues/9678)
98 153
        $newParams = [];
99 153
        $pattern = $this->replaceNamedArguments($pattern, $params, $newParams);
100 153
        $params = $newParams;
101
102
        try {
103 153
            $formatter = new \MessageFormatter($language, $pattern);
104
105 151
            if ($formatter === null) {
106
                // formatter may be null in PHP 5.x
107
                $this->_errorCode = intl_get_error_code();
108
                $this->_errorMessage = 'Message pattern is invalid: ' . intl_get_error_message();
109 151
                return false;
110
            }
111 2
        } catch (\IntlException $e) {
112
            // IntlException is thrown since PHP 7
113 2
            $this->_errorCode = $e->getCode();
114 2
            $this->_errorMessage = 'Message pattern is invalid: ' . $e->getMessage();
115 2
            return false;
116
        } catch (\Exception $e) {
117
            // Exception is thrown by HHVM
118
            $this->_errorCode = $e->getCode();
119
            $this->_errorMessage = 'Message pattern is invalid: ' . $e->getMessage();
120
            return false;
121
        }
122
123 151
        $result = $formatter->format($params);
124
125 151
        if ($result === false) {
126
            $this->_errorCode = $formatter->getErrorCode();
127
            $this->_errorMessage = $formatter->getErrorMessage();
128
            return false;
129
        }
130
131 151
        return $result;
132
    }
133
134
    /**
135
     * Parses an input string according to an [ICU message format](https://unicode-org.github.io/icu/userguide/format_parse/messages/) pattern.
136
     *
137
     * It uses the PHP intl extension's [MessageFormatter::parse()](https://www.php.net/manual/en/messageformatter.parsemessage.php)
138
     * and adds support for named arguments.
139
     * Usage of this method requires PHP intl extension to be installed.
140
     *
141
     * @param string $pattern The pattern to use for parsing the message.
142
     * @param string $message The message to parse, conforming to the pattern.
143
     * @param string $language The locale to use for formatting locale-dependent parts
144
     * @return array|bool An array containing items extracted, or `FALSE` on error.
145
     * @throws \yii\base\NotSupportedException when PHP intl extension is not installed.
146
     */
147 7
    public function parse($pattern, $message, $language)
148
    {
149 7
        $this->_errorCode = 0;
150 7
        $this->_errorMessage = '';
151
152 7
        if (!class_exists('MessageFormatter', false)) {
153
            throw new NotSupportedException('You have to install PHP intl extension to use this feature.');
154
        }
155
156
        // replace named arguments
157 7
        if (($tokens = self::tokenizePattern($pattern)) === false) {
0 ignored issues
show
introduced by
The condition $tokens = self::tokenize...ern($pattern) === false is always false.
Loading history...
158
            $this->_errorCode = -1;
159
            $this->_errorMessage = 'Message pattern is invalid.';
160
161
            return false;
162
        }
163 7
        $map = [];
164 7
        foreach ($tokens as $i => $token) {
165 7
            if (is_array($token)) {
166 7
                $param = trim($token[0]);
167 7
                if (!isset($map[$param])) {
168 7
                    $map[$param] = count($map);
169
                }
170 7
                $token[0] = $map[$param];
171 7
                $tokens[$i] = '{' . implode(',', $token) . '}';
172
            }
173
        }
174 7
        $pattern = implode('', $tokens);
175 7
        $map = array_flip($map);
176
177 7
        $formatter = new \MessageFormatter($language, $pattern);
178 7
        if ($formatter === null) {
179
            $this->_errorCode = -1;
180
            $this->_errorMessage = 'Message pattern is invalid.';
181
182
            return false;
183
        }
184 7
        $result = $formatter->parse($message);
185 7
        if ($result === false) {
186
            $this->_errorCode = $formatter->getErrorCode();
187
            $this->_errorMessage = $formatter->getErrorMessage();
188
189
            return false;
190
        }
191
192 7
        $values = [];
193 7
        foreach ($result as $key => $value) {
194 7
            $values[$map[$key]] = $value;
195
        }
196
197 7
        return $values;
198
    }
199
200
    /**
201
     * Replace named placeholders with numeric placeholders and quote unused.
202
     *
203
     * @param string $pattern The pattern string to replace things into.
204
     * @param array $givenParams The array of values to insert into the format string.
205
     * @param array $resultingParams Modified array of parameters.
206
     * @param array $map
207
     * @return string The pattern string with placeholders replaced.
208
     */
209 153
    private function replaceNamedArguments($pattern, $givenParams, &$resultingParams = [], &$map = [])
210
    {
211 153
        if (($tokens = self::tokenizePattern($pattern)) === false) {
0 ignored issues
show
introduced by
The condition $tokens = self::tokenize...ern($pattern) === false is always false.
Loading history...
212 1
            return false;
213
        }
214 152
        foreach ($tokens as $i => $token) {
215 152
            if (!is_array($token)) {
216 152
                continue;
217
            }
218 152
            $param = trim($token[0]);
219 152
            if (array_key_exists($param, $givenParams)) {
220
                // if param is given, replace it with a number
221 149
                if (!isset($map[$param])) {
222 149
                    $map[$param] = count($map);
223
                    // make sure only used params are passed to format method
224 149
                    $resultingParams[$map[$param]] = $givenParams[$param];
225
                }
226 149
                $token[0] = $map[$param];
227 149
                $quote = '';
228
            } else {
229
                // quote unused token
230 4
                $quote = "'";
231
            }
232 152
            $type = isset($token[1]) ? trim($token[1]) : 'none';
233
            // replace plural and select format recursively
234 152
            if ($type === 'plural' || $type === 'select') {
235 137
                if (!isset($token[2])) {
236
                    return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
237
                }
238 137
                if (($subtokens = self::tokenizePattern($token[2])) === false) {
239
                    return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
240
                }
241 137
                $c = count($subtokens);
242 137
                for ($k = 0; $k + 1 < $c; $k++) {
243 137
                    if (is_array($subtokens[$k]) || !is_array($subtokens[++$k])) {
244
                        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
245
                    }
246 137
                    $subpattern = $this->replaceNamedArguments(implode(',', $subtokens[$k]), $givenParams, $resultingParams, $map);
247 137
                    $subtokens[$k] = $quote . '{' . $quote . $subpattern . $quote . '}' . $quote;
248
                }
249 137
                $token[2] = implode('', $subtokens);
250
            }
251 152
            $tokens[$i] = $quote . '{' . $quote . implode(',', $token) . $quote . '}' . $quote;
252
        }
253
254 152
        return implode('', $tokens);
255
    }
256
257
    /**
258
     * Fallback implementation for MessageFormatter::formatMessage.
259
     * @param string $pattern The pattern string to insert things into.
260
     * @param array $args The array of values to insert into the format string
261
     * @param string $locale The locale to use for formatting locale-dependent parts
262
     * @return false|string The formatted pattern string or `false` if an error occurred
263
     */
264 18
    protected function fallbackFormat($pattern, $args, $locale)
265
    {
266 18
        if (($tokens = self::tokenizePattern($pattern)) === false) {
0 ignored issues
show
introduced by
The condition $tokens = self::tokenize...ern($pattern) === false is always false.
Loading history...
267
            $this->_errorCode = -1;
268
            $this->_errorMessage = 'Message pattern is invalid.';
269
270
            return false;
271
        }
272 18
        foreach ($tokens as $i => $token) {
273 18
            if (is_array($token)) {
274 18
                if (($tokens[$i] = $this->parseToken($token, $args, $locale)) === false) {
275
                    $this->_errorCode = -1;
276
                    $this->_errorMessage = 'Message pattern is invalid.';
277
278
                    return false;
279
                }
280
            }
281
        }
282
283 16
        return implode('', $tokens);
284
    }
285
286
    /**
287
     * Tokenizes a pattern by separating normal text from replaceable patterns.
288
     * @param string $pattern patter to tokenize
289
     * @return array|bool array of tokens or false on failure
290
     */
291 178
    private static function tokenizePattern($pattern)
292
    {
293 178
        $charset = Yii::$app ? Yii::$app->charset : 'UTF-8';
294 178
        $depth = 1;
295 178
        if (($start = $pos = mb_strpos($pattern, '{', 0, $charset)) === false) {
296 139
            return [$pattern];
297
        }
298 178
        $tokens = [mb_substr($pattern, 0, $pos, $charset)];
299 178
        while (true) {
300 178
            $open = mb_strpos($pattern, '{', $pos + 1, $charset);
301 178
            $close = mb_strpos($pattern, '}', $pos + 1, $charset);
302 178
            if ($open === false && $close === false) {
303 177
                break;
304
            }
305 178
            if ($open === false) {
306 177
                $open = mb_strlen($pattern, $charset);
307
            }
308 178
            if ($close > $open) {
309 160
                $depth++;
310 160
                $pos = $open;
311
            } else {
312 178
                $depth--;
313 178
                $pos = $close;
314
            }
315 178
            if ($depth === 0) {
316 178
                $tokens[] = explode(',', mb_substr($pattern, $start + 1, $pos - $start - 1, $charset), 3);
317 178
                $start = $pos + 1;
318 178
                $tokens[] = mb_substr($pattern, $start, $open - $start, $charset);
319 178
                $start = $open;
320
            }
321
322 178
            if ($depth !== 0 && ($open === false || $close === false)) {
323 1
                break;
324
            }
325
        }
326 178
        if ($depth !== 0) {
327 1
            return false;
328
        }
329
330 177
        return $tokens;
331
    }
332
333
    /**
334
     * Parses a token.
335
     * @param array $token the token to parse
336
     * @param array $args arguments to replace
337
     * @param string $locale the locale
338
     * @return bool|string parsed token or false on failure
339
     * @throws \yii\base\NotSupportedException when unsupported formatting is used.
340
     */
341 18
    private function parseToken($token, $args, $locale)
342
    {
343
        // parsing pattern based on ICU grammar:
344
        // https://unicode-org.github.io/icu-docs/#/icu4c/classMessageFormat.html
345 18
        $charset = Yii::$app ? Yii::$app->charset : 'UTF-8';
346 18
        $param = trim($token[0]);
347 18
        if (isset($args[$param])) {
348 16
            $arg = $args[$param];
349
        } else {
350 3
            return '{' . implode(',', $token) . '}';
351
        }
352 16
        $type = isset($token[1]) ? trim($token[1]) : 'none';
353
        switch ($type) {
354 16
            case 'date':
355 16
            case 'time':
356 16
            case 'spellout':
357 16
            case 'ordinal':
358 16
            case 'duration':
359 16
            case 'choice':
360 16
            case 'selectordinal':
361
                throw new NotSupportedException("Message format '$type' is not supported. You have to install PHP intl extension to use this feature.");
362 16
            case 'number':
363 9
                $format = isset($token[2]) ? trim($token[2]) : null;
364 9
                if (is_numeric($arg) && ($format === null || $format === 'integer')) {
365 7
                    $number = number_format($arg);
366 7
                    if ($format === null && ($pos = strpos($arg, '.')) !== false) {
367
                        // add decimals with unknown length
368 1
                        $number .= '.' . substr($arg, $pos + 1);
369
                    }
370
371 7
                    return $number;
372
                }
373 2
                throw new NotSupportedException("Message format 'number' is only supported for integer values. You have to install PHP intl extension to use this feature.");
374 10
            case 'none':
375 9
                return $arg;
376 6
            case 'select':
377
                /* https://unicode-org.github.io/icu-docs/#/icu4c/classicu_1_1SelectFormat.html
378
                selectStyle = (selector '{' message '}')+
379
                */
380 5
                if (!isset($token[2])) {
381
                    return false;
382
                }
383 5
                $select = self::tokenizePattern($token[2]);
384 5
                $c = count($select);
385 5
                $message = false;
386 5
                for ($i = 0; $i + 1 < $c; $i++) {
387 5
                    if (is_array($select[$i]) || !is_array($select[$i + 1])) {
388
                        return false;
389
                    }
390 5
                    $selector = trim($select[$i++]);
391 5
                    if ($message === false && $selector === 'other' || $selector == $arg) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($message === false && $...') || $selector == $arg, Probably Intended Meaning: $message === false && ($...' || $selector == $arg)
Loading history...
392 5
                        $message = implode(',', $select[$i]);
393
                    }
394
                }
395 5
                if ($message !== false) {
0 ignored issues
show
introduced by
The condition $message !== false is always false.
Loading history...
396 5
                    return $this->fallbackFormat($message, $args, $locale);
397
                }
398
                break;
399 2
            case 'plural':
400
                /* https://unicode-org.github.io/icu-docs/#/icu4c/classicu_1_1PluralFormat.html
401
                pluralStyle = [offsetValue] (selector '{' message '}')+
402
                offsetValue = "offset:" number
403
                selector = explicitValue | keyword
404
                explicitValue = '=' number  // adjacent, no white space in between
405
                keyword = [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+
406
                message: see MessageFormat
407
                */
408 2
                if (!isset($token[2])) {
409
                    return false;
410
                }
411 2
                $plural = self::tokenizePattern($token[2]);
412 2
                $c = count($plural);
413 2
                $message = false;
414 2
                $offset = 0;
415 2
                for ($i = 0; $i + 1 < $c; $i++) {
416 2
                    if (is_array($plural[$i]) || !is_array($plural[$i + 1])) {
417
                        return false;
418
                    }
419 2
                    $selector = trim($plural[$i++]);
420
421 2
                    if ($i == 1 && strncmp($selector, 'offset:', 7) === 0) {
422 1
                        $offset = (int) trim(mb_substr($selector, 7, ($pos = mb_strpos(str_replace(["\n", "\r", "\t"], ' ', $selector), ' ', 7, $charset)) - 7, $charset));
423 1
                        $selector = trim(mb_substr($selector, $pos + 1, mb_strlen($selector, $charset), $charset));
424
                    }
425
                    if (
426 2
                        $message === false && $selector === 'other' ||
427 2
                        strncmp($selector, '=', 1) === 0 && (int) mb_substr($selector, 1, mb_strlen($selector, $charset), $charset) === $arg ||
428 2
                        $selector === 'one' && $arg - $offset == 1
429
                    ) {
430 2
                        $message = implode(',', str_replace('#', $arg - $offset, $plural[$i]));
431
                    }
432
                }
433 2
                if ($message !== false) {
434 2
                    return $this->fallbackFormat($message, $args, $locale);
435
                }
436
                break;
437
        }
438
439
        return false;
440
    }
441
}
442