Completed
Branch FET-10619-money-entity (c56734)
by
unknown
157:04 queued 143:03
created

Money::subtract()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 11
Ratio 100 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 1
dl 11
loc 11
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace EventEspresso\core\entities\money;
4
5
use EE_Error;
6
use EE_Organization_Config;
7
use EE_Registry;
8
use EventEspresso\core\exceptions\InvalidDataTypeException;
9
use EventEspresso\core\services\currency\Calculator;
10
use EventEspresso\core\services\currency\CurrencySignMoneyFormatter;
11
use EventEspresso\core\services\currency\DecimalMoneyFormatter;
12
use EventEspresso\core\services\currency\InternationalMoneyFormatter;
13
use EventEspresso\core\services\currency\MoneyFormatter;
14
use EventEspresso\core\services\currency\ThousandsMoneyFormatter;
15
use InvalidArgumentException;
16
17
defined('EVENT_ESPRESSO_VERSION') || exit;
18
19
20
21
/**
22
 * Class Money
23
 * Immutable object representing an amount of money for a particular currency
24
 *
25
 * @package       Event Espresso
26
 * @author        Brent Christensen
27
 * @since         $VID:$
28
 */
29
class Money
30
{
31
32
    /**
33
     * number of decimal places to be added to currencies for internal computations,
34
     * but removed before any output or formatting is applied.
35
     * This allows us to avoid rounding errors during calculations.
36
     */
37
    const EXTRA_PRECISION = 3;
38
39
    /**
40
     * @var int $amount
41
     */
42
    private $amount;
43
44
    /**
45
     * @var Currency $currency
46
     */
47
    private $currency;
48
49
    /**
50
     * @var Calculator $calculator
51
     */
52
    protected static $calculator;
53
54
    /**
55
     * @var MoneyFormatter[] $formatters
56
     */
57
    protected static $formatters;
58
59
60
61
    /**
62
     * Money constructor.
63
     *
64
     * @param float|int|string $amount money amount IN THE STANDARD UNIT FOR THE CURRENCY ie: dollars, Euros, etc
65
     *                                 example: $12.5 USD would equate to a value amount of 12.50
66
     * @param Currency         $currency
67
     * @throws InvalidDataTypeException
68
     */
69
    public function __construct($amount, Currency $currency)
70
    {
71
        $this->currency = $currency;
72
        $this->amount = (string)$this->parseAmount($amount);
0 ignored issues
show
Documentation Bug introduced by
The property $amount was declared of type integer, but (string) $this->parseAmount($amount) is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
73
    }
74
75
76
77
    /**
78
     * factory method that returns a Money object using the currency corresponding to the site's country
79
     * example: Money::forSite(12.5)
80
     *
81
     * @param float|int|string $amount money amount IN THE STANDARD UNIT FOR THE CURRENCY ie: dollars, Euros, etc
82
     *                                 example: $12.5 USD would equate to a value amount of 12.50
83
     * @return Money
84
     * @throws InvalidArgumentException
85
     */
86
    public static function forSite($amount)
87
    {
88
        $CNT_ISO = isset(EE_Registry::instance()->CFG->organization)
89
                   && EE_Registry::instance()->CFG->organization instanceof EE_Organization_Config
90
            ? EE_Registry::instance()->CFG->organization->CNT_ISO
91
            : 'US';
92
        return new self($amount, Currency::createFromCountryCode($CNT_ISO));
93
    }
94
95
96
97
    /**
98
     * factory method that returns a Money object using the currency as specified by the supplied ISO country code
99
     * example: Money::forCountry(12.5,'US')
100
     *
101
     * @param float|int|string $amount money amount IN THE STANDARD UNIT FOR THE CURRENCY ie: dollars, Euros, etc
102
     *                                 example: $12.5 USD would equate to a value amount of 12.50
103
     * @param string           $CNT_ISO
104
     * @return Money
105
     * @throws InvalidArgumentException
106
     */
107
    public static function forCountry($amount, $CNT_ISO)
108
    {
109
        return new self($amount, Currency::createFromCountryCode($CNT_ISO));
110
    }
111
112
113
114
    /**
115
     * factory method that returns a Money object using the currency as specified by the supplied currency code
116
     * example: Money::forCurrency(12.5, 'USD')
117
     *
118
     * @param float|int|string $amount money amount IN THE STANDARD UNIT FOR THE CURRENCY ie: dollars, Euros, etc
119
     *                                 example: $12.5 USD would equate to a value amount of 12.50
120
     * @param string           $currency_code
121
     * @return Money
122
     * @throws InvalidDataTypeException
123
     * @throws EE_Error
124
     * @throws InvalidArgumentException
125
     */
126
    public static function forCurrency($amount, $currency_code)
127
    {
128
        return new self($amount, Currency::createFromCode($currency_code));
129
    }
130
131
132
133
    /**
134
     * factory method that returns a Money object for the currency specified as if it were a class method
135
     * example: Money::USD(12.5);
136
     * money amount IN THE STANDARD UNIT FOR THE CURRENCY ie: dollars, Euros, etc
137
     * example: $12.5 USD would equate to a value amount of 12.50
138
     *
139
     * @param string $currency_code
140
     * @param array  $arguments
141
     * @return Money
142
     * @throws InvalidDataTypeException
143
     * @throws EE_Error
144
     * @throws InvalidArgumentException
145
     */
146
    public static function __callStatic($currency_code, $arguments)
147
    {
148
        return new self($arguments[0], Currency::createFromCode($currency_code));
149
    }
150
151
152
153
    /**
154
     * @param float|int|string $amount money amount IN THE STANDARD UNIT FOR THE CURRENCY ie: dollars, Euros, etc
155
     *                                 example: $12.5 USD would equate to a value amount of 12.50
156
     * @return float|int|number|string
157
     * @throws InvalidDataTypeException
158
     */
159
    private function parseAmount($amount)
160
    {
161
        $type = gettype($amount);
162
        switch ($type) {
163
            case 'integer' :
164
            case 'double' :
165
            case 'string' :
166
                // shift the decimal position by the number of decimal places used internally
167
                // ex: 12.5 for a currency using 2 decimal places, would become 1250
168
                // then if our extra internal precision was 3, it would become 1250000
169
                $amount *= pow(10, $this->precision());
170
                // then round up the remaining value if there is still a fractional amount left
171
                $amount = round($amount, 0, PHP_ROUND_HALF_UP);
172
            break;
173
            default  :
174
                throw new InvalidDataTypeException(
175
                    '$amount',
176
                    $amount,
177
                    'integer (or float or string)'
178
                );
179
        }
180
        return $amount;
181
    }
182
183
184
185
    /**
186
     * adds or subtracts additional decimal places based on the value of the Money::EXTRA_PRECISION constant
187
     *
188
     * @param bool $positive
189
     * @return int
190
     */
191
    private function precision($positive = true)
192
    {
193
        $sign = $positive ? 1 : -1;
194
        return ((int)$this->currency->decimalPlaces() + Money::EXTRA_PRECISION) * $sign;
195
    }
196
197
198
199
    /**
200
     * Returns the money amount as an unformatted string
201
     * IF YOU REQUIRE A FORMATTED STRING, THEN USE Money::format()
202
     *
203
     * @return string
204
     */
205
    public function amount()
206
    {
207
        // shift the decimal position BACK by the number of decimal places used internally
208
        // ex: 1250 for a currency using 2 decimal places, would become 12.5
209
        $amount = (string)$this->amount * pow(10, $this->precision(false));
210
        // then shave off our extra internal precision using the number of decimal places for the currency
211
        $amount = round($amount, $this->currency->decimalPlaces());
212
        return $amount;
213
    }
214
215
216
217
    /**
218
     * applies formatting based on the specified formatting level
219
     * corresponding to one of the constants on \EventEspresso\core\services\currency\MoneyFormatter
220
     *
221
     * @param int $formatting_level
222
     * @return string
223
     */
224
    public function format($formatting_level = MoneyFormatter::ADD_THOUSANDS)
225
    {
226
        $formatted_amount = $this->amount();
227
        $formatters = $this->formatters();
228
        // if we are applying thousands formatting...
229
        if ($formatting_level >= MoneyFormatter::ADD_THOUSANDS) {
230
            // then let's remove decimal formatting since it's included in thousands formatting
231
            unset($formatters[MoneyFormatter::DECIMAL_ONLY]);
232
        }
233
        for ($x = 1; $x <= $formatting_level; $x++) {
234
            if (isset($formatters[$x]) && $formatters[$x] instanceof MoneyFormatter) {
235
                $formatted_amount = $formatters[$x]->format($formatted_amount, $this->currency);
236
            }
237
        }
238
        return $formatted_amount;
239
    }
240
241
242
243
    /**
244
     * Returns the Currency object for this money
245
     *
246
     * @return Currency
247
     */
248
    public function currency()
249
    {
250
        return $this->currency;
251
    }
252
253
254
255
    /**
256
     * adds the supplied Money amount to this Money amount
257
     * and returns a new Money object
258
     *
259
     * @param Money $other
260
     * @return Money
261
     * @throws InvalidArgumentException
262
     */
263 View Code Duplication
    public function add(Money $other)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
264
    {
265
        $this->verifySameCurrency($other->currency());
266
        return new Money(
267
            $this->calculator()->add(
268
                $this->amount(),
269
                $other->amount()
270
            ),
271
            $this->currency()
272
        );
273
    }
274
275
276
277
    /**
278
     * subtracts the supplied Money amount from this Money amount
279
     * and returns a new Money object
280
     *
281
     * @param Money $other
282
     * @return Money
283
     * @throws InvalidArgumentException
284
     */
285 View Code Duplication
    public function subtract(Money $other)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
286
    {
287
        $this->verifySameCurrency($other->currency());
288
        return new Money(
289
            $this->calculator()->subtract(
290
                $this->amount(),
291
                $other->amount()
292
            ),
293
            $this->currency()
294
        );
295
    }
296
297
298
299
    /**
300
     * multiplies this Money amount by the supplied $multiplier
301
     * and returns a new Money object
302
     *
303
     * @param float|int|string $multiplier
304
     * @param int              $rounding_mode
305
     * @return Money
306
     * @throws InvalidDataTypeException
307
     */
308 View Code Duplication
    public function multiply($multiplier, $rounding_mode = Calculator::ROUND_HALF_UP)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
309
    {
310
        return new Money(
311
            $this->calculator()->multiply(
312
                $this->amount(),
313
                $multiplier,
314
                $this->precision(),
315
                $rounding_mode
316
            ),
317
            $this->currency()
318
        );
319
    }
320
321
322
323
    /**
324
     * divides this Money amount by the supplied $divisor
325
     * and returns a new Money object
326
     *
327
     * @param float|int|string $divisor
328
     * @param int              $rounding_mode
329
     * @return Money
330
     * @throws InvalidDataTypeException
331
     */
332 View Code Duplication
    public function divide($divisor, $rounding_mode = Calculator::ROUND_HALF_UP)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
333
    {
334
        return new Money(
335
            $this->calculator()->divide(
336
                $this->amount(),
337
                $divisor,
338
                $this->precision(),
339
                $rounding_mode
340
            ),
341
            $this->currency()
342
        );
343
    }
344
345
346
347
    /**
348
     * @param Currency $other_currency
349
     * @throws InvalidArgumentException
350
     */
351
    public function verifySameCurrency(Currency $other_currency)
352
    {
353
        if ($this->currency()->equals($other_currency) !== true) {
354
            throw new InvalidArgumentException(
355
                esc_html__(
356
                    'Currencies must be the same in order to add or subtract their values.',
357
                    'event_espresso'
358
                )
359
            );
360
        }
361
    }
362
363
364
365
    /**
366
     * @return Calculator
367
     */
368
    protected function calculator()
369
    {
370
        $this->initializeCalculators();
371
        return self::$calculator;
372
    }
373
374
375
376
    /**
377
     * loops through a filterable array of Calculator services
378
     * and selects the first one that is supported by the current server
379
     */
380
    protected function initializeCalculators()
381
    {
382
        if (self::$calculator instanceof Calculator) {
383
            return;
384
        }
385
        $calculators = apply_filters(
386
            'FHEE__EventEspresso\core\entities\money\Money__initializeCalculators__Calculator_array',
387
            array(
388
                '\EventEspresso\core\services\currency\DefaultCalculator',
389
            )
390
        );
391
        foreach ($calculators as $calculator) {
392
            if (! class_exists($calculator)) {
393
                continue;
394
            }
395
            $calculator = new $calculator();
396
            if ($calculator instanceof Calculator && $calculator->isSupported()) {
397
                self::$calculator = $calculator;
398
                break;
399
            }
400
        }
401
    }
402
403
404
405
    /**
406
     * @return MoneyFormatter[]
407
     */
408
    protected function formatters()
409
    {
410
        $this->initializeFormatters();
411
        return self::$formatters;
412
    }
413
414
415
416
    /**
417
     * initializes a filterable array of MoneyFormatter services
418
     */
419
    protected function initializeFormatters()
420
    {
421
        if (! empty(self::$formatters)) {
422
            return;
423
        }
424
        self::$formatters = apply_filters(
425
            'FHEE__EventEspresso\core\entities\money\Money__initializeFormatters__MoneyFormatters_array',
426
            array(
427
                1 => new DecimalMoneyFormatter(),
428
                2 => new ThousandsMoneyFormatter(),
429
                3 => new CurrencySignMoneyFormatter(),
430
                4 => new InternationalMoneyFormatter(),
431
            )
432
        );
433
    }
434
435
436
437
    /**
438
     * @return string
439
     */
440
    public function __toString()
441
    {
442
        return $this->format(MoneyFormatter::DECIMAL_ONLY);
443
    }
444
445
446
447
448
}
449
// End of file Money.php
450
// Location: core/entities/money/Money.php