Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

Opinionated extension of moneyphp/money with Doctrine types

License

NotificationsYou must be signed in to change notification settings

onmoon/money

Repository files navigation

Latest VersionBuildLicenseEmail

OnMoon Money is an opinionated wrapper around MoneyPHP Money:https://github.com/moneyphp/money

Installation

The preferred way to install this extension is throughcomposer.

composer require onmoon/money

Features

On top of the wonderful API of the original, more strictness and some additional features are added.

Money classes can be extended and used as Doctrine Embeddables

The MoneyPHP objects are final, so you can't create your own domain value objects adding more semantics to the code:

<?phpnamespaceApp\Application\Service;useMoney\Money;class InvoiceService{publicfunctioncalculateFee(Money$amount) :Money    {...    }}

With OnMoon Money you can do this:

<?phpnamespaceApp\Domain\Entity\Invoice\ValueObject;useOnMoon\Money\Money;class InvoiceAmountextends Money{}
<?phpnamespaceApp\Domain\Entity\Invoice\ValueObject;useOnMoon\Money\Money;class InvoiceFeeextends Money{}
<?phpnamespaceApp\Application\Service;useApp\Domain\Entity\Invoice\ValueObject\InvoiceAmount;useApp\Domain\Entity\Invoice\ValueObject\InvoiceFee;class InvoiceService{publicfunctioncalculateFee(InvoiceAmount$amount) :InvoiceFee    {...    }}

Also MoneyPHP Money class stores Currency internally as an object, that is a problem for mapping value objectsin Doctrine using embeddables, as the Money object is itself an embeddable and you get nested embeddables:

<?phpnamespaceMoney;finalclass Moneyimplements \JsonSerializable{/**     * @var Currency     */private$currency;...}

OnMoon Money class stores currency internally as a string, and can be mapped as one embeddable using the providedDoctrine Types:

<?phpnamespaceOnMoon\Money;abstractclass BaseMoney{/** @var string */private$amount;/** @var string */private$currency;...}
<doctrine-mappingxmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping                          https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">    <embeddablename="My\Awesome\MoneyClass">        <fieldname="amount"type="money"column="income"nullable="false" />        <fieldname="currency"type="currency"column="income_currency"nullable="false" />    </embeddable></doctrine-mapping>

Money is created only from strings in strict formats depending on the currency

MoneyPHP allows creating Money objects from a wide range of inputs and requires the input amountto be in subunits of the currency. There is no check how many subunits the currency actually has.This requires you to perform validation and checks in your code and can be error-prone.

<?phpuseMoney\Money;useMoney\Currency;$money =newMoney(100,newCurrency('EUR'));// 1 Euro$money =newMoney(100.00,newCurrency('EUR'));// 1 Euro$money =newMoney('100',newCurrency('EUR'));// 1 Euro$money =newMoney('100.00',newCurrency('EUR'));// 1 Euro$money =newMoney('100.00',newCurrency('XBT'));// 0.00000100 Bitcoins

OnMoon Money instead accepts ammounts only as strings containing the monetary amount in ahuman-readable format and strictly enforces the format depending on the currency used.

<?phpuseOnMoon\Money\Money;useOnMoon\Money\GaapMoney;useOnMoon\Money\Bitcoin;useOnMoon\Money\Currency;$money = Money::create('100', Currency::create('BIF'));// 100 Burundi Francs$money = Money::create('100.00', Currency::create('EUR'));// 100 Euros$money = GaapMoney::create('100.000', Currency::create('IQD'));// 100 Iraqi Dinars$money = Bitcoin::create('100.00000000', Currency::create('XBT'));// 100 Bitcoins$money = Money::create(100, Currency::create('EUR'));// Error, invalid type$money = Money::create(100.00, Currency::create('EUR'));// Error, invalid type$money = Money::create('100', Currency::create('EUR'));// Error, no subunits specified$money = Money::create('100.0', Currency::create('EUR'));// Error, not all subunits specified$money = Money::create('100.000', Currency::create('EUR'));// Error, too many subunits specified

The same API, but strictly typed

MoneyPHP Money:

Money\Money::multiply($multiplier, $roundingMode = self::ROUND_HALF_UP)Money\Money::allocate(array $ratios)

OnMoon Money:

OnMoon\Money\Money::multiply(string $multiplier, int $roundingMode = LibMoney::ROUND_UP) : selfOnMoon\Money\Money::allocate(string ...$ratios) : array

etc.

Custom validation for your code extending the library classes with meaningful messages

<?phpnamespaceApp\Domain\Entity\Invoice\ValueObject;useMoney\Currencies;useMoney\Currencies\CurrencyList;useOnMoon\Money\BaseMoney;useOnMoon\Money\Money;useOnMoon\Money\Currency;useOnMoon\Money\Exception\CannotCreateMoney;class InvoiceIncomeextends Money{publicstaticfunctionhumanReadableName() :string    {return'Invoice Income';    }protectedstaticfunctionamountMustBeZeroOrGreater() :bool    {returntrue;    }protectedstaticfunctiongetAllowedCurrencies() :Currencies    {returnnewCurrencyList(['EUR' =>2,'USD' =>2]);    }protectedstaticfunctionvalidate(BaseMoney$money) :void    {if ($money->getCurrency()->getCode() ==='EUR' &&$money->greaterThan(Money::create('50.00',$money->getCurrency()))        ) {thrownewCannotCreateMoney('Cannot exceed 50.00 for EUR currency');        }       }}$invoiceIncome = InvoiceIncome::create('100.00', Currency::create('RUB'));// Error: Invalid Invoice Income with amount: 100.00 and currency: RUB. Currency not allowed.$invoiceIncome = InvoiceIncome::create('100.00', Currency::create('EUR'));// Error: Cannot exceed 50.00 for EUR currency$invoiceIncome = InvoiceIncome::create('-100.00', Currency::create('USD'));// Error: Invalid Invoice Income with amount: -100.00 and currency: USD. Amount must be zero or greater.$invoiceIncome = InvoiceIncome::create('100.00', Currency::create('USD'));// ok

Usage

For beginning, you should make yourself familiar with theMoneyPHP Money documentation

Currency classes

OnMoon Money provies a class for representing currency values:OnMoon\Money\Currency.

To create a currency object you will need the currency code:

<?phpuseOnMoon\Money\Currency;$euroCode ='EUR';$euro = Currency::create($euroCode);

Money classes

The API of a OnMoon Money class is the same as the MoneyPHP Money class:

You can create your own Money classes with your own semantics:

<?phpnamespaceApp\Domain\Entity\Invoice\ValueObject;useOnMoon\Money\Money;class InvoiceAmountextends Money{}

You can create an instance of the specific Money class by using the named constructor:

<?phpuseOnMoon\Money\Money;useOnMoon\Money\Currency;useApp\Domain\Entity\Invoice\ValueObject\InvoiceAmount;$money = Money::create('100.00', Currency::create('EUR'));// instance of OnMoon\Money\Money$money = InvoiceAmount::create('100.00', Currency::create('EUR'));// instance of App\Domain\Entity\Invoice\ValueObject\InvoiceAmount

Subunits

The library provies three base classes that you can use directly or extend from:

OnMoon\Money\Money - can work with currencies with up to 2 subunits

OnMoon\Money\GaapMoney - can work with currencies with up to 4 subunits and conforms with theGAAP standard

OnMoon\Money\BTC - can work with 8 subunits and is restricted to the Bitcoin (XBT) currency

Depending on the base class you use or extend from, some currencies may be unavailable dueto requiring more subunits than the base class can work with.

You should choose the base class depending on the currencies that your application willuse with the money class.

<?phpnamespaceApp\Domain\Entity\Invoice\ValueObject;useOnMoon\Money\Money;useOnMoon\Money\GaapMoney;useOnMoon\Money\Currency;class InvoiceAmountextends Money{}class InvoiceFeeextends GaapMoney{}$money = InvoiceAmount::create('100.00', Currency::create('EUR'));// ok$money = InvoiceAmount::create('100.000', Currency::create('BHD'));// error$money = InvoiceFee::create('100.00', Currency::create('EUR'));// ok$money = InvoiceFee::create('100.000', Currency::create('BHD'));// ok

If you need your own custom subunit amount you can extend any Money class andimplement theclassSubunits method.

<?phpnamespaceApp\Domain\Entity\Invoice\ValueObject;useOnMoon\Money\Money;useOnMoon\Money\Currency;class InvoiceAmountextends Money{protectedstaticfunctionclassSubunits() :int    {return0;    }}$money = InvoiceAmount::create('100', Currency::create('DJF'));// ok$money = InvoiceAmount::create('100.00', Currency::create('EUR'));// error

Remember, that you cannot use Money classes of different subunits in the Money class API:

namespaceApp\Domain\Entity\Invoice\ValueObject;useOnMoon\Money\Money;useOnMoon\Money\Currency;class TwoSubunitMoneyextends Money{protectedstaticfunctionsubUnits() :int    {return2;    }}class FourSubunitMoneyextends Money{protectedstaticfunctionsubUnits() :int    {return4;    }}$twoSubunitMoney = TwoSubunitMoney::create('100.00', Currency::create('EUR'));$otherTwoSubunitMoney = TwoSubunitMoney::create('100.00', Currency::create('EUR'));$twoSubunitMoney->add($otherTwoSubunitMoney);// ok$fourSubunitMoney = FourSubunitMoney::create('100.00', Currency::create('EUR'));$twoSubunitMoney->add($fourSubunitMoney);// error

Validation

On top of the validation provided by the base classes, you can enforce additionalconstraints in your extended classes.

By implementing one of the following methods:

<?phpnamespaceApp\Domain\Entity\Invoice\ValueObject;useOnMoon\Money\Money;useOnMoon\Money\Currency;class PositiveAmountMoneyextends Money{protectedstaticfunctionamountMustBeZeroOrGreater() :bool    {returntrue;    }}$money = PositiveAmountMoney::create('0.00', Currency::create('EUR'));// ok$money = PositiveAmountMoney::create('-0.01', Currency::create('EUR'));// errorclass GreaterThanZeroAmountMoneyextends Money{protectedstaticfunctionamountMustBeGreaterThanZero() :bool    {returntrue;    }}$money = GreaterThanZeroAmountMoney::create('0.01', Currency::create('EUR'));// ok$money = GreaterThanZeroAmountMoney::create('0.00', Currency::create('EUR'));// errorclass ZeroOrLessAmountMoneyextends Money{protectedstaticfunctionamountMustBeZeroOrLess() :bool    {returntrue;    }}$money = ZeroOrLessAmountMoney::create('0.00', Currency::create('EUR'));// ok$money = ZeroOrLessAmountMoney::create('0.01', Currency::create('EUR'));// errorclass NegativeAmountMoneyextends Money{protectedstaticfunctionamountMustBeLessThanZero() :bool    {returntrue;    }}$money = NegativeAmountMoney::create('-0.01', Currency::create('EUR'));// ok$money = NegativeAmountMoney::create('0.00', Currency::create('EUR'));// error

If you need more complex validation logic, implement the following method:

<?phpnamespaceApp\Domain\Entity\Invoice\ValueObject;useOnMoon\Money\BaseMoney;useOnMoon\Money\Exception\CannotCreateMoney;useOnMoon\Money\Money;useOnMoon\Money\Currency;class ComplexValidationMoneyextends Money{protectedstaticfunctionvalidate(BaseMoney$money) :void    {if ($money->getCurrency()->getCode() ==='EUR' &&$money->greaterThan(Money::create('50.00',$money->getCurrency()))        ) {thrownewCannotCreateMoney('Cannot work with Euros if amount is greater than 50.00');        }       }}$money = ComplexValidationMoney::create('40.00', Currency::create('EUR'));// ok$money = ComplexValidationMoney::create('51.00', Currency::create('EUR'));// error

You can also specify the list of currencies that are allowed for a Money class and all classes that extend from it:

<?phpnamespaceApp\Domain\Entity\Invoice\ValueObject;useMoney\Currencies;useMoney\Currencies\CurrencyList;useOnMoon\Money\BaseMoney;useOnMoon\Money\Exception\CannotCreateMoney;useOnMoon\Money\Money;useOnMoon\Money\Currency;class OnlyUsdMoneyextends Money{protectedstaticfunctiongetAllowedCurrencies() :Currencies    {returnnewCurrencyList(['USD' =>2]);    }}$money = OnlyUsdMoney::create('50.00', Currency::create('USD'));// ok$money = OnlyUsdMoney::create('50.00', Currency::create('EUR'));// error

The default classes provided by the library support the following currencies:

OnMoon\Money\Money - All ISO currencies with 0-2 subunits

OnMoon\Money\GaapMoney - All ISO currencies with 0-4 subunits

OnMoon\Money\BTC - Only XBT with 8 subunits

All operations on the Money class that change the amount will return the base class instead of the extended,as the resulting amount can violate the invariants of the extended class:

<?phpnamespaceApp\Domain\Entity\Invoice\ValueObject;useOnMoon\Money\Money;useOnMoon\Money\Currency;class MyMoneyextends Money{protectedstaticfunctionamountMustBeZeroOrGreater() :bool    {returntrue;    }}$money      = MyMoney::create('100.00', Currency::create('EUR'));// instance of App\Domain\Entity\Invoice\ValueObject\MyMoney$otherMoney = MyMoney::create('200.00', Currency::create('EUR'));// instance of App\Domain\Entity\Invoice\ValueObject\MyMoney$sum =$money->subtract($otherMoney);// returns instance of OnMoon\Money\Money

Error handling

Exceptions thrown by OnMoon money classes are extended from two base exceptions:

  • OnMoon\Money\Exception\MoneyLogicError - Errors that represent a logic error in your code and should beavoided in production, error messages should not be shown to the user.
  • OnMoon\Money\Exception\MoneyRuntimeError - Errors that represent a runtime error in your code and can depend on user input.You can use them safely to display errors to the user.

Examples ofOnMoon\Money\Exception\MoneyRuntimeError error messages:

  • Invalid Money with amount: 100.00 and currency: RUB. Currency not allowed.
  • Invalid Money with amount: 50.000 and currency: EUR. Invalid amount format. The correct format is: /^-?\d+.\d{2}$/.
  • Invalid Money with amount: -11.00 and currency: USD. Amount must be greater than zero.

You can make theese messages even more helpful, by implementing thehumanReadableName method in your Money Classes:

<?phpnamespaceApp\Domain\Entity\Transaction\ValueObject;useOnMoon\Money\Money;class TransactionFeeextends Money{publicstaticfunctionhumanReadableName() :string    {return'Transaction Fee';    }}

The error messages then will look like this:

  • Invalid Transaction Fee with amount: 100.00 and currency: RUB. Currency not allowed.
  • Invalid Transaction Fee with amount: 50.000 and currency: EUR. Invalid amount format. The correct format is: /^-?\d+.\d{2}$/.
  • Invalid Transaction Fee with amount: -11.00 and currency: USD. Amount must be greater than zero.

If you want to catch all exceptions thrown by OnMoon Money, including the exceptions of theunderlying MoneyPHP Money code - use theMoney\Exception interface.

Using the library with Symfony and Doctrine

The library provides four Doctrine types to persist the Money and Currency objects to the database:

  • OnMoon\Money\Type\BTCMoneyType - Should be used only for classes extendingOnMoon\Money\BTC
  • OnMoon\Money\Type\GaapMoneyType - Should be used only for classes extendingOnMoon\Money\GaapMoney
  • OnMoon\Money\Type\MoneyType - Should be used only for classes extendingOnMoon\Money\Money
  • OnMoon\Money\Type\CurrencyType - Should be used only for classes extendingOnMoon\Money\Curency

The rule of thumb for Type classes mapping the Money object is that the Type class decimal precisionshould be equal to the Money class subunits. If they will be different, you will get other amountsfrom the database than previously saved.

Example code

Entity:

<?phpnamespaceApp\Domain\Entity\Invoice;useApp\Domain\Entity\Invoice\ValueObject\InvoiceIncome;class Invoice{/** @var InvoiceIncome $income */private$income;publicfunction__construct(InvoiceIncome$income)    {$this->income =$income;    }publicfunctionincome() :InvoiceIncome    {return$this->income();        }}

Value object:

<?phpnamespaceApp\Domain\Entity\Invoice\ValueObject;useOnMoon\Money\Money;class InvoiceIncomeextends Money{}

/config/packages/doctrine.xml:

<?xml version="1.0" encoding="UTF-8" ?><containerxmlns="http://symfony.com/schema/dic/services"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:doctrine="http://symfony.com/schema/dic/doctrine"xsi:schemaLocation="http://symfony.com/schema/dic/services        http://symfony.com/schema/dic/services/services-1.0.xsd        http://symfony.com/schema/dic/doctrine        http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd">    <doctrine:config>        <doctrine:dbal>            <doctrine:typename="money">OnMoon\Money\Type\MoneyType</doctrine:type>            <doctrine:typename="currency">OnMoon\Money\Type\CurrencyType</doctrine:type>        </doctrine:dbal>    </doctrine:config></container>

Entity mapping:

<doctrine-mappingxmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping                          https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">    <entityname="App\Domain\Entity\Invoice\Invoice"table="invoices">        <embeddedname="income"class="App\Domain\Entity\Invoice\ValueObject\InvoiceIncome"use-column-prefix="false" />    </entity></doctrine-mapping>

Value object mapping:

<doctrine-mappingxmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping                          https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">    <embeddablename="App\Domain\Entity\Invoice\ValueObject\InvoiceIncome">        <fieldname="amount"type="money"column="income"nullable="false" />        <fieldname="currency"type="currency"column="income_currency"nullable="false" />    </embeddable></doctrine-mapping>

About

Opinionated extension of moneyphp/money with Doctrine types

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages


[8]ページ先頭

©2009-2025 Movatter.jp