LocalizedNumberTransformer::transformFromHttp()   B
last analyzed

Complexity

Conditions 10
Paths 8

Size

Total Lines 30
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 10

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 16
c 1
b 0
f 1
dl 0
loc 30
ccs 16
cts 16
cp 1
rs 7.6666
cc 10
nc 8
nop 2
crap 10

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Bdf\Form\Leaf\Transformer;
4
5
use Attribute;
6
use Bdf\Form\ElementInterface;
7
use Bdf\Form\Transformer\TransformerInterface;
8
use InvalidArgumentException;
9
use Locale;
10
use NumberFormatter;
11
12
/**
13
 * Transformer localized string number to native PHP number (int or double)
14
 *
15
 * Inspired from : https://github.com/symfony/symfony/blob/5.x/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php
16
 *
17
 * @template T as numeric
18
 */
19
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
20
class LocalizedNumberTransformer implements TransformerInterface
21
{
22
    /**
23
     * Number of digit to keep after the comma
24
     *
25
     * @var int|null
26
     */
27
    private $scale;
28
29
    /**
30
     * @var int
31
     * @psalm-var NumberFormatter::ROUND_*
32
     */
33
    private $roundingMode;
34
35
    /**
36
     * Group by thousand or not
37
     *
38
     * @var bool
39
     */
40
    private $grouping;
41
42
    /**
43
     * The locale to use
44
     * null for use the current locale
45
     *
46
     * @var string|null
47
     */
48
    private $locale;
49
50
    /**
51
     * LocalizedNumberTransformer constructor.
52
     *
53
     * @param int|null $scale Number of digit to keep after the comma. Null to keep all digits (do not round)
54
     * @param bool $grouping Group by thousand or not
55
     * @param NumberFormatter::ROUND_* $roundingMode
56
     * @param string|null $locale The locale to use. null for use the current locale
57
     */
58 205
    public function __construct(?int $scale = null, bool $grouping = false, int $roundingMode = NumberFormatter::ROUND_HALFUP, ?string $locale = null)
59
    {
60 205
        $this->scale = $scale;
61 205
        $this->grouping = $grouping;
62 205
        $this->roundingMode = $roundingMode;
63 205
        $this->locale = $locale;
64 205
    }
65
66
    /**
67
     * {@inheritdoc}
68
     *
69
     * @throws InvalidArgumentException If the given value is not numeric or cannot be formatted
70
     */
71 58
    final public function transformToHttp($value, ElementInterface $input): ?string
72
    {
73 58
        if ($value === null) {
74 7
            return null;
75
        }
76
77 52
        if (!is_numeric($value)) {
78 8
            throw new InvalidArgumentException('Expected a numeric or null.');
79
        }
80
81 44
        $formatter = $this->getNumberFormatter();
82 44
        $value = $formatter->format($value);
83
84 44
        $this->checkError($formatter);
85
86 44
        return $value;
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     *
92
     * @return T|null The numeric value
0 ignored issues
show
Bug introduced by
The type Bdf\Form\Leaf\Transformer\T was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
93
     *
94
     * @throws InvalidArgumentException If the given value is not scalar or cannot be parsed
95
     */
96 135
    final public function transformFromHttp($value, ElementInterface $input)
97
    {
98 135
        if ($value !== null && !is_int($value) && !is_float($value) && !is_string($value)) {
99 6
            throw new InvalidArgumentException('Expected a scalar or null.');
100
        }
101
102 129
        if ($value === null || $value === '') {
103 12
            return null;
104
        }
105
106 117
        $value = (string) $value;
107
108 117
        $formatter = $this->getNumberFormatter();
109 117
        $decSep = $formatter->getSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
110
111
        // Normalize "standard" decimal format to locale format
112 117
        if ($decSep !== '.') {
113 39
            $value = str_replace('.', $decSep, $value);
114
        }
115
116 117
        if (str_contains($value, $decSep)) {
117 50
            $type = NumberFormatter::TYPE_DOUBLE;
118
        } else {
119 70
            $type = PHP_INT_SIZE === 8 ? NumberFormatter::TYPE_INT64 : NumberFormatter::TYPE_INT32;
120
        }
121
122 117
        $result = $formatter->parse($value, $type);
123 117
        $this->checkError($formatter);
124
125 111
        return $this->cast($this->round($result));
126
    }
127
128
    /**
129
     * Create the NumberFormatter instance
130
     *
131
     * @return NumberFormatter
132
     */
133 147
    private function getNumberFormatter(): NumberFormatter
134
    {
135 147
        $formatter = new NumberFormatter($this->locale ?? Locale::getDefault(), NumberFormatter::DECIMAL);
136
137 147
        if (null !== $this->scale) {
138 119
            $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $this->scale);
139 119
            $formatter->setAttribute(NumberFormatter::ROUNDING_MODE, $this->roundingMode);
140
        }
141
142 147
        $formatter->setAttribute(NumberFormatter::GROUPING_USED, $this->grouping);
0 ignored issues
show
Bug introduced by
$this->grouping of type boolean is incompatible with the type integer expected by parameter $value of NumberFormatter::setAttribute(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

142
        $formatter->setAttribute(NumberFormatter::GROUPING_USED, /** @scrutinizer ignore-type */ $this->grouping);
Loading history...
143
144 147
        return $formatter;
145
    }
146
147
    /**
148
     * Cast the number to the desired type
149
     *
150
     * @return T
151
     */
152 48
    protected function cast($value)
153
    {
154 48
        return $value;
155
    }
156
157
    /**
158
     * Rounds a number according to the configured scale and rounding mode
159
     *
160
     * @param int|float $number A number
161
     *
162
     * @return int|float The rounded number
163
     */
164 111
    private function round($number)
165
    {
166 111
        if (is_int($number) || $this->scale === null) {
167 76
            return $number;
168
        }
169
170 35
        switch ($this->roundingMode) {
171
            case NumberFormatter::ROUND_HALFEVEN:
172 6
                return round($number, $this->scale, PHP_ROUND_HALF_EVEN);
173
            case NumberFormatter::ROUND_HALFUP:
174 5
                return round($number, $this->scale, PHP_ROUND_HALF_UP);
175
            case NumberFormatter::ROUND_HALFDOWN:
176 4
                return round($number, $this->scale, PHP_ROUND_HALF_DOWN);
177
        }
178
179 20
        $coef = 10 ** $this->scale;
180 20
        $number *= $coef;
181
182 20
        switch ($this->roundingMode) {
183
            case NumberFormatter::ROUND_CEILING:
184 4
                $number = ceil($number);
185 4
                break;
186
            case NumberFormatter::ROUND_FLOOR:
187 4
                $number = floor($number);
188 4
                break;
189
            case NumberFormatter::ROUND_UP:
190 6
                $number = $number > 0 ? ceil($number) : floor($number);
191 6
                break;
192
            case NumberFormatter::ROUND_DOWN:
193 6
                $number = $number > 0 ? floor($number) : ceil($number);
194 6
                break;
195
        }
196
197 20
        return $number / $coef;
198
    }
199
200
    /**
201
     * Check if the formatter is in error state, and throw exception
202
     *
203
     * @param NumberFormatter $formatter
204
     * @throws InvalidArgumentException If the formatter has an error
205
     */
206 147
    private function checkError(NumberFormatter $formatter): void
207
    {
208 147
        if (intl_is_failure($formatter->getErrorCode())) {
209 6
            throw new InvalidArgumentException($formatter->getErrorMessage());
210
        }
211 141
    }
212
}
213