Skip to content

Commit 0cd2f0e

Browse files
author
Slava
authored
Annotate immutability of Money and mutation-free of Currency (#17)
* Refactor Currency to be mostly mutation-free * Annotate Money as immutable * Update CHANGELOG * Remove pointless casting * Run tests paralel
1 parent 476ec17 commit 0cd2f0e

File tree

5 files changed

+106
-42
lines changed

5 files changed

+106
-42
lines changed

.github/workflows/workflow.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ubuntu-latest
1111

1212
strategy:
13-
max-parallel: 1
13+
fail-fast: false
1414
matrix:
1515
php-version: ['7.3', '7.4', '8.0']
1616

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ Security - in case of vulnerabilities.
1616

1717
## [Unreleased]
1818

19+
## 1.2.0 (2021-10-08)
20+
21+
### Changed
22+
23+
+ Annotate the immutability of `Money`.
24+
+ Annotate the mutation-free behavior of all methods of `Currency`, except `addCurrency`.
25+
1926
## 1.1.2 (2021-04-19)
2027

2128
### Changed

src/Currency.php

+73-12
Original file line numberDiff line numberDiff line change
@@ -1526,6 +1526,36 @@ final class Currency implements JsonSerializable
15261526
*/
15271527
private $currencyCode;
15281528

1529+
/**
1530+
* @var string
1531+
*/
1532+
private $displayName;
1533+
1534+
/**
1535+
* @var int
1536+
*/
1537+
private $numericCode;
1538+
1539+
/**
1540+
* @var int
1541+
*/
1542+
private $defaultFractionDigits;
1543+
1544+
/**
1545+
* @var int
1546+
*/
1547+
private $subUnit;
1548+
1549+
/**
1550+
* @var string
1551+
*/
1552+
private $sign;
1553+
1554+
/**
1555+
* @var bool
1556+
*/
1557+
private $deprecated;
1558+
15291559
public function __construct(string $currencyCode)
15301560
{
15311561
$currencyCode = mb_strtoupper($currencyCode);
@@ -1534,15 +1564,27 @@ public function __construct(string $currencyCode)
15341564
throw new InvalidArgumentException(sprintf('Unknown currency code "%s"', $currencyCode));
15351565
}
15361566

1567+
assert(isset(
1568+
self::$currencies[$currencyCode]['display_name'],
1569+
self::$currencies[$currencyCode]['numeric_code'],
1570+
self::$currencies[$currencyCode]['default_fraction_digits'],
1571+
self::$currencies[$currencyCode]['sub_unit'],
1572+
self::$currencies[$currencyCode]['sign'],
1573+
self::$currencies[$currencyCode]['pretty_print_format'],
1574+
self::$currencies[$currencyCode]['negative_pretty_print_format'],
1575+
self::$currencies[$currencyCode]['deprecated'],
1576+
));
1577+
15371578
$this->currencyCode = $currencyCode;
1579+
$this->displayName = self::$currencies[$currencyCode]['display_name'];
1580+
$this->numericCode = self::$currencies[$currencyCode]['numeric_code'];
1581+
$this->defaultFractionDigits = self::$currencies[$currencyCode]['default_fraction_digits'];
1582+
$this->subUnit = self::$currencies[$currencyCode]['sub_unit'];
1583+
$this->sign = self::$currencies[$currencyCode]['sign'];
1584+
$this->deprecated = self::$currencies[$currencyCode]['deprecated'];
15381585
}
15391586

1540-
/**
1541-
* Returns the ISO 4217 currency code of this currency.
1542-
*
1543-
* @return string
1544-
*/
1545-
public function __toString()
1587+
public function __toString(): string
15461588
{
15471589
return $this->getCurrencyCode();
15481590
}
@@ -1607,6 +1649,8 @@ public static function getCurrenciesIncludingDeprecated(): array
16071649

16081650
/**
16091651
* Returns the ISO 4217 currency code of this currency.
1652+
*
1653+
* @psalm-mutation-free
16101654
*/
16111655
public function getCurrencyCode(): string
16121656
{
@@ -1616,60 +1660,77 @@ public function getCurrencyCode(): string
16161660
/**
16171661
* Returns the default number of fraction digits used with this
16181662
* currency.
1663+
*
1664+
* @psalm-mutation-free
16191665
*/
16201666
public function getDefaultFractionDigits(): int
16211667
{
1622-
return self::$currencies[$this->getCurrencyCode()]['default_fraction_digits'];
1668+
return $this->defaultFractionDigits;
16231669
}
16241670

16251671
/**
16261672
* Returns the name that is suitable for displaying this currency.
1673+
*
1674+
* @psalm-mutation-free
16271675
*/
16281676
public function getDisplayName(): string
16291677
{
1630-
return self::$currencies[$this->getCurrencyCode()]['display_name'];
1678+
return $this->displayName;
16311679
}
16321680

16331681
/**
16341682
* Returns the ISO 4217 numeric code of this currency.
1683+
*
1684+
* @psalm-mutation-free
16351685
*/
16361686
public function getNumericCode(): int
16371687
{
1638-
return self::$currencies[$this->getCurrencyCode()]['numeric_code'];
1688+
return $this->numericCode;
16391689
}
16401690

16411691
/**
16421692
* Returns the minor currency sub units.
1693+
*
1694+
* @psalm-mutation-free
16431695
*/
16441696
public function getSubUnit(): int
16451697
{
1646-
return self::$currencies[$this->getCurrencyCode()]['sub_unit'];
1698+
return $this->subUnit;
16471699
}
16481700

16491701
/**
16501702
* Returns the currency sign.
1703+
*
1704+
* @psalm-mutation-free
16511705
*/
16521706
public function getSign(): string
16531707
{
1654-
return self::$currencies[$this->getCurrencyCode()]['sign'];
1708+
return $this->sign;
16551709
}
16561710

16571711
/**
16581712
* Returns the deprecation status.
1713+
*
1714+
* @psalm-mutation-free
16591715
*/
16601716
public function isDeprecated(): bool
16611717
{
1662-
return self::$currencies[$this->getCurrencyCode()]['deprecated'];
1718+
return $this->deprecated;
16631719
}
16641720

16651721
/**
16661722
* {@inheritdoc}
1723+
*
1724+
* @psalm-mutation-free
16671725
*/
16681726
public function jsonSerialize(): string
16691727
{
16701728
return $this->getCurrencyCode();
16711729
}
16721730

1731+
/**
1732+
* @psalm-mutation-free
1733+
*/
16731734
public function equals(self $currency): bool
16741735
{
16751736
return $this->getCurrencyCode() === $currency->getCurrencyCode();

src/Money.php

+23-29
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
* @see http://www.github.com/sebastianbergmann/money
1818
* @see http://martinfowler.com/bliki/ValueObject.html
1919
* @see http://martinfowler.com/eaaCatalog/money.html
20+
*
21+
* @psalm-immutable
2022
*/
2123
final class Money implements JsonSerializable
2224
{
@@ -195,9 +197,7 @@ public function getCurrency(): Currency
195197
*/
196198
public function add(self $other): self
197199
{
198-
$this->assertSameCurrency($other);
199-
200-
$value = $this->getAmount() + $other->getAmount();
200+
$value = $this->getAmount() + $this->castOtherAmountToInt($other);
201201

202202
if (!is_int($value)) {
203203
throw new OverflowException('Value reached maximum amount');
@@ -219,9 +219,7 @@ public function add(self $other): self
219219
*/
220220
public function subtract(self $other): self
221221
{
222-
$this->assertSameCurrency($other);
223-
224-
$value = $this->getAmount() - $other->getAmount();
222+
$value = $this->getAmount() - $this->castOtherAmountToInt($other);
225223

226224
if (!is_int($value)) {
227225
throw new UnderflowException('Value reached minimum amount');
@@ -254,11 +252,7 @@ public function negate(): self
254252
*/
255253
public function multiply(float $factor, int $roundingMode = PHP_ROUND_HALF_UP): self
256254
{
257-
$this->assertRoundingMode($roundingMode);
258-
259-
$amount = round($factor * $this->getAmount(), 0, $roundingMode);
260-
261-
return $this->changeFloatAmount($amount);
255+
return $this->changeFloatAmount($this->roundValueByMode($factor * $this->getAmount(), $roundingMode));
262256
}
263257

264258
/**
@@ -342,12 +336,12 @@ public function allocateByRatios(array $ratios): array
342336
*
343337
* @see https://github.com/sebastianbergmann/money/issues/27
344338
*
345-
* @param float $percentage
339+
* @param float|int $percentage
346340
* @param int $roundingMode
347341
*
348342
* @return self[]
349343
*/
350-
public function extractPercentage($percentage, $roundingMode = PHP_ROUND_HALF_UP): array
344+
public function extractPercentage($percentage, int $roundingMode = PHP_ROUND_HALF_UP): array
351345
{
352346
$amount = round($this->getAmount() / (100 + $percentage) * $percentage, 0, $roundingMode);
353347
$percentage = $this->changeFloatAmount($amount);
@@ -373,9 +367,7 @@ public function extractPercentage($percentage, $roundingMode = PHP_ROUND_HALF_UP
373367
*/
374368
public function compareTo(self $other): int
375369
{
376-
$this->assertSameCurrency($other);
377-
378-
return $this->getAmount() <=> $other->getAmount();
370+
return $this->getAmount() <=> $this->castOtherAmountToInt($other);
379371
}
380372

381373
/**
@@ -480,39 +472,41 @@ public function isNegative(): bool
480472
* Convert currency to a target currency given a conversion rate and rounding mode.
481473
*
482474
* @param Currency $targetCurrency
483-
* @param $conversionRate
484-
* @param $roundingMode
475+
* @param float $conversionRate
476+
* @param int $roundingMode
485477
*
486478
* @return self
487479
*/
488480
public function convert(Currency $targetCurrency, float $conversionRate, int $roundingMode): self
489481
{
490-
$this->assertRoundingMode($roundingMode);
491-
492-
$targetAmount = $this->castToInt(round($conversionRate * $this->getAmount(), 0, $roundingMode));
493-
494-
return new self($targetAmount, $targetCurrency);
482+
return new self(
483+
$this->castToInt($this->roundValueByMode($conversionRate * $this->getAmount(), $roundingMode)),
484+
$targetCurrency,
485+
);
495486
}
496487

497488
/**
498-
* Asserts that rounding mode is a valid integer value.
499-
*
489+
* @param float|int $value
500490
* @param int $roundingMode
501491
*
502-
* @throws InvalidArgumentException
492+
* @return float
503493
*/
504-
private function assertRoundingMode(int $roundingMode): void
494+
private function roundValueByMode($value, int $roundingMode): float
505495
{
506496
if (!in_array($roundingMode, self::ROUNDING_MODES, true)) {
507497
throw new InvalidArgumentException('$roundingMode must be a valid rounding mode (PHP_ROUND_*)');
508498
}
499+
500+
return round($value, 0, $roundingMode);
509501
}
510502

511-
private function assertSameCurrency(self $other): void
503+
private function castOtherAmountToInt(self $amount): int
512504
{
513-
if (!$this->getCurrency()->equals($other->getCurrency())) {
505+
if (!$this->getCurrency()->equals($amount->getCurrency())) {
514506
throw new CurrencyMismatchException();
515507
}
508+
509+
return $amount->getAmount();
516510
}
517511

518512
/**

tests/MoneyTest.php

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
/**
1111
* Class MoneyTest.
12+
*
13+
* @psalm-suppress UnusedMethodCall Some methods are called with expectation of exceptions.
1214
*/
1315
class MoneyTest extends TestCase
1416
{

0 commit comments

Comments
 (0)