brick / money

A money and currency library for PHP
MIT License
1.65k stars 102 forks source link

Doctrine value object #24

Closed zspine closed 4 years ago

zspine commented 4 years ago

Hi

Excellent library to work with money. I tried to implement it as a value object in doctrine and it seems there are some complicated challenges and I have to use another separate proxy class to work with value objects. Do you have any recommendation docs to work with ORMs?

Cheers

BenMorel commented 4 years ago

Hi, AFAIK there isn't a way to map a value object to 2 database fields (value + currency) in Doctrine. However, what I usually do it store the money as 2 fields in the entity, and do the transformation to and from Money in the getter/setter:

class Product
{
    private int $price;
    private string $currency;

    public function setPrice(Money $price) : void
    {
        $this->price    = $price->getMinorAmount()->toInt();
        $this->currency = $price->getCurrency()->getCurrencyCode();
    }

    public function getPrice() : Money
    {
        return Money::ofMinor($this->price, $this->currency);
    }
}

This has the advantage to only do the transformation when the value is requested, and not every time the entity is hydrated, even when you don't need it.

zspine commented 4 years ago

Hi, Thanks a lot for the quick response. I agree there are no options to map a value to 2 database fields, but I am not trying to create a custom mapping type. Instead, I am trying to work with doctrine embeddables (value object) so I can map both amount and currency to relevant database fields. Actually my current implementation is similar to your above example and it's working without any issues, however it make more sense to have an option to map BigDecimal, BigInteger & Money object itself as an embeddable . Here is my current implementation:

//Money.php
use Doctrine\ORM\Mapping as ORM;
use Brick\Math\RoundingMode;
use Brick\Money\Context\CustomContext;
use Brick\Money\Money as BrickMoney;

/**
 * @ORM\Embeddable()
 */
final class Money
{
    /**
     * @ORM\Column(type="decimal", precision=14, scale=6, nullable=false, options={"default":0})
     */
    private $amount = 0;

    /**
     * @ORM\Column(type="string", nullable=false, options={"default":"EUR"})
     */
    private $currency;

    /**
     * @var BrickMoney
     */
    private $money;

    public function __construct($value = null, ?string $currency = null)
    {
        $this->amount = $value ?? "0";
        $this->currency = (\mb_strlen($currency) > 0) ? strtoupper($currency) : "USD";
    }

    public function getMoney(): BrickMoney
    {
        if (null === $this->money) {
            $this->money = BrickMoney::of($this->getAmount(), $this->getCurrency(), new CustomContext(5), RoundingMode::HALF_UP);
        }
        return $this->money;
    }

    public function getAmount(): string
    {
        return $this->amount;
    }

    public function getCurrency(): string
    {
        return $this->currency;
    }
}

//Product.php
use Doctrine\ORM\Mapping as ORM;

class Product
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string")
     */
    private $name;

    /**
     * @ORM\Column(type="string")
     */
    private $slug;

    /**
     * @ORM\Embedded(class="App\Entity\Embeddable\Money")
     */
    private $price;
}
```php

Thank you
BenMorel commented 4 years ago

This would work as well, indeed! Can we close this issue?

zspine commented 4 years ago

@BenMorel Sure, Thank you for your quick response. Thanks again for this excellent library, it saved us from lots of hassle. If we managed to solve the embeddable issue will definitely share the solution.

Cheers!

zspine commented 4 years ago

Hi, AFAIK there isn't a way to map a value object to 2 database fields (value + currency) in Doctrine. However, what I usually do it store the money as 2 fields in the entity, and do the transformation to and from Money in the getter/setter:

class Product
{
    private int $price;
    private string $currency;

    public function setPrice(Money $price) : void
    {
        $this->price    = $price->getMinorAmount()->toInt();
        $this->currency = $price->getCurrency()->getCurrencyCode();
    }

    public function getPrice() : Money
    {
        return Money::ofMinor($this->price, $this->currency);
    }
}

This has the advantage to only do the transformation when the value is requested, and not every time the entity is hydrated, even when you don't need it.

Hi, finally we ended up following your implementation example. Using doctrine embeddable makes things harder to understand and also seems like unnecessary extra layer with brick money object. Thanks again for your input on this!