Reputation: 2179
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.
Upvotes: 19
Views: 24746
Reputation: 6379
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.
Upvotes: 2
Reputation: 2333
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;
}
}
Upvotes: 14
Reputation: 13117
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.
Upvotes: 13
Reputation: 1499
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.
Upvotes: 4