5 use Brick\Money\Currency
;
7 use Brick\Math\RoundingMode
;
11 use CRM_Utils_Constant
;
13 use Brick\Money\Context\AutoContext
;
19 * This class provides standardised formatting
26 * @param string|int|float|BigDecimal $amount
27 * @param string|null $currency
28 * Currency, defaults to site currency if not provided.
29 * @param string|null $locale
33 * @throws \CRM_Core_Exception
35 * @noinspection PhpDocMissingThrowsInspection
36 * @noinspection PhpUnhandledExceptionInspection
38 public function money($amount, ?
string $currency = NULL, ?
string $locale = NULL): string {
39 if (($amount = $this->checkAndConvertAmount($amount)) === '') {
43 $currency = Civi
::settings()->get('defaultCurrency');
45 if (!isset($locale)) {
46 $locale = CRM_Core_I18n
::getLocale();
48 $money = Money
::of($amount, $currency, NULL, RoundingMode
::HALF_UP
);
49 $formatter = $this->getMoneyFormatter($currency, $locale);
50 return $money->formatWith($formatter);
54 * Get a formatted number.
56 * @param string|int|float|Money $amount
57 * Amount in a machine money format.
58 * @param string|null $locale
59 * @param array $attributes
60 * Additional values supported by NumberFormatter
61 * https://www.php.net/manual/en/class.numberformatter.php
62 * By default this will set it to round to 8 places and not
66 * @throws \CRM_Core_Exception
68 public function number($amount, ?
string $locale = NULL, array $attributes = [
69 NumberFormatter
::MIN_FRACTION_DIGITS
=> 0,
70 NumberFormatter
::MAX_FRACTION_DIGITS
=> 8,
72 if (($amount = $this->checkAndConvertAmount($amount)) === '') {
75 $formatter = $this->getMoneyFormatter(NULL, $locale, NumberFormatter
::DECIMAL
, $attributes);
76 return $formatter->format($amount);
80 * Get a number formatted with rounding expectations derived from the currency.
82 * @param string|float|int $amount
83 * @param string $currency
84 * @param string|null $locale
88 * @noinspection PhpDocMissingThrowsInspection
89 * @noinspection PhpUnhandledExceptionInspection
91 public function moneyNumber($amount, string $currency, $locale): string {
92 if (($amount = $this->checkAndConvertAmount($amount)) === '') {
95 $formatter = $this->getMoneyFormatter($currency, $locale, NumberFormatter
::DECIMAL
);
96 $money = Money
::of($amount, $currency, NULL, RoundingMode
::HALF_UP
);
97 return $money->formatWith($formatter);
101 * Get a money value with formatting but not rounding.
103 * @param string|float|int $amount
104 * @param string|null $currency
105 * @param string|null $locale
109 * @noinspection PhpDocMissingThrowsInspection
110 * @noinspection PhpUnhandledExceptionInspection
112 public function moneyLong($amount, ?
string $currency, ?
string $locale): string {
113 if (($amount = $this->checkAndConvertAmount($amount)) === '') {
116 $formatter = $this->getMoneyFormatter($currency, $locale, NumberFormatter
::CURRENCY
, [
117 NumberFormatter
::MAX_FRACTION_DIGITS
=> 9,
119 $money = Money
::of($amount, $currency, new AutoContext());
120 return $money->formatWith($formatter);
124 * Get a number with minimum decimal places based on the currency but no rounding.
126 * @param string|float|int $amount
127 * @param string|null $currency
128 * @param string|null $locale
132 * @noinspection PhpDocMissingThrowsInspection
133 * @noinspection PhpUnhandledExceptionInspection
135 public function moneyNumberLong($amount, ?
string $currency, ?
string $locale): string {
136 if (($amount = $this->checkAndConvertAmount($amount)) === '') {
139 $formatter = $this->getMoneyFormatter($currency, $locale, NumberFormatter
::DECIMAL
, [
140 NumberFormatter
::MAX_FRACTION_DIGITS
=> 9,
142 $money = Money
::of($amount, $currency, new AutoContext());
143 return $money->formatWith($formatter);
147 * Should we use the configured thousand & decimal separators.
149 * The goal is to phase this into being FALSE - but for now
150 * we are looking at how to manage an 'opt in'
152 protected function isUseSeparatorSettings(): bool {
153 return !CRM_Utils_Constant
::value('IGNORE_SEPARATOR_CONFIG');
157 * Get the money formatter for when we are using configured thousand separators.
159 * Our intent is to phase out these settings in favour of deriving them from the locale.
161 * @param string|null $currency
162 * @param string|null $locale
164 * See https://www.php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants
165 * @param array $attributes
166 * See https://www.php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatattribute
168 * @return \NumberFormatter
170 * @noinspection PhpDocMissingThrowsInspection
171 * @noinspection PhpUnhandledExceptionInspection
173 public function getMoneyFormatter(?
string $currency = NULL, ?
string $locale = NULL, int $style = NumberFormatter
::CURRENCY
, array $attributes = []): NumberFormatter
{
175 $currency = Civi
::settings()->get('defaultCurrency');
177 $cacheKey = __CLASS__
. $currency . '_' . $locale . '_' . $style . (!empty($attributes) ?
md5(json_encode($attributes)) : '');
178 if (!isset(\Civi
::$statics[$cacheKey])) {
179 $formatter = new NumberFormatter($locale, $style);
181 if (!isset($attributes[NumberFormatter
::MIN_FRACTION_DIGITS
])) {
182 $attributes[NumberFormatter
::MIN_FRACTION_DIGITS
] = Currency
::of($currency)
183 ->getDefaultFractionDigits();
185 if (!isset($attributes[NumberFormatter
::MAX_FRACTION_DIGITS
])) {
186 $attributes[NumberFormatter
::MAX_FRACTION_DIGITS
] = Currency
::of($currency)
187 ->getDefaultFractionDigits();
190 foreach ($attributes as $attribute => $value) {
191 $formatter->setAttribute($attribute, $value);
193 if ($locale === CRM_Core_I18n
::getLocale() && $this->isUseSeparatorSettings()) {
194 $formatter->setSymbol(NumberFormatter
::MONETARY_GROUPING_SEPARATOR_SYMBOL
, CRM_Core_Config
::singleton()->monetaryThousandSeparator
);
195 $formatter->setSymbol(NumberFormatter
::MONETARY_SEPARATOR_SYMBOL
, CRM_Core_Config
::singleton()->monetaryDecimalPoint
);
197 \Civi
::$statics[$cacheKey] = $formatter;
199 return \Civi
::$statics[$cacheKey];
203 * Since the input can be various data types and values, we need to handle
204 * them before passing on to the Brick libraries which would throw exceptions
205 * for ones that we are ok just converting to the empty string.
207 * @param string|int|float|BigDecimal $amount
209 * Either the empty string if an empty-ish value, or the original amount as a string.
210 * @throws \CRM_Core_Exception
212 private function checkAndConvertAmount($amount): string {
213 // Empty value => empty string
214 // FALSE should be an error but some smarty variables are filled with FALSE to avoid ENOTICES.
215 if (is_null($amount) ||
$amount === '' ||
$amount === FALSE) {
218 // Verify the amount is a number or numeric string/object.
219 // We cast to string because it can be a BigDecimal object.
220 if ($amount === TRUE ||
!is_numeric((string) $amount)) {
221 throw new \
CRM_Core_Exception('Invalid value for type money');
223 return (string) $amount;