Completed
Push — remove-intl-polyfills ( 2e4797 )
by Alexander
19:18 queued 03:32
created

MessageFormatter   C

Complexity

Total Complexity 67

Size/Duplication

Total Lines 254
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 83.06%

Importance

Changes 0
Metric Value
wmc 67
lcom 1
cbo 3
dl 0
loc 254
ccs 103
cts 124
cp 0.8306
rs 5.7097
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A getErrorCode() 0 4 1
A getErrorMessage() 0 4 1
B format() 0 31 5
B fallbackFormat() 0 21 5
C tokenizePattern() 0 41 13
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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 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 Yii;
11
use yii\base\Component;
12
use yii\base\NotSupportedException;
13
14
/**
15
 * MessageFormatter allows formatting messages via [ICU message format](http://userguide.icu-project.org/formatparse/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
 * - Issues no error if format is invalid returning false and holding error for retrieval via `getErrorCode()`
22
 *   and `getErrorMessage()` methods.
23
 * - Issues no error when an insufficient number of arguments have been provided. Instead, the placeholders will not be
24
 *   substituted. It prevents translation mistakes to crash whole page.
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](http://php.net/manual/en/book.intl.php) if
27
 *   you want 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](http://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 string $errorCode Code of the last error. This property is read-only.
40
 * @property string $errorMessage Description of the last error. This property is read-only.
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 http://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 http://php.net/manual/en/messageformatter.geterrormessage.php
65
     * @return string Description of the last error.
66
     */
67 42
    public function getErrorMessage()
68
    {
69 42
        return $this->_errorMessage;
70
    }
71
72
    /**
73
     * Formats a message via [ICU message format](http://userguide.icu-project.org/formatparse/messages).
74
     *
75
     * It uses the PHP intl extension's [MessageFormatter](http://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 152
    public function format($pattern, $params, $language)
85
    {
86 152
        $this->_errorCode = 0;
87 152
        $this->_errorMessage = '';
88
89 152
        if ($params === []) {
90 4
            return $pattern;
91
        }
92
93 148
        if (!class_exists('MessageFormatter', false)) {
94
            return $this->fallbackFormat($pattern, $params, $language);
95
        }
96
97
        try {
98 148
            $formatter = new \MessageFormatter($language, $pattern);
99 2
        } catch (\IntlException $e) {
0 ignored issues
show
Bug introduced by
The class IntlException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
100 2
            $this->_errorCode = $e->getCode();
101 2
            $this->_errorMessage = 'Message pattern is invalid: ' . $e->getMessage();
102 2
            return false;
103
        }
104
105 146
        $result = $formatter->format($params);
106
107 146
        if ($result === false) {
108
            $this->_errorCode = $formatter->getErrorCode();
109
            $this->_errorMessage = $formatter->getErrorMessage();
110
            return false;
111
        }
112
113 146
        return $result;
114
    }
115
116
    /**
117
     * Fallback implementation for MessageFormatter::formatMessage.
118
     * @param string $pattern The pattern string to insert things into.
119
     * @param array $args The array of values to insert into the format string
120
     * @param string $locale The locale to use for formatting locale-dependent parts
121
     * @return false|string The formatted pattern string or `false` if an error occurred
122
     */
123 18
    protected function fallbackFormat($pattern, $args, $locale)
124
    {
125 18
        if (($tokens = self::tokenizePattern($pattern)) === false) {
126
            $this->_errorCode = -1;
127
            $this->_errorMessage = 'Message pattern is invalid.';
128
129
            return false;
130
        }
131 18
        foreach ($tokens as $i => $token) {
132 18
            if (is_array($token)) {
133 18
                if (($tokens[$i] = $this->parseToken($token, $args, $locale)) === false) {
134
                    $this->_errorCode = -1;
135
                    $this->_errorMessage = 'Message pattern is invalid.';
136
137 18
                    return false;
138
                }
139
            }
140
        }
141
142 16
        return implode('', $tokens);
143
    }
144
145
    /**
146
     * Tokenizes a pattern by separating normal text from replaceable patterns.
147
     * @param string $pattern patter to tokenize
148
     * @return array|bool array of tokens or false on failure
149
     */
150 18
    private static function tokenizePattern($pattern)
151
    {
152 18
        $charset = Yii::$app ? Yii::$app->charset : 'UTF-8';
153 18
        $depth = 1;
154 18
        if (($start = $pos = mb_strpos($pattern, '{', 0, $charset)) === false) {
155 3
            return [$pattern];
156
        }
157 18
        $tokens = [mb_substr($pattern, 0, $pos, $charset)];
158 18
        while (true) {
159 18
            $open = mb_strpos($pattern, '{', $pos + 1, $charset);
160 18
            $close = mb_strpos($pattern, '}', $pos + 1, $charset);
161 18
            if ($open === false && $close === false) {
162 18
                break;
163
            }
164 18
            if ($open === false) {
165 18
                $open = mb_strlen($pattern, $charset);
166
            }
167 18
            if ($close > $open) {
168 11
                $depth++;
169 11
                $pos = $open;
170
            } else {
171 18
                $depth--;
172 18
                $pos = $close;
173
            }
174 18
            if ($depth === 0) {
175 18
                $tokens[] = explode(',', mb_substr($pattern, $start + 1, $pos - $start - 1, $charset), 3);
176 18
                $start = $pos + 1;
177 18
                $tokens[] = mb_substr($pattern, $start, $open - $start, $charset);
178 18
                $start = $open;
179
            }
180
181 18
            if ($depth !== 0 && ($open === false || $close === false)) {
182
                break;
183
            }
184
        }
185 18
        if ($depth !== 0) {
186
            return false;
187
        }
188
189 18
        return $tokens;
190
    }
191
192
    /**
193
     * Parses a token.
194
     * @param array $token the token to parse
195
     * @param array $args arguments to replace
196
     * @param string $locale the locale
197
     * @return bool|string parsed token or false on failure
198
     * @throws \yii\base\NotSupportedException when unsupported formatting is used.
199
     */
200 18
    private function parseToken($token, $args, $locale)
201
    {
202
        // parsing pattern based on ICU grammar:
203
        // http://icu-project.org/apiref/icu4c/classMessageFormat.html#details
204 18
        $charset = Yii::$app ? Yii::$app->charset : 'UTF-8';
205 18
        $param = trim($token[0]);
206 18
        if (isset($args[$param])) {
207 16
            $arg = $args[$param];
208
        } else {
209 3
            return '{' . implode(',', $token) . '}';
210
        }
211 16
        $type = isset($token[1]) ? trim($token[1]) : 'none';
212
        switch ($type) {
213 16
            case 'date':
214 16
            case 'time':
215 16
            case 'spellout':
216 16
            case 'ordinal':
217 16
            case 'duration':
218 16
            case 'choice':
219 16
            case 'selectordinal':
220
                throw new NotSupportedException("Message format '$type' is not supported. You have to install PHP intl extension to use this feature.");
221 16
            case 'number':
222 9
                $format = isset($token[2]) ? trim($token[2]) : null;
223 9
                if (is_numeric($arg) && ($format === null || $format === 'integer')) {
224 7
                    $number = number_format($arg);
225 7
                    if ($format === null && ($pos = strpos($arg, '.')) !== false) {
226
                        // add decimals with unknown length
227 1
                        $number .= '.' . substr($arg, $pos + 1);
228
                    }
229
230 7
                    return $number;
231
                }
232 2
                throw new NotSupportedException("Message format 'number' is only supported for integer values. You have to install PHP intl extension to use this feature.");
233 10
            case 'none':
234 9
                return $arg;
235 6
            case 'select':
236
                /* http://icu-project.org/apiref/icu4c/classicu_1_1SelectFormat.html
237
                selectStyle = (selector '{' message '}')+
238
                */
239 5
                if (!isset($token[2])) {
240
                    return false;
241
                }
242 5
                $select = self::tokenizePattern($token[2]);
243 5
                $c = count($select);
244 5
                $message = false;
245 5
                for ($i = 0; $i + 1 < $c; $i++) {
246 5
                    if (is_array($select[$i]) || !is_array($select[$i + 1])) {
247
                        return false;
248
                    }
249 5
                    $selector = trim($select[$i++]);
250 5
                    if ($message === false && $selector === 'other' || $selector == $arg) {
251 5
                        $message = implode(',', $select[$i]);
252
                    }
253
                }
254 5
                if ($message !== false) {
255 5
                    return $this->fallbackFormat($message, $args, $locale);
256
                }
257
                break;
258 2
            case 'plural':
259
                /* http://icu-project.org/apiref/icu4c/classicu_1_1PluralFormat.html
260
                pluralStyle = [offsetValue] (selector '{' message '}')+
261
                offsetValue = "offset:" number
262
                selector = explicitValue | keyword
263
                explicitValue = '=' number  // adjacent, no white space in between
264
                keyword = [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+
265
                message: see MessageFormat
266
                */
267 2
                if (!isset($token[2])) {
268
                    return false;
269
                }
270 2
                $plural = self::tokenizePattern($token[2]);
271 2
                $c = count($plural);
272 2
                $message = false;
273 2
                $offset = 0;
274 2
                for ($i = 0; $i + 1 < $c; $i++) {
275 2
                    if (is_array($plural[$i]) || !is_array($plural[$i + 1])) {
276
                        return false;
277
                    }
278 2
                    $selector = trim($plural[$i++]);
279
280 2
                    if ($i == 1 && strncmp($selector, 'offset:', 7) === 0) {
281 1
                        $offset = (int) trim(mb_substr($selector, 7, ($pos = mb_strpos(str_replace(["\n", "\r", "\t"], ' ', $selector), ' ', 7, $charset)) - 7, $charset));
282 1
                        $selector = trim(mb_substr($selector, $pos + 1, mb_strlen($selector, $charset), $charset));
283
                    }
284 2
                    if ($message === false && $selector === 'other' ||
285 2
                        $selector[0] === '=' && (int) mb_substr($selector, 1, mb_strlen($selector, $charset), $charset) === $arg ||
286 2
                        $selector === 'one' && $arg - $offset == 1
287
                    ) {
288 2
                        $message = implode(',', str_replace('#', $arg - $offset, $plural[$i]));
289
                    }
290
                }
291 2
                if ($message !== false) {
292 2
                    return $this->fallbackFormat($message, $args, $locale);
293
                }
294
                break;
295
        }
296
297
        return false;
298
    }
299
}
300