This library provides classes for money manipulation using a pure object-oriented approach.
Why did we make yet another PHP-library for money? After all, there are:
But, there are disadvantages to using it in pure object-oriented development.
- There are no interfaces — we can't create an object that will represent money itself.
- All existed implementations are classes with hundreds loc and dozens of methods like
add
,sub
,divide
,compare
etc., which are essentially procedural style. - We can't add new functionality as there are no interfaces, and most implementations are final.
- Eager execution — all calculations happen in construct-time as constructor's arguments should be evaluated.
- Existed libraries hide the scale, the only thing we can control is a rounding method.
- We have the
Money
interface! This implementation is not procedural DTO. - There is no hundreds-lines class with tens of methods. Every operation is an object that implements the
Money
interface. - Lazy execution: you can construct complex expressions without immediate calculations.
- Explicit scale to avoid ambiguous results.
The presence of the interface allows you to make different classes. For example, you can easily implement UserBalance
that takes data from a database or some API or even from a file.
<?php
declare(strict_types=1);
use ElegantBro\Money\Currencies\USD;
use ElegantBro\Money\Currency;
use ElegantBro\Money\Money;
final class UserBalance implements Money
{
/**
* @var string
*/
private $userId;
/**
* @var PDO
*/
private $pdo;
public function __construct(string $userId, PDO $pdo)
{
$this->userId = $userId;
$this->pdo = $pdo;
}
public function amount(): string
{
$stmt = $this->pdo->prepare('SELECT SUM(debit - credit) FROM balances WHERE user_id = ?');
$stmt->execute([$this->userId]);
return $stmt->fetchColumn();
}
public function currency(): Currency
{
return new USD();
}
public function scale(): int
{
return 2;
}
}
Besides, you can create some static implementations like TwoDollars
:
<?php
declare(strict_types=1);
use ElegantBro\Money\Currencies\USD;
use ElegantBro\Money\Currency;
use ElegantBro\Money\Money;
final class TwoDollars implements Money
{
public function amount(): string
{
return '2';
}
public function currency(): Currency
{
return new USD();
}
public function scale(): int
{
return 2;
}
}
You are free to use those objects in any out-of-the-box or custom operations.
Let's take a look at the following example: you need some tax which is 10%
of the given amount but not less than $2
.
This is a real object-oriented solution:
<?php
declare(strict_types=1);
use ElegantBro\Money\ArrayLot;
use ElegantBro\Money\Money;
use ElegantBro\Money\Operations\MaxOf;
use ElegantBro\Money\Operations\Multiplied;
use ElegantBro\Money\Wrapper;
final class Tax extends Wrapper
{
public function __construct(Money $origin, Money $minTax)
{
$this->is(
new MaxOf(
new ArrayLot(
Multiplied::keepScale($origin, '0.1'),
$minTax
),
$origin->scale()
)
);
}
}
// on the client side
$tax = new Tax(
new UserBalance($uuid, $pdo),
new TwoDollars()
);
This is a shiny declarative code, but the biggest advantage is laziness. No one calculation will happen until the amount
method will be called.
Using procedural implementation like moneyphp/money
you have to create some sort of ugly MoneyHelper
with static tax
method:
<?php
declare(strict_types=1);
use Money/Money;
class MoneyHelper
{
public static function tax(Money $amount, Money $minTax): Money
{
return Money::max(
$amount->multiply('0.1'),
$minTax
);
}
// usually helpers contain dozens of static methods
}
// on the client side
$tax = MoneyHelper::tax(
$userBalance, // you should fetch user balance from the database before
Money::USD(200)
);
It is quite possible that this result will not be needed in the subsequent calculation. For example, there is some tax-free condition, and you should check it before the tax calculation to avoid unnecessary DB-request and end up with temporal coupling.
$ composer require elegant-bro/money
Pass all tests locally before creating the pull request.
Build the test container and run all tests
make all
Other commands
# build the Dockerfile
make build
# install composer requirements
make install
# enter the container shell
make shell
# style check
make style-check
# run unit tests
make unit
# ensure coverage is 100%
make coverage