Passed
Pull Request — 4 (#10222)
by Steve
07:01
created

MoneyField   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 333
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 125
dl 0
loc 333
rs 8.72
c 0
b 0
f 0
wmc 46

20 Methods

Rating   Name   Duplication   Size   Complexity  
A getAmountField() 0 3 1
A __clone() 0 4 1
A setLocale() 0 4 1
A Value() 0 4 1
A dataValue() 0 4 1
A getLocale() 0 3 1
A getAllowedCurrencies() 0 3 1
A getCurrencyField() 0 3 1
A performReadonlyTransformation() 0 5 1
A setSubmittedValue() 0 21 3
A setForm() 0 5 1
A getDBMoney() 0 7 1
A saveInto() 0 11 2
A setDisabled() 0 8 1
B setValue() 0 33 7
A setReadonly() 0 8 1
A __construct() 0 11 1
B buildCurrencyField() 0 34 7
B setAllowedCurrencies() 0 19 7
A validate() 0 19 6

How to fix   Complexity   

Complex Class

Complex classes like MoneyField 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 MoneyField, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use InvalidArgumentException;
6
use SilverStripe\ORM\ArrayLib;
7
use SilverStripe\ORM\FieldType\DBMoney;
8
use SilverStripe\ORM\DataObjectInterface;
9
10
/**
11
 * A form field that can save into a {@link Money} database field.
12
 * See {@link CurrencyField} for a similar implementation
13
 * that can save into a single float database field without indicating the currency.
14
 *
15
 * @author Ingo Schommer, SilverStripe Ltd. (<firstname>@silverstripe.com)
16
 */
17
class MoneyField extends FormField
18
{
19
20
    // TODO replace with `FormField::SCHEMA_DATA_TYPE_TEXT` when MoneyField is implemented
21
    /** @skipUpgrade */
22
    protected $schemaDataType = 'MoneyField';
23
24
    /**
25
     * Limit the currencies
26
     *
27
     * @var array
28
     */
29
    protected $allowedCurrencies = [];
30
31
    /**
32
     * @var NumericField
33
     */
34
    protected $fieldAmount = null;
35
36
    /**
37
     * @var FormField
38
     */
39
    protected $fieldCurrency = null;
40
41
    /**
42
     * Gets field for the currency selector
43
     *
44
     * @return FormField
45
     */
46
    public function getCurrencyField()
47
    {
48
        return $this->fieldCurrency;
49
    }
50
51
    /**
52
     * Gets field for the amount input
53
     *
54
     * @return NumericField
55
     */
56
    public function getAmountField()
57
    {
58
        return $this->fieldAmount;
59
    }
60
61
    public function __construct($name, $title = null, $value = "")
62
    {
63
        $this->setName($name);
64
        $this->fieldAmount = NumericField::create(
65
            "{$name}[Amount]",
66
            _t('SilverStripe\\Forms\\MoneyField.FIELDLABELAMOUNT', 'Amount')
67
        )
68
            ->setScale(2);
69
        $this->buildCurrencyField();
70
71
        parent::__construct($name, $title, $value);
72
    }
73
74
    public function __clone()
75
    {
76
        $this->fieldAmount = clone $this->fieldAmount;
77
        $this->fieldCurrency = clone $this->fieldCurrency;
78
    }
79
80
    /**
81
     * Builds a new currency field based on the allowed currencies configured
82
     *
83
     * @return FormField
84
     */
85
    protected function buildCurrencyField()
86
    {
87
        $name = $this->getName();
88
89
        // Validate allowed currencies
90
        $currencyValue = $this->fieldCurrency ? $this->fieldCurrency->dataValue() : null;
91
        $allowedCurrencies = $this->getAllowedCurrencies();
92
        if (count($allowedCurrencies ?: []) === 1) {
93
            // Hidden field for single currency
94
            $field = HiddenField::create("{$name}[Currency]");
95
            reset($allowedCurrencies);
96
            $currencyValue = key($allowedCurrencies ?: []);
97
        } elseif ($allowedCurrencies) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allowedCurrencies of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
98
            // Dropdown field for multiple currencies
99
            $field = DropdownField::create(
100
                "{$name}[Currency]",
101
                _t('SilverStripe\\Forms\\MoneyField.FIELDLABELCURRENCY', 'Currency'),
102
                $allowedCurrencies
103
            );
104
        } else {
105
            // Free-text entry for currency value
106
            $field = TextField::create(
107
                "{$name}[Currency]",
108
                _t('SilverStripe\\Forms\\MoneyField.FIELDLABELCURRENCY', 'Currency')
109
            );
110
        }
111
112
        $field->setReadonly($this->isReadonly());
113
        $field->setDisabled($this->isDisabled());
114
        if ($currencyValue) {
115
            $field->setValue($currencyValue);
116
        }
117
        $this->fieldCurrency = $field;
118
        return $field;
119
    }
120
121
    public function setSubmittedValue($value, $data = null)
122
    {
123
        if (empty($value)) {
124
            $this->value = null;
125
            $this->fieldCurrency->setValue(null);
126
            $this->fieldAmount->setValue(null);
127
            return $this;
128
        }
129
130
        // Handle submitted array value
131
        if (!is_array($value)) {
132
            throw new InvalidArgumentException("Value is not submitted array");
133
        }
134
135
        // Update each field
136
        $this->fieldCurrency->setSubmittedValue($value['Currency'], $value);
137
        $this->fieldAmount->setSubmittedValue($value['Amount'], $value);
138
139
        // Get data value
140
        $this->value = $this->dataValue();
141
        return $this;
142
    }
143
144
    public function setValue($value, $data = null)
145
    {
146
        if (empty($value)) {
147
            $this->value = null;
148
            $this->fieldCurrency->setValue(null);
149
            $this->fieldAmount->setValue(null);
150
            return $this;
151
        }
152
153
        // Convert string to array
154
        // E.g. `44.00 NZD`
155
        if (is_string($value) &&
156
            preg_match('/^(?<amount>[\\d\\.]+)( (?<currency>\w{3}))?$/i', (string) $value, $matches)
157
        ) {
158
            $currency = isset($matches['currency']) ? strtoupper($matches['currency']) : null;
159
            $value = [
160
                'Currency' => $currency,
161
                'Amount' => (float)$matches['amount'],
162
            ];
163
        } elseif ($value instanceof DBMoney) {
164
            $value = [
165
                'Currency' => $value->getCurrency(),
166
                'Amount' => $value->getAmount(),
167
            ];
168
        } elseif (!is_array($value)) {
169
            throw new InvalidArgumentException("Invalid currency format");
170
        }
171
172
        // Save value
173
        $this->fieldCurrency->setValue($value['Currency'], $value);
174
        $this->fieldAmount->setValue($value['Amount'], $value);
175
        $this->value = $this->dataValue();
176
        return $this;
177
    }
178
179
    /**
180
     * Get value as DBMoney object useful for formatting the number
181
     *
182
     * @return DBMoney
183
     */
184
    protected function getDBMoney()
185
    {
186
        return DBMoney::create_field('Money', [
187
            'Currency' => $this->fieldCurrency->dataValue(),
188
            'Amount' => $this->fieldAmount->dataValue()
189
        ])
190
            ->setLocale($this->getLocale());
0 ignored issues
show
Bug introduced by
The method setLocale() does not exist on SilverStripe\ORM\FieldType\DBField. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

190
            ->/** @scrutinizer ignore-call */ setLocale($this->getLocale());
Loading history...
191
    }
192
193
    public function dataValue()
194
    {
195
        // Non-localised money
196
        return $this->getDBMoney()->getValue();
197
    }
198
199
    public function Value()
200
    {
201
        // Localised money
202
        return $this->getDBMoney()->Nice();
203
    }
204
205
    /**
206
     * 30/06/2009 - Enhancement:
207
     * SaveInto checks if set-methods are available and use them
208
     * instead of setting the values in the money class directly. saveInto
209
     * initiates a new Money class object to pass through the values to the setter
210
     * method.
211
     *
212
     * (see @link MoneyFieldTest_CustomSetter_Object for more information)
213
     *
214
     * @param DataObjectInterface|Object $dataObject
215
     */
216
    public function saveInto(DataObjectInterface $dataObject)
217
    {
218
        $fieldName = $this->getName();
219
        if ($dataObject->hasMethod("set$fieldName")) {
0 ignored issues
show
Bug introduced by
The method hasMethod() does not exist on SilverStripe\ORM\DataObjectInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to SilverStripe\ORM\DataObjectInterface. ( Ignorable by Annotation )

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

219
        if ($dataObject->/** @scrutinizer ignore-call */ hasMethod("set$fieldName")) {
Loading history...
220
            $dataObject->$fieldName = $this->getDBMoney();
221
        } else {
222
            $currencyField = "{$fieldName}Currency";
223
            $amountField = "{$fieldName}Amount";
224
225
            $dataObject->$currencyField = $this->fieldCurrency->dataValue();
226
            $dataObject->$amountField = $this->fieldAmount->dataValue();
227
        }
228
    }
229
230
    /**
231
     * Returns a readonly version of this field.
232
     */
233
    public function performReadonlyTransformation()
234
    {
235
        $clone = clone $this;
236
        $clone->setReadonly(true);
237
        return $clone;
238
    }
239
240
    public function setReadonly($bool)
241
    {
242
        parent::setReadonly($bool);
243
244
        $this->fieldAmount->setReadonly($bool);
245
        $this->fieldCurrency->setReadonly($bool);
246
247
        return $this;
248
    }
249
250
    public function setDisabled($bool)
251
    {
252
        parent::setDisabled($bool);
253
254
        $this->fieldAmount->setDisabled($bool);
255
        $this->fieldCurrency->setDisabled($bool);
256
257
        return $this;
258
    }
259
260
    /**
261
     * Set list of currencies. Currencies should be in the 3-letter ISO 4217 currency code.
262
     *
263
     * @param array $currencies
264
     * @return $this
265
     */
266
    public function setAllowedCurrencies($currencies)
267
    {
268
        if (empty($currencies)) {
269
            $currencies = [];
270
        } elseif (is_string($currencies)) {
0 ignored issues
show
introduced by
The condition is_string($currencies) is always false.
Loading history...
271
            $currencies = [
272
                $currencies => $currencies
273
            ];
274
        } elseif (!is_array($currencies)) {
0 ignored issues
show
introduced by
The condition is_array($currencies) is always true.
Loading history...
275
            throw new InvalidArgumentException("Invalid currency list");
276
        } elseif (!ArrayLib::is_associative($currencies)) {
277
            $currencies = array_combine($currencies ?: [], $currencies ?: []);
278
        }
279
280
        $this->allowedCurrencies = $currencies;
281
282
        // Rebuild currency field
283
        $this->buildCurrencyField();
284
        return $this;
285
    }
286
287
    /**
288
     * @return array
289
     */
290
    public function getAllowedCurrencies()
291
    {
292
        return $this->allowedCurrencies;
293
    }
294
295
    /**
296
     * Assign locale to format this currency in
297
     *
298
     * @param string $locale
299
     * @return $this
300
     */
301
    public function setLocale($locale)
302
    {
303
        $this->fieldAmount->setLocale($locale);
304
        return $this;
305
    }
306
307
    /**
308
     * Get locale to format this currency in.
309
     * Defaults to current locale.
310
     *
311
     * @return string
312
     */
313
    public function getLocale()
314
    {
315
        return $this->fieldAmount->getLocale();
316
    }
317
318
    /**
319
     * Validate this field
320
     *
321
     * @param Validator $validator
322
     * @return bool
323
     */
324
    public function validate($validator)
325
    {
326
        // Validate currency
327
        $currencies = $this->getAllowedCurrencies();
328
        $currency = $this->fieldCurrency->dataValue();
329
        if ($currency && $currencies && !in_array($currency, $currencies ?: [])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $currencies of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
330
            $validator->validationError(
331
                $this->getName(),
332
                _t(
333
                    __CLASS__ . '.INVALID_CURRENCY',
334
                    'Currency {currency} is not in the list of allowed currencies',
335
                    ['currency' => $currency]
336
                )
337
            );
338
            return false;
339
        }
340
341
        // Field-specific validation
342
        return $this->fieldAmount->validate($validator) && $this->fieldCurrency->validate($validator);
343
    }
344
345
    public function setForm($form)
346
    {
347
        $this->fieldCurrency->setForm($form);
348
        $this->fieldAmount->setForm($form);
349
        return parent::setForm($form);
350
    }
351
}
352