├── database └── .gitkeep ├── src ├── Facades │ └── .gitkeep ├── MoneyServiceProvider.php ├── Rules │ └── ValidMoney.php ├── MoneyParser.php └── MoneyCast.php ├── resources ├── views │ └── .gitkeep └── lang │ ├── en │ └── validation.php │ ├── it │ └── validation.php │ └── fr │ └── validation.php ├── config └── money.php ├── pint.json ├── CHANGELOG.md ├── LICENSE.md ├── composer.json └── README.md /database/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Facades/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/money.php: -------------------------------------------------------------------------------- 1 | 'USD', 7 | ]; 8 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "declare_strict_types": true, 5 | "explicit_string_variable": true, 6 | "mb_str_functions": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /resources/lang/en/validation.php: -------------------------------------------------------------------------------- 1 | 'The value is not a valid money', 7 | 'money_positive' => 'The value is not a valid positive money', 8 | 'money_positive_or_zero' => 'The value is not a valid positive or zero money', 9 | 'money_negative' => 'The value is not a valid negative money', 10 | 'money_negative_or_zero' => 'The value is not a valid negative or zero money', 11 | 'money_min' => 'The value must be greater than :value', 12 | 'money_max' => 'The value must be less than :value', 13 | ]; 14 | -------------------------------------------------------------------------------- /resources/lang/it/validation.php: -------------------------------------------------------------------------------- 1 | 'Il valore non è un importo valido', 7 | 'money_positive' => 'Questo valore non è un importo maggiore di 0', 8 | 'money_positive_or_zero' => 'Questo valore non è un importo maggiore o uguale a 0', 9 | 'money_negative' => 'Questo valore non è un importo minore di 0', 10 | 'money_negative_or_zero' => 'Questo valore non è un importo minore o uguale a 0', 11 | 'money_min' => 'Il valore deve essere maggiore di :value', 12 | 'money_max' => 'Il valore deve essere minore di :value', 13 | ]; 14 | -------------------------------------------------------------------------------- /resources/lang/fr/validation.php: -------------------------------------------------------------------------------- 1 | "Cette valeur n'est pas valide", 7 | 'money_positive' => "Cette valeur n'est pas valide et supérieure à zéro", 8 | 'money_positive_or_zero' => "Cette valeur n'est pas valide et supérieure ou égale à zéro", 9 | 'money_negative' => "Cette valeur n'est pas valide et inférieure à zéro", 10 | 'money_negative_or_zero' => "Cette valeur n'est pas valide et inférieure ou égale à zéro", 11 | 'money_min' => 'Cette valeur doit être supérieur à :value', 12 | 'money_max' => 'Cette valeur doit être inférieur à :value', 13 | ]; 14 | -------------------------------------------------------------------------------- /src/MoneyServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-money') 21 | ->hasTranslations() 22 | ->hasConfigFile(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-money` will be documented in this file. 4 | 5 | ## v1.0.4 Laravel 11 - 2024-03-16 6 | 7 | ### What's Changed 8 | 9 | - Bump ramsey/composer-install from 2 to 3 by @dependabot in https://github.com/ElegantEngineeringTech/laravel-money/pull/16 10 | 11 | **Full Changelog**: https://github.com/ElegantEngineeringTech/laravel-money/compare/v1.0.3...v1.0.4 12 | 13 | ## v1.0.3 Laravel 11 - 2024-03-16 14 | 15 | ### What's Changed 16 | 17 | - Bump aglipanci/laravel-pint-action from 2.3.0 to 2.3.1 by @dependabot in https://github.com/ElegantEngineeringTech/laravel-money/pull/15 18 | 19 | **Full Changelog**: https://github.com/ElegantEngineeringTech/laravel-money/compare/v1.0.2...v1.0.3 20 | 21 | ## v1.0.1 Support negative amount - 2023-09-19 22 | 23 | Add support for negative amount 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) elegantly 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Rules/ValidMoney.php: -------------------------------------------------------------------------------- 1 | nullable && $money === null) { 35 | $fail('money::validation.money')->translate(); 36 | } 37 | 38 | if ($money) { 39 | if ( 40 | $this->min !== null && 41 | $money->isLessThan($this->min) 42 | ) { 43 | $fail('money::validation.money_min')->translate([ 44 | 'value' => $this->min, 45 | ]); 46 | } 47 | 48 | if ( 49 | $this->max !== null && 50 | $money->isGreaterThan($this->max) 51 | ) { 52 | $fail('money::validation.money_max')->translate([ 53 | 'value' => $this->max, 54 | ]); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/MoneyParser.php: -------------------------------------------------------------------------------- 1 | trim()->replace([',', ' '], '')->value(); 47 | 48 | if (blank($value)) { 49 | return null; 50 | } 51 | 52 | /** 53 | * Not found currency or amount will return "" and not null 54 | */ 55 | preg_match("/(?[A-Z]{3})? ?(?[-\d,\.]*)/", $value, $matches); 56 | /** @var array{ currency: string, amount: string } $matches */ 57 | $amount = $matches['amount']; 58 | $currency = $matches['currency'] ?: $currency; 59 | 60 | if (blank($amount)) { 61 | return null; 62 | } 63 | 64 | return Money::of( 65 | amount: $amount, 66 | currency: $currency, 67 | roundingMode: RoundingMode::HALF_EVEN 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elegantly/laravel-money", 3 | "description": "Use Brick/Money in your Laravel app", 4 | "keywords": [ 5 | "elegantly", 6 | "laravel", 7 | "laravel-money", 8 | "money" 9 | ], 10 | "homepage": "https://github.com/ElegantEngineeringTech/laravel-money", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Quentin Gabriele", 15 | "email": "quentin.gabriele@gmail.com", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.1", 21 | "brick/money": "^0.10.0", 22 | "illuminate/contracts": "^10.0||^11.0||^12.0", 23 | "spatie/laravel-package-tools": "^1.13.0" 24 | }, 25 | "require-dev": { 26 | "laravel/pint": "^1.14", 27 | "nunomaduro/collision": "^8.1.1||^7.10.0", 28 | "larastan/larastan": "^3.0", 29 | "orchestra/testbench": "^8.22.0||^9.0.0||^10.0.0", 30 | "pestphp/pest": "^3.0||^4.0", 31 | "pestphp/pest-plugin-arch": "^3.0||^4.0", 32 | "pestphp/pest-plugin-laravel": "^3.0||^4.0", 33 | "phpstan/extension-installer": "^1.3||^2.0", 34 | "phpstan/phpstan-deprecation-rules": "^1.1||^2.0", 35 | "phpstan/phpstan-phpunit": "^1.3||^2.0" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "Elegantly\\Money\\": "src" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Elegantly\\Money\\Tests\\": "tests" 45 | } 46 | }, 47 | "scripts": { 48 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 49 | "analyse": "vendor/bin/phpstan analyse", 50 | "test": "vendor/bin/pest", 51 | "test-coverage": "vendor/bin/pest --coverage", 52 | "format": "vendor/bin/pint" 53 | }, 54 | "config": { 55 | "sort-packages": true, 56 | "allow-plugins": { 57 | "pestphp/pest-plugin": true, 58 | "phpstan/extension-installer": true 59 | } 60 | }, 61 | "extra": { 62 | "laravel": { 63 | "providers": [ 64 | "Elegantly\\Money\\MoneyServiceProvider" 65 | ] 66 | } 67 | }, 68 | "minimum-stability": "dev", 69 | "prefer-stable": true 70 | } 71 | -------------------------------------------------------------------------------- /src/MoneyCast.php: -------------------------------------------------------------------------------- 1 | > 17 | */ 18 | class MoneyCast implements CastsAttributes, SerializesCastableAttributes 19 | { 20 | /** 21 | * @param ?string $currencyOrAttribute The currency code or the model attribute storing the currency code. 22 | * Usage examples: 23 | * - MoneyCast::class.':currency' (Currency stored in a model attribute) 24 | * - MoneyCast::class.':EUR' (Fixed currency set to EUR) 25 | */ 26 | public function __construct( 27 | protected ?string $currencyOrAttribute = null 28 | ) { 29 | // No initialization required 30 | } 31 | 32 | public static function of(string $currencyOrAttribute): string 33 | { 34 | return static::class.':'.$currencyOrAttribute; 35 | } 36 | 37 | /** 38 | * @param array $attributes The model's attributes. 39 | */ 40 | public function isCurrencyAttribute(array $attributes): bool 41 | { 42 | return $this->currencyOrAttribute && array_key_exists($this->currencyOrAttribute, $attributes); 43 | } 44 | 45 | public function isCurrencyCode(): bool 46 | { 47 | return $this->currencyOrAttribute && mb_strlen($this->currencyOrAttribute) === 3; 48 | } 49 | 50 | /** 51 | * Retrieve the currency code from the model's attributes. 52 | * 53 | * @param array $attributes The model's attributes. 54 | * @return ?string The currency code, if available. 55 | */ 56 | protected function getCurrencyAttribute(array $attributes): ?string 57 | { 58 | if ($this->isCurrencyAttribute($attributes)) { 59 | /** @var ?string $currency */ 60 | $currency = $attributes[$this->currencyOrAttribute]; 61 | 62 | return $currency; 63 | } 64 | 65 | return null; 66 | } 67 | 68 | /** 69 | * Get the currency instance. 70 | * 71 | * @param array $attributes The model's attributes. 72 | * @return Currency The currency instance. 73 | */ 74 | protected function getCurrency(array $attributes): Currency 75 | { 76 | /** @var string $default */ 77 | $default = config('money.default_currency'); 78 | 79 | if ($currency = $this->getCurrencyAttribute($attributes)) { 80 | return Currency::of($currency); 81 | } 82 | 83 | if ($this->currencyOrAttribute && $this->isCurrencyCode()) { 84 | return Currency::of($this->currencyOrAttribute); 85 | } 86 | 87 | return Currency::of($default); 88 | } 89 | 90 | /** 91 | * Cast the given value into a Money instance. 92 | * 93 | * Money is stored as an integer representing minor units (e.g., cents). 94 | * 95 | * @param ?int $value The stored value. 96 | * @return ?Money The corresponding Money instance or null. 97 | */ 98 | public function get(Model $model, string $key, mixed $value, array $attributes): ?Money 99 | { 100 | if ($value === null) { 101 | return null; 102 | } 103 | 104 | return Money::ofMinor($value, $this->getCurrency($attributes)); 105 | } 106 | 107 | /** 108 | * Prepare the given value for database storage. 109 | * 110 | * Money is stored as an integer in minor units (e.g., cents), ensuring compatibility. 111 | * 112 | * String representations of money are parsed in various formats, such as: 113 | * - "USD 1000.00" => $1,000.00 114 | * - "USD 1000" => $1000 115 | * - "USD 10.00" => $10.00 116 | * - "USD 1,000" => $1000 (commas are ignored) 117 | * 118 | * @param null|int|float|string|Money $value The monetary value to store. 119 | * @return array The formatted data for storage. 120 | */ 121 | public function set(Model $model, string $key, mixed $value, array $attributes): array 122 | { 123 | $money = MoneyParser::parse($value, $this->getCurrency($attributes)); 124 | 125 | if ($this->currencyOrAttribute) { 126 | return [ 127 | $key => $money?->getMinorAmount()->toInt(), 128 | $this->currencyOrAttribute => $money?->getCurrency()->getCurrencyCode(), 129 | ]; 130 | } 131 | 132 | return [$key => $money?->getMinorAmount()->toInt()]; 133 | } 134 | 135 | /** 136 | * Serialize the Money instance into a float for API responses. 137 | * 138 | * @param ?Money $value The Money instance. 139 | * @param array $attributes The model's attributes. 140 | * @return ?float The monetary value as a float or null. 141 | */ 142 | public function serialize(Model $model, string $key, mixed $value, array $attributes): ?float 143 | { 144 | if ($value === null) { 145 | return null; 146 | } 147 | 148 | return $value->getAmount()->toFloat(); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elegant Integration of Brick/Money for Laravel 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/elegantly/laravel-money.svg?style=flat-square)](https://packagist.org/packages/elegantly/laravel-money) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/ElegantEngineeringTech/laravel-money/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/ElegantEngineeringTech/laravel-money/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/elegantly/laravel-money.svg?style=flat-square)](https://packagist.org/packages/elegantly/laravel-money) 6 | 7 | ## Table of Contents 8 | 9 | - [Introduction](#introduction) 10 | - [Features](#features) 11 | - [Installation](#installation) 12 | - [Configuration](#configuration) 13 | - [Storing Money in the Database](#storing-money-in-the-database) 14 | - [Usage](#usage) 15 | 16 | - [Casting with a Currency Column (Recommended)](#casting-with-a-currency-column-recommended) 17 | - [Casting with a Fixed Currency](#casting-with-a-fixed-currency) 18 | - [Parsing Values](#parsing-values) 19 | - [Validation Rule](#validation-rule) 20 | 21 | - [Testing](#testing) 22 | - [Changelog](#changelog) 23 | - [Contributing](#contributing) 24 | - [Security](#security) 25 | - [Credits](#credits) 26 | - [License](#license) 27 | 28 | ## Introduction 29 | 30 | This package provides seamless, expressive integration of [Brick/Money](https://github.com/brick/money) with Laravel. It enables safe, precise handling of monetary values in your application—using value objects, smart casting, robust parsing, and powerful validation tools. 31 | 32 | ## Features 33 | 34 | - **MoneyCast** – Automatically cast Eloquent attributes to `Brick\Money\Money`. 35 | - **MoneyParser** – Convert strings, integers, or floats into `Money` instances safely. 36 | - **ValidMoney Rule** – Validate monetary input with min/max boundaries, type safety, and nullability. 37 | 38 | ## Installation 39 | 40 | Install via Composer: 41 | 42 | ```bash 43 | composer require elegantly/laravel-money 44 | ``` 45 | 46 | ## Configuration 47 | 48 | Publish the configuration file if you need to customize defaults: 49 | 50 | ```bash 51 | php artisan vendor:publish --tag="money-config" 52 | ``` 53 | 54 | Default config (`config/money.php`): 55 | 56 | ```php 57 | return [ 58 | 'default_currency' => 'USD', 59 | ]; 60 | ``` 61 | 62 | ## Storing Money in the Database 63 | 64 | For maximum precision, store money using: 65 | 66 | - a `bigInteger` column for the amount (in the smallest currency unit) 67 | - a `string` column for the ISO currency code 68 | 69 | This avoids floating-point precision issues and ensures accurate calculations. 70 | 71 | Example migration: 72 | 73 | ```php 74 | Schema::create('invoices', function (Blueprint $table) { 75 | $table->id(); 76 | $table->bigInteger('amount'); // e.g., 1000 = $10.00 77 | $table->string('currency', 3); // ISO 4217 code 78 | $table->timestamps(); 79 | }); 80 | ``` 81 | 82 | ## Usage 83 | 84 | ### Casting with a Currency Column (Recommended) 85 | 86 | If your model stores both amount and currency, reference the currency column in the cast: 87 | 88 | ```php 89 | use Elegantly\Money\MoneyCast; 90 | use Brick\Money\Money; 91 | 92 | /** 93 | * @property Money $amount 94 | * @property string $currency 95 | */ 96 | class Invoice extends Model 97 | { 98 | 99 | protected function casts(): array 100 | { 101 | return [ 102 | 'amount' => MoneyCast::of('currency'), 103 | ]; 104 | } 105 | } 106 | ``` 107 | 108 | ### Casting with a Fixed Currency 109 | 110 | If the currency is known and constant, define it directly: 111 | 112 | ```php 113 | use Elegantly\Money\MoneyCast; 114 | use Brick\Money\Money; 115 | 116 | /** 117 | * @property Money $amount 118 | * @property Money $cost 119 | */ 120 | class Invoice extends Model 121 | { 122 | protected function casts(): array 123 | { 124 | return [ 125 | 'cost' => MoneyCast::of('EUR'), 126 | 'price' => MoneyCast::of('USD'), 127 | ]; 128 | } 129 | } 130 | ``` 131 | 132 | ### Parsing Values 133 | 134 | `MoneyParser` converts common numeric and string formats into `Money` instances: 135 | 136 | ```php 137 | use Elegantly\Money\MoneyParser; 138 | 139 | MoneyParser::parse(null, 'EUR'); // null 140 | MoneyParser::parse(110, 'EUR'); // 110.00 € 141 | MoneyParser::parse(100.10, 'EUR'); // 100.10 € 142 | MoneyParser::parse('', 'EUR'); // null 143 | MoneyParser::parse('1', 'EUR'); // 1.00 € 144 | MoneyParser::parse('100.10', 'EUR'); // 100.10 € 145 | ``` 146 | 147 | The parser handles nullability, empty strings, integers, floats, and decimal string formats gracefully. 148 | 149 | ### Validation Rule 150 | 151 | #### Using `ValidMoney` in Livewire 152 | 153 | ```php 154 | namespace App\Livewire; 155 | 156 | use Elegantly\Money\Rules\ValidMoney; 157 | use Livewire\Component; 158 | 159 | class CustomComponent extends Component 160 | { 161 | #[Validate([ 162 | new ValidMoney(nullable: false, min: 0, max: 100), 163 | ])] 164 | public ?int $price = null; 165 | } 166 | ``` 167 | 168 | #### Using `ValidMoney` in Form Requests 169 | 170 | ```php 171 | namespace App\Http\Requests; 172 | 173 | use Elegantly\Money\Rules\ValidMoney; 174 | use Illuminate\Foundation\Http\FormRequest; 175 | 176 | class CustomFormRequest extends FormRequest 177 | { 178 | public function rules() 179 | { 180 | return [ 181 | 'price' => [ 182 | new ValidMoney( 183 | nullable: false, 184 | min: 0, 185 | max: 100 186 | ), 187 | ], 188 | ]; 189 | } 190 | } 191 | ``` 192 | 193 | ## Testing 194 | 195 | Run the test suite: 196 | 197 | ```bash 198 | composer test 199 | ``` 200 | 201 | ## Changelog 202 | 203 | See the [CHANGELOG](CHANGELOG.md) for a full history of updates. 204 | 205 | ## Contributing 206 | 207 | Contributions are welcome! Review the [CONTRIBUTING](CONTRIBUTING.md) file for details. 208 | 209 | ## Security 210 | 211 | If you discover a security vulnerability, please refer to the [security policy](../../security/policy). 212 | 213 | ## Credits 214 | 215 | - [Quentin Gabriele](https://github.com/QuentinGab) 216 | - [All Contributors](../../contributors) 217 | 218 | ## License 219 | 220 | This package is open-source software released under the MIT License. 221 | See [LICENSE.md](LICENSE.md) for details. 222 | --------------------------------------------------------------------------------