16
votes

What is your strategy to store monetary values with Doctrine? The Symfony's money field is quite handy but how to map this to Doctrine's column? Is there a bundle for this that provides DBAL type?

float or int column types are insufficient because when you deal with money you often deal with currency too. I'm using two fields for this but it's awkward to handle manually.

4
Do you need to store integer or floating numbers? I think you will have a limiter number of field types.A.L
@A.L It doesn't matter much. I can always use divisor option to turn ints into floats. For now I'm using int (amount) + string (currency) but it's so awkward.SiliconMind
What do you with the + sign? Do you have one field for the amount and one field for the currency? If you have to work with different currencies, I suggest you to add a note in your question because it makes it a little bit more complex than if you have only one currency.A.L
@A.L The + sign doesn't matter since amount is stored as int - it can be positive or negative value. At least in my case, although it's the same with floats.SiliconMind

4 Answers

15
votes

Consider using the decimal type:

/**
 * @ORM\Column(type="decimal", precision=7, scale=2)
 */
protected $price = 0;

Note that there are currencies which have three decimal positions. If you intend to use such currencies, the scale parameter should be 3. If you intend to mix currencies with two and three decimal positions, add a trailing 0 if there are only two decimal positions.

Attention: $price will be a string in PHP. You can either cast it to float or multiply it with 100 (or 1000, in the case of currencies with three decimal positions) and cast it to int.


The currency itself is a separate field; it can be a string with the three letter currency code. Or – the clean way – you can create a table with all currencies you’re using and then create a ManyToOne relation for the currency entry.

12
votes

I recommend using a value object like Money\Money.

# app/Resources/Money/doctrine/Money.orm.yml
Money\Money:
  type: embeddable
  fields:
    amount:
      type: integer
  embedded:
    currency:
      class: Money\Currency
# app/Resources/Money/doctrine/Currency.orm.yml
Money\Currency:
  type: embeddable
  fields:
    code:
      type: string
      length: 3
# app/config.yml
doctrine:
  orm:
    mappings:
      Money:
        type: yml
        dir: "%kernel.root_dir%/../app/Resources/Money/doctrine"
        prefix: Money
class YourEntity
{
    /**
     * @ORM\Embedded(class="\Money\Money")
     */
    private $value;

    public function __construct(string $currencyCode)
    {
        $this->value = new \Money\Money(0, new \Money\Currency($currencyCode));
    }

    public function getValue(): \Money\Money
    {
        return $this->value;
    }
}
1
votes

You could define a own field type as long as you tell the doctrine how to handle this. To explain this I made up a ''shop'' and ''order'' where a ''money''-ValueObject gets used.

To begin we need an Entity and another ValueObject, which gets used in the entity:

Order.php:

<?php

namespace Shop\Entity;

/**
 * @Entity
 */
class Order
{
    /**
     * @Column(type="money")
     *
     * @var \Shop\ValueObject\Money
     */
    private $money;

    /**
     * ... other variables get defined here
     */

    /**
     * @param \Shop\ValueObject\Money $money
     */
    public function setMoney(\Shop\ValueObject\Money $money)
    {
        $this->money = $money;
    }

    /**
     * @return \Shop\ValueObject\Money
     */
    public function getMoney()
    {
        return $this->money;
    }

    /**
     * ... other getters and setters are coming here ...
     */
}

Money.php:

<?php

namespace Shop\ValueObject;

class Money
{

    /**
     * @param float $value
     * @param string $currency
     */
    public function __construct($value, $currency)
    {
        $this->value  = $value;
        $this->currency = $currency;
    }

    /**
     * @return float
     */
    public function getValue()
    {
        return $this->value;
    }

    /**
     * @return string
     */
    public function getCurrency()
    {
        return $this->currency;
    }
}

So far nothing special. The "magic" comes in here:

MoneyType.php:

<?php

namespace Shop\Types;

use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;

use Shop\ValueObject\Money;

class MoneyType extends Type
{
    const MONEY = 'money';

    public function getName()
    {
        return self::MONEY;
    }

    public function getSqlDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
    {
        return 'MONEY';
    }

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        list($value, $currency) = sscanf($value, 'MONEY(%f %d)');

        return new Money($value, $currency);
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value instanceof Money) {
            $value = sprintf('MONEY(%F %D)', $value->getValue(), $value->getCurrency());
        }

        return $value;
    }

    public function canRequireSQLConversion()
    {
        return true;
    }

    public function convertToPHPValueSQL($sqlExpr, AbstractPlatform $platform)
    {
        return sprintf('AsText(%s)', $sqlExpr);
    }

    public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform)
    {
        return sprintf('PointFromText(%s)', $sqlExpr);
    }
}

Then you can use the following code:

// preparing everything for example getting the EntityManager...

// Store a Location object
use Shop\Entity\Order;
use Shop\ValueObject\Money;

$order = new Order();

// set whatever needed
$order->setMoney(new Money(99.95, 'EUR'));
// other setters get called here.

$em->persist($order);
$em->flush();
$em->clear();

You could write a mapper which maps your input coming from Symfony's money field into a Money-ValueObject to simplify this further.

A couple more details are explained here: http://doctrine-orm.readthedocs.org/en/latest/cookbook/advanced-field-value-conversion-using-custom-mapping-types.html

Untested, but I used this concept before and it worked. Let me know if you got questions.

1
votes

I were serching for a solution to this problem and googling I landed on this page.

There, there is illustrated the Embeddable field available since Doctrine 2.5.

With something like this you can manage values as monetary ones that have more "params".

An example:

/** @Entity */
class Order
{
    /** @Id */
    private $id;

    /** @Embedded(class = "Money") */
    private $money;
}

/** @Embeddable */
class Money
{
    /** @Column(type = "int") */ // better than decimal see the mathiasverraes/money documentation
    private $amount;

    /** @Column(type = "string") */
    private $currency;
}

Hope this will help.

UPDATE

I wrote a PHP library that contains some useful value objects.

There is also a value object to manage monetary values (that wraps the great MoneyPHP library) and persist them to the database using a Doctrine type.

This type saves the value to the database in the form of 100-EUR that stands for 1 Euro.