diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..22aac70
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,14 @@
+/tests export-ignore
+/vendor export-ignore
+
+/LICENSE export-ignore
+/Makefile export-ignore
+/README.md export-ignore
+/phpmd.xml export-ignore
+/phpunit.xml export-ignore
+/phpstan.neon.dist export-ignore
+/infection.json.dist export-ignore
+
+/.github export-ignore
+/.gitignore export-ignore
+/.gitattributes export-ignore
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 32850ac..105bc90 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,6 +16,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
+ - name: Use PHP 8.2
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.2'
+
- name: Install dependencies
run: composer update --no-progress --optimize-autoloader
@@ -33,6 +38,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
+ - name: Use PHP 8.2
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.2'
+
- name: Install dependencies
run: composer update --no-progress --optimize-autoloader
diff --git a/Makefile b/Makefile
index 9bf35fe..1b3026e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,14 +1,17 @@
DOCKER_RUN = docker run --rm -it --net=host -v ${PWD}:/app -w /app gustavofreze/php:8.2
-.PHONY: configure test test-no-coverage review show-reports clean
+.PHONY: configure test test-file test-no-coverage review show-reports clean
configure:
@${DOCKER_RUN} composer update --optimize-autoloader
-test: review
+test:
@${DOCKER_RUN} composer tests
-test-no-coverage: review
+test-file:
+ @${DOCKER_RUN} composer tests-file-no-coverage ${FILE}
+
+test-no-coverage:
@${DOCKER_RUN} composer tests-no-coverage
review:
@@ -19,4 +22,4 @@ show-reports:
clean:
@sudo chown -R ${USER}:${USER} ${PWD}
- @rm -rf report vendor
+ @rm -rf report vendor .phpunit.cache
diff --git a/composer.json b/composer.json
index 5abcbd7..e90ade4 100644
--- a/composer.json
+++ b/composer.json
@@ -10,8 +10,6 @@
"vo",
"psr",
"money",
- "psr-4",
- "psr-12",
"tiny-blocks",
"value-object"
],
@@ -21,6 +19,10 @@
"homepage": "https://github.com/gustavofreze"
}
],
+ "support": {
+ "issues": "https://github.com/tiny-blocks/money/issues",
+ "source": "https://github.com/tiny-blocks/money"
+ },
"config": {
"sort-packages": true,
"allow-plugins": {
@@ -38,16 +40,18 @@
}
},
"require": {
- "php": "^8.1||^8.2",
- "tiny-blocks/math": "^2.0",
- "tiny-blocks/currency": "^2.0",
- "tiny-blocks/value-object": "^2.0"
+ "php": "^8.2",
+ "tiny-blocks/math": "^2",
+ "tiny-blocks/currency": "^2",
+ "tiny-blocks/value-object": "^2",
+ "ext-bcmath": "*"
},
"require-dev": {
- "infection/infection": "^0.26",
- "phpmd/phpmd": "^2.13",
- "phpunit/phpunit": "^9.6",
- "squizlabs/php_codesniffer": "^3.7"
+ "phpmd/phpmd": "^2.15",
+ "phpunit/phpunit": "^11",
+ "phpstan/phpstan": "^1",
+ "infection/infection": "^0.29",
+ "squizlabs/php_codesniffer": "^3.10"
},
"suggest": {
"ext-bcmath": "Enables the extension which is an interface to the GNU implementation as a Basic Calculator utility library."
@@ -55,13 +59,15 @@
"scripts": {
"phpcs": "phpcs --standard=PSR12 --extensions=php ./src",
"phpmd": "phpmd ./src text phpmd.xml --suffixes php --exclude --ignore-violations-on-exit",
+ "phpstan": "phpstan analyse -c phpstan.neon.dist --quiet --no-progress",
"test": "phpunit --log-junit=report/coverage/junit.xml --coverage-xml=report/coverage/coverage-xml --coverage-html=report/coverage/coverage-html tests",
"test-mutation": "infection --only-covered --logger-html=report/coverage/mutation-report.html --coverage=report/coverage --min-msi=100 --min-covered-msi=100 --threads=4",
"test-no-coverage": "phpunit --no-coverage",
"test-mutation-no-coverage": "infection --only-covered --min-msi=100 --threads=4",
"review": [
"@phpcs",
- "@phpmd"
+ "@phpmd",
+ "@phpstan"
],
"tests": [
"@test",
diff --git a/infection.json.dist b/infection.json.dist
index cfbad25..351e7f9 100644
--- a/infection.json.dist
+++ b/infection.json.dist
@@ -1,25 +1,21 @@
{
"timeout": 10,
"testFramework": "phpunit",
- "tmpDir": "report/",
+ "tmpDir": "report/infection/",
"source": {
"directories": [
"src"
]
},
"logs": {
- "text": "report/logs/infection-text.log",
- "summary": "report/logs/infection-summary.log"
+ "text": "report/infection/logs/infection-text.log",
+ "summary": "report/infection/logs/infection-summary.log"
},
"mutators": {
- "@default": true,
- "DecrementInteger": false,
- "IncrementInteger": false,
- "MethodCallRemoval": false,
- "ProtectedVisibility": false
+ "@default": true
},
"phpUnit": {
"configDir": "",
"customPath": "./vendor/bin/phpunit"
}
-}
\ No newline at end of file
+}
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 0000000..b99884f
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,7 @@
+parameters:
+ paths:
+ - src
+ level: 9
+ tmpDir: report/phpstan
+ ignoreErrors:
+ reportUnmatchedIgnoredErrors: false
diff --git a/phpunit.xml b/phpunit.xml
index 3d05fc8..7f080dd 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,25 +1,35 @@
+ bootstrap="vendor/autoload.php"
+ failOnRisky="true"
+ failOnWarning="true"
+ cacheDirectory=".phpunit.cache"
+ beStrictAboutOutputDuringTests="true">
+
+
+
- tests
+ tests
-
- src
-
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Internal/Exceptions/DifferentCurrencies.php b/src/Internal/Exceptions/DifferentCurrencies.php
index 0ac064f..747c1eb 100644
--- a/src/Internal/Exceptions/DifferentCurrencies.php
+++ b/src/Internal/Exceptions/DifferentCurrencies.php
@@ -1,5 +1,7 @@
getScale(),
$currency->name,
- $currency->getDefaultFractionDigits()
+ $currency->getFractionDigits()
)
);
}
diff --git a/src/Money.php b/src/Money.php
index e121b83..8873a3b 100644
--- a/src/Money.php
+++ b/src/Money.php
@@ -1,5 +1,7 @@
amount->multiply(multiplier: BigDecimal::from(value: 1));
+ $withAmountScale = $this->amount->multiply(multiplier: BigDecimal::from(value: self::ONE));
- if ($withAmountScale->getScale() > $this->currency->getDefaultFractionDigits()) {
+ if ($withAmountScale->getScale() > $this->currency->getFractionDigits()) {
throw new InvalidCurrencyScale(amount: $withAmountScale, currency: $this->currency);
}
}
- public static function from(float|string|BigNumber $value, string|Currency $currency): Money
+ public static function from(BigNumber $value, Currency $currency): Money
+ {
+ return new Money(amount: $value, currency: $currency);
+ }
+
+ public static function fromFloat(float $value, string $currency): Money
+ {
+ $amount = BigDecimal::from(value: $value);
+ $currency = Currency::from(value: $currency);
+
+ return new Money(amount: $amount, currency: $currency);
+ }
+
+ public static function fromString(string $value, string $currency): Money
{
- $currency = is_string($currency) ? Currency::from(value: $currency) : $currency;
- $amount = is_scalar($value) ? BigDecimal::from(value: $value) : $value;
+ $amount = BigDecimal::from(value: $value);
+ $currency = Currency::from(value: $currency);
return new Money(amount: $amount, currency: $currency);
}
@@ -39,7 +56,7 @@ public function add(Money $addend): Money
$result = $this->amount->add(addend: $addend->amount);
- return self::from(value: $result->toString(), currency: $this->currency);
+ return self::fromString(value: $result->toString(), currency: $this->currency->value);
}
public function subtract(Money $subtrahend): Money
@@ -50,7 +67,7 @@ public function subtract(Money $subtrahend): Money
$result = $this->amount->subtract(subtrahend: $subtrahend->amount);
- return self::from(value: $result->toString(), currency: $this->currency);
+ return self::fromString(value: $result->toString(), currency: $this->currency->value);
}
public function multiply(Money $multiplier): Money
@@ -61,7 +78,7 @@ public function multiply(Money $multiplier): Money
$result = $this->amount->multiply(multiplier: $multiplier->amount);
- return self::from(value: $result->toString(), currency: $this->currency);
+ return self::fromString(value: $result->toString(), currency: $this->currency->value);
}
public function divide(Money $divisor): Money
@@ -72,9 +89,9 @@ public function divide(Money $divisor): Money
$result = $this->amount
->divide(divisor: $divisor->amount)
- ->withScale(scale: $this->currency->getDefaultFractionDigits());
+ ->withScale(scale: $this->currency->getFractionDigits());
- return self::from(value: $result->toString(), currency: $this->currency);
+ return self::fromString(value: $result->toString(), currency: $this->currency->value);
}
private function areCurrenciesDifferent(Currency $currency): bool
diff --git a/tests/MoneyTest.php b/tests/MoneyTest.php
index ebc2909..4a319ed 100644
--- a/tests/MoneyTest.php
+++ b/tests/MoneyTest.php
@@ -1,165 +1,265 @@
amount->toFloat());
+ self::assertEquals($currency, $actual->currency->value);
+ }
+
+ #[DataProvider('stringDataProvider')]
+ public function testFromString(string $value, string $currency, string $expected): void
{
+ /** @Given a value and a currency */
+ $actual = Money::fromString(value: $value, currency: $currency);
+
+ /** @Then the amount and currency should match the expected values */
+ self::assertEquals($expected, $actual->amount->toString());
+ self::assertEquals($currency, $actual->currency->value);
+ }
+
+ #[DataProvider('bigNumberDataProvider')]
+ public function testFromBigNumber(BigNumber $value, Currency $currency, string $expected): void
+ {
+ /** @Given a BigNumber and a currency */
$actual = Money::from(value: $value, currency: $currency);
+ /** @Then the amount and currency should match the expected values */
self::assertEquals($expected, $actual->amount->toString());
self::assertEquals($currency->value, $actual->currency->value);
}
public function testAdd(): void
{
- $augend = Money::from(value: '100', currency: 'BRL');
- $addend = Money::from(value: '1.50', currency: Currency::BRL);
+ /** @Given two Money objects with the same currency */
+ $augend = Money::fromString(value: '100', currency: Currency::BRL->value);
+ $addend = Money::fromString(value: '1.50', currency: Currency::BRL->value);
+ /** @When adding them */
$actual = $augend->add(addend: $addend);
+ /** @Then the result should be the sum of the amounts with the same currency */
self::assertEquals('101.50', $actual->amount->toString());
self::assertEquals(Currency::BRL->value, $actual->currency->value);
}
public function testSubtract(): void
{
- $minuend = Money::from(value: '10.50', currency: 'EUR');
- $subtrahend = Money::from(value: '0.50', currency: Currency::EUR);
+ /** @Given two Money objects with the same currency */
+ $minuend = Money::fromString(value: '10.50', currency: Currency::EUR->value);
+ $subtrahend = Money::fromString(value: '0.50', currency: Currency::EUR->value);
+ /** @When subtracting them */
$actual = $minuend->subtract(subtrahend: $subtrahend);
+ /** @Then the result should be the difference of the amounts with the same currency */
self::assertEquals('10.00', $actual->amount->toString());
self::assertEquals(Currency::EUR->value, $actual->currency->value);
}
public function testMultiply(): void
{
- $multiplicand = Money::from(value: '5', currency: 'GBP');
- $multiplier = Money::from(value: '3.12', currency: Currency::GBP);
+ /** @Given two Money objects with the same currency */
+ $multiplicand = Money::fromString(value: '5', currency: Currency::GBP->value);
+ $multiplier = Money::fromString(value: '3.12', currency: Currency::GBP->value);
+ /** @When multiplying them */
$actual = $multiplicand->multiply(multiplier: $multiplier);
+ /** @Then the result should be the product of the amounts with the same currency */
self::assertEquals('15.60', $actual->amount->toString());
self::assertEquals(Currency::GBP->value, $actual->currency->value);
}
public function testDivide(): void
{
- $dividend = Money::from(value: '8.99', currency: 'CHF');
- $divisor = Money::from(value: '5', currency: Currency::CHF);
+ /** @Given two Money objects with the same currency */
+ $dividend = Money::fromString(value: '8.99', currency: Currency::CHF->value);
+ $divisor = Money::fromString(value: '5', currency: Currency::CHF->value);
+ /** @When dividing them */
$actual = $dividend->divide(divisor: $divisor);
+ /** @Then the result should be the quotient of the amounts with the same currency */
self::assertEquals('1.79', $actual->amount->toString());
self::assertEquals(Currency::CHF->value, $actual->currency->value);
}
public function testInvalidCurrencyScale(): void
{
+ /** @Given a Money object with an invalid scale for the currency */
$template = 'The decimal scale <4> provided for currency is invalid. ';
$template .= 'The scale must be less than or equal to <2>.';
+ /** @Then an InvalidCurrencyScale exception should be thrown */
$this->expectException(InvalidCurrencyScale::class);
$this->expectExceptionMessage($template);
- Money::from(value: 10.1234, currency: Currency::BRL);
+ /** @When creating the Money object */
+ Money::fromFloat(value: 10.1234, currency: Currency::BRL->value);
}
public function testAddingDifferentCurrencies(): void
{
+ /** @Given two Money objects with different currencies */
$template = 'Currencies and are different. ';
$template .= 'The currencies must be the same to perform this operation.';
+ /** @Then a DifferentCurrencies exception should be thrown */
$this->expectException(DifferentCurrencies::class);
$this->expectExceptionMessage($template);
- $augend = Money::from(value: 100, currency: Currency::BRL);
- $addend = Money::from(value: '1.50', currency: Currency::USD);
+ /** @When trying to add them */
+ $augend = Money::fromFloat(value: 100, currency: Currency::BRL->value);
+ $addend = Money::fromString(value: '1.50', currency: Currency::USD->value);
$augend->add(addend: $addend);
}
public function testSubtractionDifferentCurrencies(): void
{
+ /** @Given two Money objects with different currencies */
$template = 'Currencies and are different. ';
$template .= 'The currencies must be the same to perform this operation.';
+ /** @Then a DifferentCurrencies exception should be thrown */
$this->expectException(DifferentCurrencies::class);
$this->expectExceptionMessage($template);
- $minuend = Money::from(value: 100, currency: Currency::BRL);
- $subtrahend = Money::from(value: '1.50', currency: Currency::EUR);
+ /** @When trying to subtract them */
+ $minuend = Money::fromFloat(value: 100, currency: Currency::BRL->value);
+ $subtrahend = Money::fromString(value: '1.50', currency: Currency::EUR->value);
$minuend->subtract(subtrahend: $subtrahend);
}
public function testMultiplicationDifferentCurrencies(): void
{
+ /** @Given two Money objects with different currencies */
$template = 'Currencies and are different. ';
$template .= 'The currencies must be the same to perform this operation.';
+ /** @Then a DifferentCurrencies exception should be thrown */
$this->expectException(DifferentCurrencies::class);
$this->expectExceptionMessage($template);
- $multiplicand = Money::from(value: 100, currency: Currency::BRL);
- $multiplier = Money::from(value: '1.50', currency: Currency::GBP);
+ /** @When trying to multiply them */
+ $multiplicand = Money::fromFloat(value: 100, currency: Currency::BRL->value);
+ $multiplier = Money::fromString(value: '1.50', currency: Currency::GBP->value);
$multiplicand->multiply(multiplier: $multiplier);
}
public function testDivisionDifferentCurrencies(): void
{
+ /** @Given two Money objects with different currencies */
$template = 'Currencies and are different. ';
$template .= 'The currencies must be the same to perform this operation.';
+ /** @Then a DifferentCurrencies exception should be thrown */
$this->expectException(DifferentCurrencies::class);
$this->expectExceptionMessage($template);
- $dividend = Money::from(value: 100, currency: Currency::BRL);
- $divisor = Money::from(value: '1.50', currency: Currency::CHF);
+ /** @When trying to divide them */
+ $dividend = Money::fromFloat(value: 100, currency: Currency::BRL->value);
+ $divisor = Money::fromString(value: '1.50', currency: Currency::CHF->value);
$dividend->divide(divisor: $divisor);
}
- public function providerForTestFrom(): array
+ public static function floatDataProvider(): array
{
return [
- [
+ 'Integer value' => [
'value' => 1,
- 'currency' => Currency::XPF,
+ 'currency' => Currency::XPF->value,
+ 'expected' => 1
+ ],
+ 'Negative value' => [
+ 'value' => -50.00,
+ 'currency' => Currency::EUR->value,
+ 'expected' => -50.00
+ ],
+ 'High scale currency' => [
+ 'value' => 9.1234,
+ 'currency' => Currency::CLF->value,
+ 'expected' => 9.1234
+ ],
+ 'Float decimal value' => [
+ 'value' => 100.12,
+ 'currency' => Currency::USD->value,
+ 'expected' => 100.12
+ ],
+ 'Value with leading zeros' => [
+ 'value' => 001.23,
+ 'currency' => Currency::USD->value,
+ 'expected' => 001.23
+ ]
+ ];
+ }
+
+ public static function stringDataProvider(): array
+ {
+ return [
+ 'Integer value' => [
+ 'value' => '1',
+ 'currency' => Currency::XPF->value,
'expected' => '1'
],
- [
+ 'Negative value' => [
+ 'value' => '-50.00',
+ 'currency' => Currency::EUR->value,
+ 'expected' => '-50.00'
+ ],
+ 'High scale currency' => [
+ 'value' => '9.1234',
+ 'currency' => Currency::CLF->value,
+ 'expected' => '9.1234'
+ ],
+ 'String decimal value' => [
'value' => '100.12',
- 'currency' => Currency::USD,
+ 'currency' => Currency::USD->value,
'expected' => '100.12'
],
- [
+ 'Value with leading zeros' => [
+ 'value' => '001.23',
+ 'currency' => Currency::USD->value,
+ 'expected' => '001.23'
+ ]
+ ];
+ }
+
+ public static function bigNumberDataProvider(): array
+ {
+ return [
+ 'Big decimal value' => [
'value' => BigDecimal::from(value: 999.12),
'currency' => Currency::BRL,
'expected' => '999.12'
],
- [
+ 'Positive big decimal value' => [
'value' => PositiveBigDecimal::from(value: '9.123'),
'currency' => Currency::TND,
'expected' => '9.123'
- ],
- [
- 'value' => '9.1234',
- 'currency' => Currency::CLF,
- 'expected' => '9.1234'
]
];
}