Completed
Branch FET-10619-money-entity (6f89bb)
by
unknown
94:59 queued 83:18
created

Money::__callStatic()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 2
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
namespace EventEspresso\core\domain\values\currency;
4
5
use EventEspresso\core\exceptions\InvalidDataTypeException;
6
use EventEspresso\core\services\currency\Calculator;
7
use EventEspresso\core\services\currency\MoneyFormatter;
8
use InvalidArgumentException;
9
10
defined('EVENT_ESPRESSO_VERSION') || exit;
11
12
13
14
/**
15
 * Class Money
16
 * Immutable object representing an amount of money for a particular currency
17
 *
18
 * @package       Event Espresso
19
 * @author        Brent Christensen
20
 * @since         $VID:$
21
 */
22
class Money
23
{
24
25
    /**
26
     * number of decimal places to be added to currencies for internal computations,
27
     * but removed before any output or formatting is applied.
28
     * This allows us to avoid rounding errors during calculations.
29
     */
30
    const EXTRA_PRECISION = 3;
31
32
    /**
33
     * @var int $amount
34
     */
35
    private $amount;
36
37
    /**
38
     * @var Currency $currency
39
     */
40
    private $currency;
41
42
    /**
43
     * @var Calculator $calculator
44
     */
45
    protected $calculator;
46
47
    /**
48
     * @var MoneyFormatter[] $formatters
49
     */
50
    protected $formatters;
51
52
53
54
    /**
55
     * Money constructor.
56
     *
57
     * @param float|int|string $amount money amount IN THE STANDARD UNIT FOR THE CURRENCY ie: dollars, Euros, etc
58
     *                                 example: $12.5 USD would equate to a value amount of 12.50
59
     * @param Currency         $currency
60
     * @param Calculator       $calculator
61
     * @param MoneyFormatter[] $formatters
62
     * @throws InvalidDataTypeException
63
     */
64
    public function __construct($amount, Currency $currency, Calculator $calculator, array $formatters)
65
    {
66
        $this->currency   = $currency;
67
        $this->amount     = (string) $this->parseAmount($amount);
68
        $this->calculator = $calculator;
69
        $this->formatters = $formatters;
70
    }
71
72
73
74
    /**
75
     * @return Calculator
76
     */
77
    protected function calculator()
78
    {
79
        return $this->calculator;
80
    }
81
82
83
84
    /**
85
     * @return MoneyFormatter[]
86
     */
87
    protected function formatters()
88
    {
89
        return $this->formatters;
90
    }
91
92
93
94
    /**
95
     * @param float|int|string $amount money amount IN THE STANDARD UNIT FOR THE CURRENCY ie: dollars, Euros, etc
96
     *                                 example: $12.5 USD would equate to a value amount of 12.50
97
     * @return float|int|number|string
98
     * @throws InvalidDataTypeException
99
     */
100
    private function parseAmount($amount)
101
    {
102
        $type = gettype($amount);
103
        switch ($type) {
104
            case 'integer' :
105
            case 'double' :
106
            case 'string' :
107
                break;
108
            default  :
109
                throw new InvalidDataTypeException(
110
                    '$amount',
111
                    $amount,
112
                    'integer (or float or string)'
113
                );
114
        }
115
        if ($this->currency->decimalMark() !== '.') {
116
            // remove thousands separator and replace decimal place with standard decimal.
117
            $amount = str_replace(
118
                array(
119
                    $this->currency->thousands(),
120
                    $this->currency->decimalMark(),
121
                ),
122
                array(
123
                    '',
124
                    '.',
125
                ),
126
                $amount
127
            );
128
        }
129
        // remove any non numeric values but leave the decimal
130
        $amount = (float) preg_replace('/([^0-9\\.])/', '', $amount);
131
        // shift the decimal position by the number of decimal places used internally
132
        // ex: 12.5 for a currency using 2 decimal places, would become 1250
133
        // then if our extra internal precision was 3, it would become 1250000
134
        $amount *= pow(10, $this->precision());
135
        // then round up the remaining value if there is still a fractional amount left
136
        $amount = round($amount);
137
        return $amount;
138
    }
139
140
141
142
    /**
143
     * adds or subtracts additional decimal places based on the value of the Money::EXTRA_PRECISION constant
144
     *
145
     * @param bool $positive
146
     * @return int
147
     */
148
    private function precision($positive = true)
149
    {
150
        $sign = $positive ? 1 : -1;
151
        return ((int) $this->currency->decimalPlaces() + Money::EXTRA_PRECISION) * $sign;
152
    }
153
154
155
156
    /**
157
     * Returns the money amount as an unformatted string
158
     * IF YOU REQUIRE A FORMATTED STRING, THEN USE Money::format()
159
     *
160
     * @return string
161
     */
162
    public function amount()
163
    {
164
        // shift the decimal position BACK by the number of decimal places used internally
165
        // ex: 1250 for a currency using 2 decimal places, would become 12.50
166
        $amount = (string) $this->amount * pow(10, $this->precision(false));
167
        // then shave off our extra internal precision using the number of decimal places for the currency
168
        $amount = round($amount, $this->currency->decimalPlaces());
169
        return $amount;
170
    }
171
172
173
174
    /**
175
     * Returns the money SUBUNITS amount as an INTEGER
176
     *
177
     * @return integer
178
     */
179
    public function amountInSubunits()
180
    {
181
        // shift the decimal position BACK
182
        // by the number of decimal places used internally by the extra internal precision
183
        // ex: if our extra internal precision was 3,
184
        // then 1250000 would become 1250
185
        $amount = (string) $this->amount * pow(10, Money::EXTRA_PRECISION * -1);
186
        // then shave off anything after the decimal
187
        $amount = round($amount);
188
        return $amount;
189
    }
190
191
192
193
    /**
194
     * applies formatting based on the specified formatting level
195
     * corresponding to one of the constants on \EventEspresso\core\services\currency\MoneyFormatter
196
     *
197
     * @param int $formatting_level
198
     * @return string
199
     */
200
    public function format($formatting_level = MoneyFormatter::ADD_THOUSANDS)
201
    {
202
        $formatted_amount = $this->amount();
203
        $formatters       = $this->formatters();
204
        // if we are applying thousands formatting...
205
        if ($formatting_level >= MoneyFormatter::ADD_THOUSANDS) {
206
            // then let's remove decimal formatting since it's included in thousands formatting
207
            unset($formatters[ MoneyFormatter::DECIMAL_ONLY ]);
208
        }
209
        for ($x = 1; $x <= $formatting_level; $x++) {
210
            if (isset($formatters[ $x ]) && $formatters[ $x ] instanceof MoneyFormatter) {
211
                $formatted_amount = $formatters[ $x ]->format($formatted_amount, $this->currency);
212
            }
213
        }
214
        return $formatted_amount;
215
    }
216
217
218
219
    /**
220
     * Returns the Currency object for this money
221
     *
222
     * @return Currency
223
     */
224
    public function currency()
225
    {
226
        return $this->currency;
227
    }
228
229
230
231
    /**
232
     * adds the supplied Money amount to this Money amount
233
     * and returns a new Money object
234
     *
235
     * @param Money $other
236
     * @return Money
237
     * @throws InvalidArgumentException
238
     */
239 View Code Duplication
    public function add(Money $other)
240
    {
241
        $this->verifySameCurrency($other->currency());
242
        return new Money(
243
            $this->calculator()->add(
244
                $this->amount(),
245
                $other->amount()
246
            ),
247
            $this->currency(),
248
            $this->calculator(),
249
            $this->formatters()
250
        );
251
    }
252
253
254
255
    /**
256
     * subtracts the supplied Money amount from 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 subtract(Money $other)
264
    {
265
        $this->verifySameCurrency($other->currency());
266
        return new Money(
267
            $this->calculator()->subtract(
268
                $this->amount(),
269
                $other->amount()
270
            ),
271
            $this->currency(),
272
            $this->calculator(),
273
            $this->formatters()
274
        );
275
    }
276
277
278
279
    /**
280
     * multiplies this Money amount by the supplied $multiplier
281
     * and returns a new Money object
282
     *
283
     * @param float|int|string $multiplier
284
     * @param int              $rounding_mode
285
     * @return Money
286
     * @throws InvalidDataTypeException
287
     */
288 View Code Duplication
    public function multiply($multiplier, $rounding_mode = Calculator::ROUND_HALF_UP)
289
    {
290
        return new Money(
291
            $this->calculator()->multiply(
292
                $this->amount(),
293
                $multiplier,
294
                $this->precision(),
295
                $rounding_mode
296
            ),
297
            $this->currency(),
298
            $this->calculator(),
299
            $this->formatters()
300
        );
301
    }
302
303
304
305
    /**
306
     * divides this Money amount by the supplied $divisor
307
     * and returns a new Money object
308
     *
309
     * @param float|int|string $divisor
310
     * @param int              $rounding_mode
311
     * @return Money
312
     * @throws InvalidDataTypeException
313
     */
314 View Code Duplication
    public function divide($divisor, $rounding_mode = Calculator::ROUND_HALF_UP)
315
    {
316
        return new Money(
317
            $this->calculator()->divide(
318
                $this->amount(),
319
                $divisor,
320
                $this->precision(),
321
                $rounding_mode
322
            ),
323
            $this->currency(),
324
            $this->calculator(),
325
            $this->formatters()
326
        );
327
    }
328
329
330
331
    /**
332
     * @param Currency $other_currency
333
     * @throws InvalidArgumentException
334
     */
335
    public function verifySameCurrency(Currency $other_currency)
336
    {
337
        if ($this->currency()->equals($other_currency) !== true) {
338
            throw new InvalidArgumentException(
339
                esc_html__(
340
                    'Currencies must be the same in order to add or subtract their values.',
341
                    'event_espresso'
342
                )
343
            );
344
        }
345
    }
346
347
348
349
    /**
350
     * @return string
351
     */
352
    public function __toString()
353
    {
354
        return $this->format(MoneyFormatter::DECIMAL_ONLY);
355
    }
356
357
358
359
}
360
// End of file Money.php
361
// Location: core/entities/money/Money.php
362