├── .php-cs-fixer.php ├── LICENSE ├── README.md ├── composer.json └── src ├── Concerns ├── PersistsCurrency.php └── RegistersCurrencies.php ├── Currencies └── USD.php ├── Currency.php ├── CurrencyManager.php ├── Exceptions ├── CannotExtractCurrencyException.php ├── CurrencyDoesNotExistException.php └── InvalidCurrencyException.php ├── Money.php ├── MoneyServiceProvider.php ├── PriceFormatter.php ├── Wireable.php └── helpers.php /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | ['syntax' => 'short'], 8 | 'binary_operator_spaces' => [ 9 | 'default' => 'single_space', 10 | 'operators' => [ 11 | '=>' => null, 12 | '|' => 'no_space', 13 | ] 14 | ], 15 | 'blank_line_after_namespace' => true, 16 | 'blank_line_after_opening_tag' => true, 17 | 'no_superfluous_phpdoc_tags' => true, 18 | 'blank_line_before_statement' => [ 19 | 'statements' => ['return'] 20 | ], 21 | 'braces' => true, 22 | 'cast_spaces' => true, 23 | 'class_definition' => true, 24 | 'concat_space' => [ 25 | 'spacing' => 'one' 26 | ], 27 | 'declare_equal_normalize' => true, 28 | 'elseif' => true, 29 | 'encoding' => true, 30 | 'full_opening_tag' => true, 31 | 'declare_strict_types' => true, 32 | 'fully_qualified_strict_types' => true, // added by Shift 33 | 'function_declaration' => true, 34 | 'function_typehint_space' => true, 35 | 'heredoc_to_nowdoc' => true, 36 | 'include' => true, 37 | 'increment_style' => ['style' => 'post'], 38 | 'indentation_type' => true, 39 | 'linebreak_after_opening_tag' => true, 40 | 'line_ending' => true, 41 | 'lowercase_cast' => true, 42 | 'constant_case' => true, 43 | 'lowercase_keywords' => true, 44 | 'lowercase_static_reference' => true, // added from Symfony 45 | 'magic_method_casing' => true, // added from Symfony 46 | 'magic_constant_casing' => true, 47 | 'method_argument_space' => true, 48 | 'native_function_casing' => true, 49 | 'no_alias_functions' => true, 50 | 'no_extra_blank_lines' => [ 51 | 'tokens' => [ 52 | 'extra', 53 | 'throw', 54 | 'use', 55 | 'use_trait', 56 | ] 57 | ], 58 | 'no_blank_lines_after_class_opening' => true, 59 | 'no_blank_lines_after_phpdoc' => true, 60 | 'no_closing_tag' => true, 61 | 'no_empty_phpdoc' => true, 62 | 'no_empty_statement' => true, 63 | 'no_leading_import_slash' => true, 64 | 'no_leading_namespace_whitespace' => true, 65 | 'no_mixed_echo_print' => [ 66 | 'use' => 'echo' 67 | ], 68 | 'no_multiline_whitespace_around_double_arrow' => true, 69 | 'multiline_whitespace_before_semicolons' => [ 70 | 'strategy' => 'no_multi_line' 71 | ], 72 | 'no_short_bool_cast' => true, 73 | 'no_singleline_whitespace_before_semicolons' => true, 74 | 'no_spaces_after_function_name' => true, 75 | 'no_spaces_around_offset' => true, 76 | 'no_spaces_inside_parenthesis' => true, 77 | 'no_trailing_comma_in_list_call' => true, 78 | 'no_trailing_comma_in_singleline_array' => true, 79 | 'no_trailing_whitespace' => true, 80 | 'no_trailing_whitespace_in_comment' => true, 81 | 'no_unneeded_control_parentheses' => true, 82 | 'no_unreachable_default_argument_value' => true, 83 | 'no_useless_return' => true, 84 | 'no_whitespace_before_comma_in_array' => true, 85 | 'no_whitespace_in_blank_line' => true, 86 | 'normalize_index_brace' => true, 87 | 'not_operator_with_successor_space' => true, 88 | 'object_operator_without_whitespace' => true, 89 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 90 | 'phpdoc_indent' => true, 91 | 'general_phpdoc_tag_rename' => true, 92 | 'phpdoc_no_access' => true, 93 | 'phpdoc_no_package' => true, 94 | 'phpdoc_no_useless_inheritdoc' => true, 95 | 'phpdoc_scalar' => true, 96 | 'phpdoc_single_line_var_spacing' => true, 97 | 'phpdoc_summary' => true, 98 | 'phpdoc_to_comment' => false, 99 | 'phpdoc_trim' => true, 100 | 'phpdoc_types' => true, 101 | 'phpdoc_var_without_name' => true, 102 | 'psr_autoloading' => true, 103 | 'self_accessor' => true, 104 | 'short_scalar_cast' => true, 105 | 'simplified_null_return' => false, // disabled by Shift 106 | 'single_blank_line_at_eof' => true, 107 | 'single_blank_line_before_namespace' => true, 108 | 'single_class_element_per_statement' => true, 109 | 'single_import_per_statement' => true, 110 | 'single_line_after_imports' => true, 111 | 'no_unused_imports' => true, 112 | 'single_line_comment_style' => [ 113 | 'comment_types' => ['hash'] 114 | ], 115 | 'single_quote' => true, 116 | 'space_after_semicolon' => true, 117 | 'standardize_not_equals' => true, 118 | 'switch_case_semicolon_to_colon' => true, 119 | 'switch_case_space' => true, 120 | 'ternary_operator_spaces' => true, 121 | 'trailing_comma_in_multiline' => true, 122 | 'trim_array_spaces' => true, 123 | 'unary_operator_spaces' => true, 124 | 'whitespace_after_comma_in_array' => true, 125 | ]; 126 | 127 | $project_path = getcwd(); 128 | $finder = Finder::create() 129 | ->in([ 130 | $project_path . '/src', 131 | ]) 132 | ->name('*.php') 133 | ->notName('*.blade.php') 134 | ->ignoreDotFiles(true) 135 | ->ignoreVCS(true); 136 | 137 | return (new Config()) 138 | ->setFinder($finder) 139 | ->setRules($rules) 140 | ->setRiskyAllowed(true) 141 | ->setUsingCache(true); 142 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ArchTech Development, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Money 2 | 3 | A simple package for working with money. 4 | 5 | Main features: 6 | - Simple API 7 | - Livewire integration 8 | - Custom currency support 9 | - Highly customizable formatting 10 | - Rounding logic for compliant accounting 11 | 12 | This package is our implementation of the [Money pattern](https://martinfowler.com/eaaCatalog/money.html). 13 | 14 | You can read more about why we built it and how it works on our forum: [New package: archtechx/money](https://forum.archte.ch/archtech/t/new-package-archtechxmoney). 15 | 16 | ## Installation 17 | 18 | Require the package via composer: 19 | ```sh 20 | composer require archtechx/money 21 | ``` 22 | # Usage 23 | 24 | The package has two main classes: 25 | - `Money` which represents monetary values 26 | - `Currency` which is extended by the currencies that you're using 27 | 28 | This document uses the terms [decimal value](#decimal-value), [base value](#base-value), [default currency](#default-currency), [current currency](#current-currency), [rounding](#rounding), [math decimals](#math-decimals), [display decimals](#display-decimals), and a few others. Refer to the [Terminology](#terminology) section for definitions. 29 | 30 | ## Money 31 | 32 | **Important**: As an implementation of the [Money pattern](https://martinfowler.com/eaaCatalog/money.html), the `Money` object creates a new instance after each operation. Meaning, **all `Money` instances are immutable**. To modify the value of a variable, re-initialize it with a new value: 33 | 34 | ```php 35 | // Incorrect 36 | $money = money(1500); 37 | $money->times(3); // ❌ 38 | $money->value(); // 1500 39 | 40 | // Correct 41 | $money = money(1500); 42 | $money = $money->times(3); // ✅ 43 | $money->value(); // 4500 44 | ``` 45 | 46 | ### Creating `Money` instances 47 | ```php 48 | // Using cents 49 | $money = money(1500); // $15.00; default currency 50 | $money = money(1500, 'EUR'); // 15.00 € 51 | $money = money(2000, new USD); // $20.00 52 | $money = money(3000, CZK::class); // 30 Kč 53 | 54 | // Using decimals 55 | $money = Money::fromDecimal(15.00, 'EUR'); // 15.00 € 56 | $money = Money::fromDecimal(20.00, new USD); // $20.00 57 | $money = Money::fromDecimal(30.00, CZK::class); // 30 Kč 58 | ``` 59 | 60 | ### Arithmetics 61 | 62 | ```php 63 | // Addition 64 | $money = money(1000); 65 | $money = $money->add(500); 66 | $money->value(); // 1500 67 | 68 | // Subtraction 69 | $money = money(1000); 70 | $money = $money->subtract(500); 71 | $money->value(); // 500 72 | 73 | // Multiplication 74 | $money = money(1000); 75 | $money = $money->multiplyBy(2); // alias: ->times() 76 | $money->value(); // 2000 77 | 78 | // Division 79 | $money = money(1000); 80 | $money = $money->divideBy(2); 81 | $money->value(); // 500 82 | ``` 83 | 84 | ### Converting money to a different currency 85 | 86 | ```php 87 | $money = money(2200); 88 | $money->convertTo(CZK::class); 89 | ``` 90 | 91 | ### Comparing money instances 92 | 93 | **Equality of monetary value** 94 | ```php 95 | // Assuming CZK is 25:1 USD 96 | 97 | // ✅ true 98 | money(100, USD::class)->equals(money(100, USD::class)); 99 | 100 | // ❌ false 101 | money(100, USD::class)->equals(money(200, USD::class)); 102 | 103 | // ✅ true 104 | money(100, USD::class)->equals(money(2500, CZK::class)); 105 | 106 | // ❌ false 107 | money(100, USD::class)->equals(money(200, CZK::class)); 108 | ``` 109 | 110 | **Equality of monetary value AND currency** 111 | ```php 112 | // Assuming CZK is 25:1 USD 113 | 114 | // ✅ true 115 | money(100, USD::class)->is(money(100, USD::class)); 116 | 117 | // ❌ false: different monetary value 118 | money(100, USD::class)->is(money(200, USD::class)); 119 | 120 | // ❌ false: different currency 121 | money(100, USD::class)->is(money(2500, CZK::class)); 122 | 123 | // ❌ false: different currency AND monetary value 124 | money(100, USD::class)->is(money(200, CZK::class)); 125 | ``` 126 | 127 | ### Adding fees 128 | 129 | You can use the `addFee()` or `addTax()` methods to add a % fee to the money: 130 | ```php 131 | $money = money(1000); 132 | $money = $money->addTax(20.0); // 20% 133 | $money->value(); // 1200 134 | ``` 135 | 136 | ### Accessing the decimal value 137 | 138 | ```php 139 | $money = Money::fromDecimal(100.0, new USD); 140 | $money->value(); // 10000 141 | $money->decimal(); // 100.0 142 | ``` 143 | 144 | ### Formatting money 145 | 146 | You can format money using the `->formatted()` method. It takes [display decimals](#display-decimals) into consideration. 147 | 148 | ```php 149 | $money = Money::fromDecimal(40.25, USD::class); 150 | $money->formatted(); // $40.25 151 | ``` 152 | 153 | The method optionally accepts overrides for the [currency specification](#currency-logic): 154 | ```php 155 | $money = Money::fromDecimal(40.25, USD::class); 156 | 157 | // $ 40.25 USD 158 | $money->formatted(decimalSeparator: ',', prefix: '$ ', suffix: ' USD'); 159 | ``` 160 | 161 | The overrides can also be passed as an array: 162 | ```php 163 | $money = Money::fromDecimal(40.25, USD::class); 164 | 165 | // $ 40.25 USD 166 | $money->formatted(['decimalSeparator' => ',', 'prefix' => '$ ', 'suffix' => ' USD']); 167 | ``` 168 | 169 | There's also `->rawFormatted()` if you wish to use [math decimals](#math-decimals) instead of [display decimals](#display-decimals). 170 | ```php 171 | $money = Money::new(123456, CZK::class); 172 | $money->rawFormatted(); // 1 234,56 Kč 173 | ``` 174 | 175 | Converting the formatted value back to the `Money` instance is also possible. The package tries to extract the currency from the provided string: 176 | ```php 177 | $money = money(1000); 178 | $formatted = $money->formatted(); // $10.00 179 | $fromFormatted = Money::fromFormatted($formatted); 180 | $fromFormatted->is($money); // true 181 | ``` 182 | 183 | If you had passed overrides while [formatting the money instance](#formatting-money), the same can passed to this method. 184 | ```php 185 | $money = money(1000); 186 | $formatted = $money->formatted(['prefix' => '$ ', 'suffix' => ' USD']); // $ 10.00 USD 187 | $fromFormatted = Money::fromFormatted($formatted, USD::class, ['prefix' => '$ ', 'suffix' => ' USD']); 188 | $fromFormatted->is($money); // true 189 | ``` 190 | 191 | Notes: 192 | 1) If currency is not specified and none of the currencies match the prefix and suffix, an exception will be thrown. 193 | 2) If currency is not specified and multiple currencies use the same prefix and suffix, an exception will be thrown. 194 | 3) `fromFormatted()` misses the cents if the [math decimals](#math-decimals) are greater than [display decimals](#display-decimals). 195 | 196 | ### Rounding money 197 | 198 | Some currencies, such as the Czech Crown (CZK), generally display final prices in full crowns, but use cents for the intermediate math operations. For example: 199 | 200 | ```php 201 | $money = Money::fromDecimal(3.30, CZK::class); 202 | $money->value(); // 330 203 | $money->formatted(); // 3 Kč 204 | 205 | $money = $money->times(3); 206 | $money->value(); // 990 207 | $money->formatted(); // 10 Kč 208 | ``` 209 | 210 | If the customer purchases a single `3.30` item, he pays `3 CZK`, but if he purchases three `3.30` items, he pays `10 CZK`. 211 | 212 | This rounding (to full crowns) is standard and legal per the accounting legislation, since it makes payments easier. However, the law requires you to keep track of the rounding difference for tax purposes. 213 | 214 | #### Getting the used rounding 215 | 216 | For that use case, our package lets you get the rounding difference using a simple method call: 217 | ```php 218 | $money = Money::fromDecimal(9.90, CZK::class); 219 | $money->decimal(); // 9.90 220 | $money->formatted(); // 10 Kč 221 | $money->rounding(); // +0.10 Kč = 10 222 | 223 | $money = Money::fromDecimal(3.30, CZK::class); 224 | $money->decimal(); // 3.30 225 | $money->formatted(); // 3 Kč 226 | $money->rounding(); // -0.30 Kč = -30 227 | ``` 228 | 229 | #### Applying rounding to money 230 | 231 | ```php 232 | // Using the currency rounding 233 | $money = Money::fromDecimal(9.90, CZK::class); 234 | $money->decimal(); // 9.90 235 | $money = $money->rounded(); // currency rounding 236 | $money->decimal(); // 10.0 237 | 238 | // Using custom rounding 239 | $money = Money::fromDecimal(2.22, USD::class); 240 | $money->decimal(); // 2.22 241 | $money = $money->rounded(1); // custom rounding: 1 decimal 242 | $money->decimal(); // 2.20 243 | ``` 244 | 245 | ## Currencies 246 | 247 | To work with the registered currencies, use the bound `CurrencyManager` instance, accessible using the `currencies()` helper. 248 | 249 | ### Creating a currency 250 | 251 | This package provides only USD currency by default. 252 | 253 | You can create a currency using one of the multiple supported syntaxes. 254 | ```php 255 | // anonymous Currency object 256 | $currency = new Currency( 257 | code: 'FOO', 258 | name: 'Foo currency', 259 | rate: 1.8, 260 | prefix: '# ', 261 | suffix: ' FOO', 262 | ); 263 | 264 | // array 265 | $currency = [ 266 | 'code' => 'FOO', 267 | 'name' => 'Foo currency', 268 | 'rate' => 1.8, 269 | 'prefix' => '# ', 270 | 'suffix' => ' FOO', 271 | ]; 272 | 273 | // class 274 | class FOO extends Currency 275 | { 276 | protected string $code = 'FOO'; 277 | protected string $name = 'Foo currency'; 278 | protected float $rate = 1.8; 279 | protected string $prefix = '# '; 280 | protected string $suffix = ' FOO'; 281 | } 282 | ``` 283 | 284 | See the [Currency logic](#currency-logic) section for a list of available properties to configure. Note that when registering a currency, two values **must** be specified: 285 | 1. The code of the currency (e.g. `USD`) 286 | 2. The name of the currency (e.g. `United States Dollar`) 287 | 288 | ### Adding a currency 289 | 290 | Register a new currency: 291 | ```php 292 | currencies()->add(new USD); 293 | currencies()->add(USD::class); 294 | currencies()->add($currency); // object or array 295 | ``` 296 | 297 | ### Removing a specific currency 298 | 299 | To remove a specific currency, you can use the `remove()` method: 300 | ```php 301 | currencies()->remove('USD'); 302 | currencies()->remove(USD::class); 303 | ``` 304 | 305 | ### Removing all currencies 306 | 307 | To remove all currencies, you can use the `clear()` method: 308 | ```php 309 | currencies()->clear(); 310 | ``` 311 | 312 | ### Resetting currencies 313 | 314 | Can be useful in tests. This reverts all your changes and makes the `CurrencyManager` use `USD` as the default currency. 315 | 316 | ```php 317 | currencies()->reset(); 318 | ``` 319 | 320 | ### Currency logic 321 | 322 | Currencies can have the following properties: 323 | ```php 324 | protected string $code = null; 325 | protected string $name = null; 326 | protected float $rate = null; 327 | protected string $prefix = null; 328 | protected string $suffix = null; 329 | protected int $mathDecimals = null; 330 | protected int $displayDecimals = null; 331 | protected int $rounding = null; 332 | protected string $decimalSeparator = null; 333 | protected string $thousandsSeparator = null; 334 | ``` 335 | 336 | For each one, there's also a `public` method. Specifying a method can be useful when your currency config is dynamic, e.g. when the currency rate is taken from some API: 337 | 338 | ```php 339 | public function rate(): float 340 | { 341 | return cache()->remember("{$this->code}.rate", 3600, function () { 342 | return Http::get("https://api.currency.service/rate/USD/{$this->code}"); 343 | }); 344 | } 345 | ``` 346 | 347 | ### Setting the default currency 348 | 349 | You can set the [default currency](#default-currency) using the `setDefault()` method: 350 | ```php 351 | currencies()->setDefault('USD'); 352 | ``` 353 | 354 | ### Setting the current currency 355 | 356 | You can set the [current currency](#current-currency) using the `setCurrent()` method: 357 | ```php 358 | currencies()->setCurrent('USD'); 359 | ``` 360 | 361 | ### Persisting a selected currency across requests 362 | 363 | If your users can select the currency they want to see the app in, the package can automatically write the current currency to a persistent store of your choice, and read from that store on subsequent requests. 364 | 365 | For example, say we want to use the `currency` session key to keep track of the user's selected session. To implement that, we only need to do this: 366 | ```php 367 | currencies() 368 | ->storeCurrentUsing(fn (string $code) => session()->put('currency', $code)) 369 | ->resolveCurrentUsing(fn () => session()->get('currency')); 370 | ``` 371 | You can add this code to your AppServiceProvider's `boot()` method. 372 | 373 | Now, whenever the current currency is changed using `currencies()->setCurrent()`, perhaps in a route like this: 374 | ```php 375 | Route::get('/currency/change/{currency}', function (string $currency) { 376 | currencies()->setCurrent($currency); 377 | 378 | return redirect()->back(); 379 | }); 380 | ``` 381 | it will also be written to the `currency` session key. The route can be used by a `
` in your navbar, or any other UI element. 382 | 383 | # Terminology 384 | 385 | This section explains the terminology used in the package. 386 | 387 | ## Values 388 | 389 | Multiple different things can be meant by the "value" of a `Money` object. For that reason, we use separate terms. 390 | 391 | ### Base value 392 | 393 | The base value is the value passed to the `money()` helper: 394 | ```php 395 | $money = money(1000); 396 | ``` 397 | and returned from the `->value()` method: 398 | ```php 399 | $money->value(); // 1000 400 | ``` 401 | 402 | This is the actual integer value of the money. In most currencies this will be the cents. 403 | 404 | The package uses the base value for all money calculations. 405 | 406 | ### Decimal value 407 | 408 | The decimal value isn't used for calculations, but it is the human-readable one. It's typically used in the formatted value. 409 | ```php 410 | $money = Money::fromDecimal(100.0); // $100 USD 411 | $money->value(); // 10000 412 | $money->decimal(); // 100.0 413 | ``` 414 | 415 | ### Value in default currency 416 | 417 | This is the value of a `Money` object converted to the default currency. 418 | 419 | For example, you may want to let administrators enter the price of a product in any currency, but still store it in the default currency. 420 | 421 | It's generally recommended to use the default currency in the "code land". And only use other currencies for displaying prices to the user (e.g. customer) or letting the administrators enter prices of things in a currency that works for them. 422 | 423 | Of course, there are exceptions, and sometimes you may want to store both the currency and the value of an item. For that, the package has [JSON encoding features](#json-serialization) if you wish to store the entire `Money` object in a single database column. 424 | 425 | Storing the integer price and the string currency as separate columns is, of course, perfectly fine as well. 426 | 427 | ### Formatted value 428 | 429 | The formatted value is the Money value displayed per its currency spec. It may use the prefix, suffix, decimal separator, thousands separator, and the [display decimals](#display-decimals). 430 | 431 | For example: 432 | ```php 433 | money(123456, new CZK)->formatted(); // 1 235 Kč 434 | ``` 435 | 436 | Note that the [display decimals](#display-decimals) can be different from the [math decimals](#math-decimals). 437 | 438 | For the Czech Crown (CZK), the display decimals will be `0`, but the math decimals will be `2`. Meaning, cents are used for money calculations, and the `decimal()` method will return the base value divided by `100`, but the display decimals don't include any cents. 439 | 440 | ### Raw formatted value 441 | 442 | For the inverse of what was just explained above, you can use the `rawFormatted()` method. This returns the formatted value, **but uses the math decimals for the display decimals**. Meaning, the value in the example above will be displayed including cents: 443 | ```php 444 | money(123456, new CZK)->rawFormatted(); // 1 234,56 Kč 445 | ``` 446 | 447 | This is mostly useful for currencies like the Czech Crown which generally don't use cents, but **can** use them in specific cases. 448 | 449 | ## Currencies 450 | 451 | ### Current currency 452 | 453 | The current currency refers to the currently used currency. 454 | 455 | By default, the package doesn't use it anywhere. All calls such as `money()` will use the provided currency, or the default currency. 456 | 457 | The current currency is something you can convert money to in the final step of calculations, right before displaying it to the user in the browser. 458 | 459 | ### Default currency 460 | 461 | The default currency is the currency that Money defaults to in the context of your codebase. 462 | 463 | The `money()` helper, `Money::fromDecimal()` method, and `new Money()` all use this currency (unless a specific one is provided). 464 | 465 | It can be a good idea to use the default currency for data storage. See more about this in the [Value in default currency](#value-in-default-currency) section. 466 | 467 | ### Math decimals 468 | 469 | The math decimals refer to the amount of decimal points the currency has in a math context. 470 | 471 | All math operations are still done in floats, using the [base value](#base-value), but the math decimals are used for knowing how to round the money after each operation, how to instantiate it with the `Money::fromDecimal()` method, and more. 472 | 473 | ### Display decimals 474 | 475 | The display decimals refer to the amount of decimals used in the [formatted value](#formatted-value). 476 | 477 | # Extra features 478 | 479 | ## Livewire support 480 | 481 | The package supports Livewire out of the box. You can typehint any Livewire property as `Money` and the monetary value & currency will be stored in the component's state. 482 | 483 | ```php 484 | class EditProduct extends Component 485 | { 486 | public Money $price; 487 | 488 | // ... 489 | } 490 | ``` 491 | 492 | Livewire's custom type support isn't advanced yet, so this is a bit harder to use in the Blade view — a wrapper Alpine component is recommended. In a future release, `wire:model` will be supported for `currency` and `value` directly. 493 | 494 | The component can look roughly like this: 495 | ```html 496 |
509 | Currency: 510 | Price: 511 |
512 | ``` 513 | 514 | ## JSON serialization 515 | 516 | Both currencies and `Money` instances can be converted to JSON, and instantiated from JSON. 517 | 518 | ```php 519 | $currency = new CZK; 520 | $json = json_encode($currency); 521 | $currency = Currency::fromJson($json); 522 | 523 | $foo = money(100, 'CZK'); 524 | $bar = Money::fromJson($money->toJson()); 525 | $money->is($bar); // true 526 | ``` 527 | 528 | ## Tips 529 | 530 | ### 💡 Accepted currency code formats 531 | 532 | Most methods which accept a currency accept it in any of these formats: 533 | ```php 534 | currency(USD::class); 535 | currency(new USD); 536 | currency('USD'); 537 | 538 | money(1000, USD::class)->convertTo('CZK'); 539 | money(1000, 'USD')->convertTo(new CZK); 540 | money(1000, new USD)->convertTo(CZK::class); 541 | ``` 542 | 543 | ### 💡 Dynamically add currencies 544 | 545 | Class currencies are elegant, but not necessary. If your currency specs come from the database, or some API, you can register them as arrays. 546 | 547 | ```php 548 | // LoadCurrencies middleware 549 | 550 | currencies()->add(cache()->remember('currencies', 3600, function () { 551 | return UserCurrencies::where('user_id', auth()->id())->get()->toArray(); 552 | }); 553 | ``` 554 | 555 | Where the DB call returns an array of array currencies following the [format mentioned above](#creating-a-currency). 556 | 557 | ## Development & contributing 558 | 559 | Run all checks locally: 560 | 561 | ```sh 562 | ./check 563 | ``` 564 | 565 | Code style will be automatically fixed by php-cs-fixer. 566 | 567 | No database is needed to run the tests. 568 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "archtechx/money", 3 | "description": "A lightweight package for handling money math in PHP.", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Samuel Štancl", 9 | "email": "samuel@archte.ch" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "ArchTech\\Money\\": "src/" 15 | }, 16 | "files": [ 17 | "src/helpers.php", 18 | "src/Wireable.php" 19 | ] 20 | }, 21 | "autoload-dev": { 22 | "psr-4": { 23 | "ArchTech\\Money\\Tests\\": "tests/" 24 | } 25 | }, 26 | "require": { 27 | "php": "^8.2", 28 | "illuminate/support": "^10.0|^11.0|^12.0", 29 | "archtechx/helpers": "^0.3.2" 30 | }, 31 | "require-dev": { 32 | "orchestra/testbench": "^8.0|^9.0|^10.0", 33 | "pestphp/pest": "^2.0|^3.7", 34 | "phpstan/phpstan": "^1.9.8|^2.1", 35 | "pestphp/pest-plugin-laravel": "^2.0|^3.1", 36 | "larastan/larastan": "^2.4|^3.0" 37 | }, 38 | "extra": { 39 | "laravel": { 40 | "providers": [ 41 | "ArchTech\\Money\\MoneyServiceProvider" 42 | ] 43 | } 44 | }, 45 | "minimum-stability": "dev", 46 | "prefer-stable": true, 47 | "config": { 48 | "allow-plugins": { 49 | "pestphp/pest-plugin": true 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Concerns/PersistsCurrency.php: -------------------------------------------------------------------------------- 1 | resolveCurrentUsing) 17 | ? ($this->resolveCurrentUsing)() 18 | : null; 19 | } 20 | 21 | /** Set the handler for resolving the current currency. */ 22 | public function resolveCurrentUsing(Closure $callback): static 23 | { 24 | $this->resolveCurrentUsing = $callback; 25 | 26 | return $this; 27 | } 28 | 29 | protected function storeCurrent(string $currency): static 30 | { 31 | if (isset($this->storeCurrentUsing)) { 32 | ($this->storeCurrentUsing)($currency); 33 | } 34 | 35 | return $this; 36 | } 37 | 38 | /** Set the handler for storing the current currency. */ 39 | public function storeCurrentUsing(Closure $callback): static 40 | { 41 | $this->storeCurrentUsing = $callback; 42 | 43 | return $this; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Concerns/RegistersCurrencies.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | protected array $currencies = []; 21 | 22 | /** Register a currency. */ 23 | public function add(string|Currency|Closure|array $currencies): static 24 | { 25 | // $currencies can be: 26 | // new Currency(...) 27 | // [new Currency(..), new Currency(...)] 28 | // USD::class 29 | // new USD 30 | // ['code' => 'GBP', 'rate' => 0.8, 'name' => 'British Pound'] 31 | // Or a Closure returning any of the above 32 | 33 | // Invoke Closures 34 | $currencies = value($currencies); 35 | 36 | // Make sure we're working with an array 37 | $currencies = is_array($currencies) ? $currencies : [$currencies]; 38 | 39 | // If we're working with a single currency as an 40 | // array, we'll manually wrap it again in []. 41 | if (isset($currencies['code'])) { 42 | $currencies = [$currencies]; 43 | } 44 | 45 | foreach ($currencies as $currency) { 46 | // ['code' => 'GBP', 'rate' => 0.8, 'name' => 'British Pound'] 47 | if (is_array($currency)) { 48 | $currency = Currency::fromArray($currency); 49 | } 50 | 51 | // USD::class 52 | if (is_string($currency)) { 53 | $currency = new $currency; 54 | } 55 | 56 | /** @var Currency $currency */ 57 | $this->currencies[$currency->code()] = $currency; 58 | } 59 | 60 | return $this; 61 | } 62 | 63 | /** Unregister a currency. */ 64 | public function remove(string $currency): static 65 | { 66 | $code = $this->getCode($currency); 67 | 68 | if ($this->has($code)) { 69 | unset($this->currencies[$code]); 70 | } 71 | 72 | return $this; 73 | } 74 | 75 | /** List all registered currencies */ 76 | public function all(): array 77 | { 78 | return $this->currencies; 79 | } 80 | 81 | /** Unregister all currencies. */ 82 | public function clear(): static 83 | { 84 | $this->currencies = []; 85 | 86 | return $this; 87 | } 88 | 89 | /** Fetch a currency by its code. */ 90 | public function get(string $currency): Currency 91 | { 92 | // Converting this to the code in case a class string is passed 93 | $code = $this->getCode($currency); 94 | 95 | $this->ensureCurrencyExists($code); 96 | 97 | return $this->currencies[$code]; 98 | } 99 | 100 | /** Check if a currency is registered. */ 101 | public function has(string $currency): bool 102 | { 103 | // Converting this to the code in case a class string is passed 104 | $code = $this->getCode($currency); 105 | 106 | return isset($this->currencies[$code]); 107 | } 108 | 109 | /** Abort execution if a currency doesn't exist. */ 110 | public function ensureCurrencyExists(string $currency): static 111 | { 112 | if (! $this->has($currency)) { 113 | throw new CurrencyDoesNotExistException($currency); 114 | } 115 | 116 | return $this; 117 | } 118 | 119 | /** Get a currency's code. */ 120 | public function getCode(Currency|string $currency): string 121 | { 122 | if (is_string($currency) && isset($this->currencies[$currency])) { 123 | return $currency; 124 | } 125 | 126 | if ($currency instanceof Currency) { 127 | return $currency->code(); 128 | } 129 | 130 | if (class_exists($currency) && (new ReflectionClass($currency))->isSubclassOf(Currency::class)) { 131 | /** @var Currency $currency * */ 132 | $currency = new $currency; 133 | 134 | return $currency->code(); 135 | } 136 | 137 | throw new InvalidCurrencyException( 138 | "{$currency} is not a valid currency.", 139 | ); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Currencies/USD.php: -------------------------------------------------------------------------------- 1 | */ 12 | class Currency implements Arrayable, JsonSerializable 13 | { 14 | /** Code of the currency (e.g. 'CZK'). */ 15 | protected string $code; 16 | 17 | /** Name of the currency (e.g. 'Czech Crown'). */ 18 | protected string $name; 19 | 20 | /** Rate of this currency relative to the default currency. */ 21 | protected float $rate; 22 | 23 | /** Prefix placed at the beginning of the formatted value. */ 24 | protected string $prefix; 25 | 26 | /** Suffix placed at the end of the formatted value. */ 27 | protected string $suffix; 28 | 29 | /** Number of decimals used in money calculations. */ 30 | protected int $mathDecimals; 31 | 32 | /** Number of decimals used in the formatted value. */ 33 | protected int $displayDecimals; 34 | 35 | /** The character used to separate the decimal values. */ 36 | protected string $decimalSeparator; 37 | 38 | /** The character used to separate groups of thousands. */ 39 | protected string $thousandsSeparator; 40 | 41 | /** How many decimals of the currency's values should get rounded. */ 42 | protected int $rounding; 43 | 44 | /** Should trailing decimal zeros be trimmed. */ 45 | protected bool $trimTrailingDecimalZeros; 46 | 47 | /** Create a new Currency instance. */ 48 | public function __construct( 49 | ?string $code = null, 50 | ?string $name = null, 51 | ?float $rate = null, 52 | ?string $prefix = null, 53 | ?string $suffix = null, 54 | ?int $mathDecimals = null, 55 | ?int $displayDecimals = null, 56 | ?int $rounding = null, 57 | ?string $decimalSeparator = null, 58 | ?string $thousandsSeparator = null, 59 | ?bool $trimTrailingDecimalZeros = null, 60 | ) { 61 | $this->code = $code ?? $this->code ?? ''; 62 | $this->name = $name ?? $this->name ?? ''; 63 | $this->rate = $rate ?? $this->rate ?? 1.0; 64 | $this->prefix = $prefix ?? $this->prefix ?? ''; 65 | $this->suffix = $suffix ?? $this->suffix ?? ''; 66 | $this->mathDecimals = $mathDecimals ?? $this->mathDecimals ?? 2; 67 | $this->displayDecimals = $displayDecimals ?? $this->displayDecimals ?? 2; 68 | $this->decimalSeparator = $decimalSeparator ?? $this->decimalSeparator ?? '.'; 69 | $this->thousandsSeparator = $thousandsSeparator ?? $this->thousandsSeparator ?? ','; 70 | $this->rounding = $rounding ?? $this->rounding ?? $this->mathDecimals; 71 | $this->trimTrailingDecimalZeros = $trimTrailingDecimalZeros ?? $this->trimTrailingDecimalZeros ?? false; 72 | 73 | $this->check(); 74 | } 75 | 76 | /** Create an anonymous Currency instance from an array. */ 77 | public static function fromArray(array $currency): static 78 | { 79 | return new static(...$currency); 80 | } 81 | 82 | /** Get the currency's code. */ 83 | public function code(): string 84 | { 85 | return $this->code; 86 | } 87 | 88 | /** Get the currency's name. */ 89 | public function name(): string 90 | { 91 | return $this->name; 92 | } 93 | 94 | /** Get the currency's rate. */ 95 | public function rate(): float 96 | { 97 | return $this->rate; 98 | } 99 | 100 | /** Get the currency's prefix. */ 101 | public function prefix(): string 102 | { 103 | return $this->prefix; 104 | } 105 | 106 | /** Get the currency's suffix. */ 107 | public function suffix(): string 108 | { 109 | return $this->suffix; 110 | } 111 | 112 | /** Get the currency's math decimal count. */ 113 | public function mathDecimals(): int 114 | { 115 | return $this->mathDecimals; 116 | } 117 | 118 | /** Get the currency's math decimal count. */ 119 | public function displayDecimals(): int 120 | { 121 | return $this->displayDecimals; 122 | } 123 | 124 | /** Get the currency's decimal separator. */ 125 | public function decimalSeparator(): string 126 | { 127 | return $this->decimalSeparator; 128 | } 129 | 130 | /** Get the currency's thousands separator. */ 131 | public function thousandsSeparator(): string 132 | { 133 | return $this->thousandsSeparator; 134 | } 135 | 136 | /** Get the currency's rounding. */ 137 | public function rounding(): int 138 | { 139 | return $this->rounding; 140 | } 141 | 142 | /** Get the currency's setting for trimming trailing decimal zeros. */ 143 | public function trimTrailingDecimalZeros(): bool 144 | { 145 | return $this->trimTrailingDecimalZeros; 146 | } 147 | 148 | /** Convert the currency to a string (returns the code). */ 149 | public function __toString() 150 | { 151 | return $this->code(); 152 | } 153 | 154 | /** Convert the currency to an array. */ 155 | public function toArray(): array 156 | { 157 | return [ 158 | 'code' => $this->code, 159 | 'name' => $this->name, 160 | 'rate' => $this->rate, 161 | 'prefix' => $this->prefix, 162 | 'suffix' => $this->suffix, 163 | 'mathDecimals' => $this->mathDecimals, 164 | 'displayDecimals' => $this->displayDecimals, 165 | 'rounding' => $this->rounding, 166 | 'decimalSeparator' => $this->decimalSeparator, 167 | 'thousandsSeparator' => $this->thousandsSeparator, 168 | 'trimTrailingDecimalZeros' => $this->trimTrailingDecimalZeros, 169 | ]; 170 | } 171 | 172 | /** Get the data used for JSON serialization. */ 173 | public function jsonSerialize(): array 174 | { 175 | return $this->toArray(); 176 | } 177 | 178 | /** Create a currency from JSON. */ 179 | public static function fromJson(string|array $json): self 180 | { 181 | if (is_string($json)) { 182 | $json = json_decode($json, true, flags: JSON_THROW_ON_ERROR); 183 | } 184 | 185 | return static::fromArray($json); 186 | } 187 | 188 | /** 189 | * Ensure that the currency has all required values. 190 | * 191 | * @throws InvalidCurrencyException 192 | */ 193 | protected function check(): void 194 | { 195 | if (! $this->code() || ! $this->name()) { 196 | throw new InvalidCurrencyException('This currency does not have a code or a name.'); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/CurrencyManager.php: -------------------------------------------------------------------------------- 1 | reset(); 24 | } 25 | 26 | /** Reset the object to the default state. */ 27 | public function reset(): static 28 | { 29 | $this->currencies = [ 30 | 'USD' => new USD, 31 | ]; 32 | 33 | $this->default = 'USD'; 34 | 35 | $this->forgetCurrent(); 36 | 37 | return $this; 38 | } 39 | 40 | public function forgetCurrent(): static 41 | { 42 | unset($this->current); 43 | 44 | return $this; 45 | } 46 | 47 | /** Get the default currency. */ 48 | public function getDefault(): Currency 49 | { 50 | return $this->get($this->default); 51 | } 52 | 53 | /** Set the default currency. */ 54 | public function setDefault(string $currency): static 55 | { 56 | $code = $this->getCode($currency); 57 | 58 | $this->ensureCurrencyExists($code); 59 | 60 | $this->default = $code; 61 | 62 | return $this; 63 | } 64 | 65 | /** Get the current currency. */ 66 | public function getCurrent(): Currency 67 | { 68 | return $this->get($this->current ??= $this->resolveCurrent() ?? $this->default); 69 | } 70 | 71 | /** Set the current currency. */ 72 | public function setCurrent(Currency|string $currency): static 73 | { 74 | $code = $this->getCode($currency); 75 | 76 | $this->ensureCurrencyExists($code); 77 | 78 | $this->storeCurrent($this->current = $code); 79 | 80 | return $this; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Exceptions/CannotExtractCurrencyException.php: -------------------------------------------------------------------------------- 1 | */ 12 | final class Money implements JsonSerializable, Arrayable, Wireable 13 | { 14 | protected int $value; 15 | protected Currency $currency; 16 | 17 | /** Create a new Money instance. */ 18 | public function __construct(int $value, Currency|string|null $currency = null) 19 | { 20 | $this->value = $value; 21 | $this->currency = currency($currency); 22 | } 23 | 24 | /** Create a new Money instance with the same currency. */ 25 | public function new(int $value): self 26 | { 27 | return new self($value, $this->currency); 28 | } 29 | 30 | /** Create a new Money instance with the same currency from a decimal value. */ 31 | protected function newFromDecimal(float $decimal): self 32 | { 33 | return static::fromDecimal($decimal, $this->currency); 34 | } 35 | 36 | /** Create a Money instance from a decimal value. */ 37 | public static function fromDecimal(float $decimal, Currency|string|null $currency = null): self 38 | { 39 | return new static( 40 | (int) round($decimal * pow(10, currency($currency)->mathDecimals())), 41 | currency($currency) 42 | ); 43 | } 44 | 45 | /** Add money (in base value). */ 46 | public function add(int $value): self 47 | { 48 | return $this->new($this->value + $value); 49 | } 50 | 51 | /** Add money (from another Money instance). */ 52 | public function addMoney(self $money): self 53 | { 54 | return $this->add( 55 | $money->convertTo($this->currency)->value() 56 | ); 57 | } 58 | 59 | /** Add money (in decimal value). */ 60 | public function addDecimal(float $decimal): self 61 | { 62 | return $this->addMoney( 63 | $this->newFromDecimal($decimal) 64 | ); 65 | } 66 | 67 | /** Subtract money (in base value). */ 68 | public function subtract(int $value): self 69 | { 70 | return $this->new($this->value - $value); 71 | } 72 | 73 | /** Subtract money (in decimal value). */ 74 | public function subtractDecimal(float $decimal): self 75 | { 76 | return $this->subtractMoney( 77 | $this->newFromDecimal($decimal) 78 | ); 79 | } 80 | 81 | /** Subtract money (of another Money instance). */ 82 | public function subtractMoney(self $money): self 83 | { 84 | return $this->subtract( 85 | $money->convertTo($this->currency)->value() 86 | ); 87 | } 88 | 89 | /** Multiply the money by a coefficient. */ 90 | public function multiplyBy(float $coefficient): self 91 | { 92 | return $this->new( 93 | (int) round($this->value * $coefficient) 94 | ); 95 | } 96 | 97 | /** Multiply the money by a coefficient. */ 98 | public function times(float $coefficient): self 99 | { 100 | return $this->multiplyBy($coefficient); 101 | } 102 | 103 | /** Divide the money by a number. */ 104 | public function divideBy(float $number): self 105 | { 106 | if ($number == 0) { 107 | $number = 1; 108 | } 109 | 110 | return $this->new( 111 | (int) round($this->value() / $number) 112 | ); 113 | } 114 | 115 | /** Add a % fee to the money. */ 116 | public function addFee(float $rate): self 117 | { 118 | return $this->multiplyBy( 119 | round(1 + ($rate / 100), $this->currency->mathDecimals()) 120 | ); 121 | } 122 | 123 | /** Add a % tax to the money. */ 124 | public function addTax(float $rate): self 125 | { 126 | return $this->addFee($rate); 127 | } 128 | 129 | /** Subtract a % fee from the money. */ 130 | public function subtractFee(float $rate): self 131 | { 132 | return $this->divideBy( 133 | round(1 + ($rate / 100), $this->currency->mathDecimals()) 134 | ); 135 | } 136 | 137 | /** Subtract a % tax from the money. */ 138 | public function subtractTax(float $rate): self 139 | { 140 | return $this->subtractFee($rate); 141 | } 142 | 143 | /** Get the base value of the money in the used currency. */ 144 | public function value(): int 145 | { 146 | return $this->value; 147 | } 148 | 149 | /** Get the used currency. */ 150 | public function currency(): Currency 151 | { 152 | return $this->currency; 153 | } 154 | 155 | /** Get the decimal representation of the value. */ 156 | public function decimal(): float 157 | { 158 | return $this->value / pow(10, $this->currency->mathDecimals()); 159 | } 160 | 161 | /** Format the value. */ 162 | public function formatted(mixed ...$overrides): string 163 | { 164 | return PriceFormatter::format($this->decimal(), $this->currency, variadic_array($overrides)); 165 | } 166 | 167 | /** Format the raw (unrounded) value. */ 168 | public function rawFormatted(mixed ...$overrides): string 169 | { 170 | return $this->formatted(array_merge(variadic_array($overrides), [ 171 | 'displayDecimals' => $this->currency->mathDecimals(), 172 | ])); 173 | } 174 | 175 | /** 176 | * Create a Money instance from a formatted string. 177 | * 178 | * @param string $formatted The string formatted using the `formatted()` or `rawFormatted()` method. 179 | * @param Currency|string|null $currency The currency to use when passing the overrides. If not provided, the currency of the formatted string is used. 180 | * @param array ...$overrides The overrides used when formatting the money instance. 181 | */ 182 | public static function fromFormatted(string $formatted, Currency|string|null $currency = null, mixed ...$overrides): self 183 | { 184 | $currency = isset($currency) 185 | ? currency($currency) 186 | : PriceFormatter::extractCurrency($formatted); 187 | 188 | $decimal = PriceFormatter::resolve($formatted, $currency, variadic_array($overrides)); 189 | 190 | return static::fromDecimal($decimal, currency($currency)); 191 | } 192 | 193 | /** Get the string representation of the Money instance. */ 194 | public function __toString(): string 195 | { 196 | return $this->formatted(); 197 | } 198 | 199 | /** Convert the instance to an array representation. */ 200 | public function toArray(): array 201 | { 202 | return [ 203 | 'value' => $this->value, 204 | 'currency' => $this->currency->code(), 205 | ]; 206 | } 207 | 208 | /** Check if the value equals the value of another Money instance, adjusted for currency. */ 209 | public function equals(self $money): bool 210 | { 211 | return $this->valueInDefaultCurrency() === $money->valueInDefaultCurrency(); 212 | } 213 | 214 | /** Check if the value and currency match another Money instance. */ 215 | public function is(self $money): bool 216 | { 217 | return $this->currency()->code() === $money->currency()->code() 218 | && $this->equals($money); 219 | } 220 | 221 | /** Get the data used for JSON serializing this object. */ 222 | public function jsonSerialize(): array 223 | { 224 | return $this->toArray(); 225 | } 226 | 227 | /** Convert the instance to JSON */ 228 | public function toJson(): string 229 | { 230 | return json_encode($this, JSON_THROW_ON_ERROR); 231 | } 232 | 233 | /** Instantiate Money from JSON. */ 234 | public static function fromJson(string|array $json): self 235 | { 236 | if (is_string($json)) { 237 | $json = json_decode($json, true, flags: JSON_THROW_ON_ERROR); 238 | } 239 | 240 | return new static($json['value'], $json['currency']); 241 | } 242 | 243 | /** Value in the default currency. */ 244 | public function valueInDefaultCurrency(): int 245 | { 246 | $mathDecimalDifference = $this->currency->mathDecimals() - currencies()->getDefault()->mathDecimals(); 247 | 248 | return $this 249 | ->divideBy($this->currency->rate()) 250 | ->divideBy(pow(10, $mathDecimalDifference)) 251 | ->value(); 252 | } 253 | 254 | /** Convert the money to a different currency. */ 255 | public function convertTo(Currency|string $currency): self 256 | { 257 | // We're converting from the current currency to the default currency, and then to the intended currency 258 | $newCurrency = currency($currency); 259 | $mathDecimalDifference = $newCurrency->mathDecimals() - currencies()->getDefault()->mathDecimals(); 260 | 261 | return new static( 262 | (int) round($this->valueInDefaultCurrency() * $newCurrency->rate() * pow(10, $mathDecimalDifference), 0), 263 | $currency 264 | ); 265 | } 266 | 267 | /** Convert the Money to the current currency. */ 268 | public function toCurrent(): self 269 | { 270 | return $this->convertTo(currencies()->getCurrent()); 271 | } 272 | 273 | /** Convert the Money to the current currency. */ 274 | public function toDefault(): self 275 | { 276 | return $this->convertTo(currencies()->getDefault()); 277 | } 278 | 279 | /** Round the Money to a custom precision. */ 280 | public function rounded(?int $precision = null): self 281 | { 282 | $precision ??= $this->currency->rounding(); 283 | 284 | return $this->new(((int) round($this->value, -$precision))); 285 | } 286 | 287 | /** Get the money rounding (typically this is the difference between the actual value and the formatted value.) */ 288 | public function rounding(): int 289 | { 290 | return $this->rounded()->value() - $this->value(); 291 | } 292 | 293 | /** Get the cents from the decimal value. */ 294 | public function cents(): self 295 | { 296 | return $this->newFromDecimal( 297 | $this->decimal() - floor($this->decimal()) 298 | ); 299 | } 300 | 301 | public function toLivewire() 302 | { 303 | return $this->toArray(); 304 | } 305 | 306 | public static function fromLivewire($value) 307 | { 308 | return static::fromJson($value); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/MoneyServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(CurrencyManager::class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/PriceFormatter.php: -------------------------------------------------------------------------------- 1 | toArray(), $overrides) 17 | ); 18 | 19 | $decimal = number_format( 20 | $decimal, 21 | $currency->displayDecimals(), 22 | $currency->decimalSeparator(), 23 | $currency->thousandsSeparator(), 24 | ); 25 | 26 | if ($currency->trimTrailingDecimalZeros()) { 27 | // Remove trailing zeros from the formatted string 28 | $decimal = rtrim($decimal, '0'); 29 | 30 | // Once there are no more decimal values, remove the decimal separator as well 31 | $decimal = rtrim($decimal, $currency->decimalSeparator()); 32 | } 33 | 34 | return $currency->prefix() . $decimal . $currency->suffix(); 35 | } 36 | 37 | /** Extract the decimal from the formatted string as per the currency's specifications. */ 38 | public static function resolve(string $formatted, Currency $currency, array $overrides = []): float 39 | { 40 | $currency = Currency::fromArray( 41 | array_merge(currency($currency)->toArray(), $overrides) 42 | ); 43 | 44 | $formatted = ltrim($formatted, $currency->prefix()); 45 | $formatted = rtrim($formatted, $currency->suffix()); 46 | 47 | $removeNonDigits = preg_replace('/[^\d' . preg_quote($currency->decimalSeparator(), '/') . ']/', '', $formatted); 48 | 49 | if (! is_string($removeNonDigits)) { 50 | throw new Exception('The formatted string could not be resolved to a valid number.'); 51 | } 52 | 53 | return (float) str_replace($currency->decimalSeparator(), '.', $removeNonDigits); 54 | } 55 | 56 | /** Tries to extract the currency from the formatted string. */ 57 | public static function extractCurrency(string $formatted): Currency 58 | { 59 | $possibleCurrency = null; 60 | 61 | foreach (currencies()->all() as $currency) { 62 | if ( 63 | str_starts_with($formatted, $currency->prefix()) 64 | && str_ends_with($formatted, $currency->suffix()) 65 | ) { 66 | if ($possibleCurrency) { 67 | throw new CannotExtractCurrencyException("Multiple currencies are using the same prefix and suffix as '$formatted'. Please specify the currency of the formatted string."); 68 | } 69 | 70 | $possibleCurrency = $currency; 71 | } 72 | } 73 | 74 | return $possibleCurrency ?? throw new CannotExtractCurrencyException("None of the currencies are using the prefix and suffix that would match with the formatted string '$formatted'."); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Wireable.php: -------------------------------------------------------------------------------- 1 | getDefault()); 14 | } 15 | } 16 | 17 | if (! function_exists('currency')) { 18 | /** Fetch a currency. If no argument is provided, the current currency will be returned. */ 19 | function currency(Currency|string|null $currency = null): Currency 20 | { 21 | if ($currency) { 22 | return $currency instanceof Currency 23 | ? $currency 24 | : currencies()->get($currency); 25 | } 26 | 27 | return currencies()->getCurrent(); 28 | } 29 | } 30 | 31 | if (! function_exists('currencies')) { 32 | /** Get the CurrencyManager instance. */ 33 | function currencies(): CurrencyManager 34 | { 35 | return app(CurrencyManager::class); 36 | } 37 | } 38 | --------------------------------------------------------------------------------