Search code examples
phpsilverstripe

Silverstripe: Converting a database value to and from a cms field


I am storing money in my database using an integer. This means that $0.50 is 50. I extended the Integer db field in such a way that this is now working correctly at the front end. It is nicely converted to and from integer.

In the backend, however I am having problems. The silverstripe CMS seems to do its own conversion (adding thousand separators for example), which has interesting results :).

How would you guys approach this problem? I tried to use an onbeforewrite and a custom getter.

This is the code I have, starting with an extension of the integer db-field

    /**
 * Format a number to currency
 * @param int    $number_of_decimals    When larger than 0 it will return this number of decimals, AND divide the amount by 10^number of the amount of decimals
 * @param bool   $round                 Round the resulting number to the closest whole number
 * @param string $thousands_char        Character used as thousands separator
 * @param string $decimal_char          Character used as decimal separator
 * @return string
 */
public function toCurrency($number_of_decimals=2, $round=false, $thousands_char=".", $decimal_char=",") {
    $divide_by = pow(10,$number_of_decimals);
    $value = $this->owner->value/$divide_by;
    if($round) {
        //no decimals when rounding :)
        $number_of_decimals=0;
    }
    return number_format($value, $number_of_decimals, $decimal_char,$thousands_char);
}

public function fromCurrency($number_of_decimals=2, $thousands_char=".", $decimal_char=",") {
    $multiply_by = pow(10,$number_of_decimals);
    //get rid of the thousand separator
    $value = str_replace($thousands_char,"",$this->owner->value);
    //replace the decimal char with a point
    $value = str_replace($decimal_char,".",$value);
    $value = $value*$multiply_by;
    return number_format($value, 0, ".","");
}

Also I added this to an extension of the SiteConfig (thus creating a sort of globally available function

/**
 * Creates a DBField equivalent of the value, based on the type. In such a way, we can make use of the dame functions that are in an extension of a dbfield.
 * @param $type The type of the DBfield to create (e.g. Varchar, Int etc.).
 * @param $value The value, a string or number
 * @return mixed
 */
public function ToDBField($type,$value) {
    $field = $type::create();
    $field->setValue($value);
    return $field;
}

These functions do the actual work, and they are in a dataobject:

    public function GetAmount() {
        $amount = parent::getField("Amount");
        if (is_subclass_of(Controller::curr(), "LeftAndMain")) {
            $int_amount = SiteConfig::current_site_config()->ToDBField("Int", $amount);
            return $int_amount->toCurrency($number_of_decimals=2, $round=false, $thousands_char="", $decimal_char=".");
        }
        return $amount;
    }

    public function onBeforeWrite() {
        $int_amount = SiteConfig::current_site_config()->ToDBField("Int", $this->Amount);
        $this->Amount = $int_amount->fromCurrency(2,",",".");
        parent::onBeforeWrite();
    }

Solution

  • So the problem you're facing is one of the default scaffolding based on the DbField type. By default, type Int will use a NumericField in the CMS which does no currency formatting.

    We can override the scaffolding for a form field in the getCMSFields function of a DataObject. Specifically using FieldList::replaceField to replace the form field with one of our choosing.

    function getCMSFields()
    {
        $fields = parent::getCMSFields();
        $fields->replaceField('Amount', CurrencyField::create('Amount', 'Amount'));
    
        return $fields;
    }
    

    You have a number of options for this from building your own from scratch, building on-top of an existing form field or using a pre-existing one. CurrencyField and MoneyField both should do what you want without you needing to have your own functions exposed on the DataObject to do the conversion back and forth.

    There are a few things to note:

    • From the documentation, CurrencyField is limited to US-centric formats
    • CurrencyField and MoneyField are not designed to be backed by an integer

    At this point, I would consider altering your approach. MoneyField seems to describe most-closely to what you want with the ability to more easily handle different currencies. The drawback is that it requires a Money database field.

    I've never used the MoneyField though I believe for a few projects I have used the CurrencyField. Because of that, I can't speak for how easy it is to work with and tweak to the way you want.

    If you still would rather not take this approach, I would look at extending perhaps the CurrencyField class (by this I mean Object-Orientated extending a class, not the built-in extension system) and tweaking that to your liking. Implementing that however is likely too broad to cover in this question.