You are viewing revision #6 of this wiki article.
This version may not be up to date with the latest version.
You may want to view the differences to the latest version.
By default, the decimal separator in php (also in mysql) is a dot (.). So when we work with floats in Yii (in calculations, validation, sql statements etc.), the decimal separator has to be a dot. If we want to use for example a comma (,) as the decimal separator in our application, we have to:
- format the float value before printing/echoing : handle the output : 4.45 => "4,45"
- transform user input into a valid float value before using in calculations etc. : handle the input : "4,45" => 4.45
This article suggests where/when and how exactly to do this handling.
In the following we'll use the model 'Product' with the decimal attribute 'price' as an example.
THOUGHTS ¶
1. The formatting of a number ¶
HOW ¶
In Yii, a number can be formatted using the formatNumber()
function of the class CFormatter in the following way:
Yii::app()->format->formatNumber($value);
Yii::app()->format->number($value); // is the same as above, see details to CFormatter
By default, this will format the value without decimals, and with a comma (,) between every group of thousands.
To change this default behavior we have to override the numberFormat
attribute of the same class, which defines the format to be used. For example:
public $numberFormat=array('decimals'=>2, 'decimalSeparator'=>',', 'thousandSeparator'=>'');
After this, numbers will be formatted with 2 decimals, a comma (,) before the decimals and no separator between groups of thousands.
WHERE/WHEN ¶
The question is: at what point in the application flow would it be best to format a numerical value? The answer depends...
---
If you 'always' send the price of a product directly to the output after reading it from the database, without using it in calculations, you could do the formatting in the afterFind()
function of the model Product.php
:
public function afterFind() {
$this->price = Yii::app()->format->number($this->price);
return parent::afterFind();
}
The advantage is that the formatting is done centrally in one place, and we don't need to change the views. The disatvantage is that $product->price
contains the formatted number string, thus is not a valid numerical value and can not be used as such:
$product = Product::model()->findByPk($id); // ... when we select a model like this ...
echo $product->price; // "14,90" (if price stored in db is 14.90)
echo $product->price * 10; // 140 ( = "14,90" * 10) - not the expected result!
---
This wiki article suggests to do the formatting in the CHtml class, by overriding its two functions value()
and resolveValue()
.
This seems to be a good approach. But don't forget that it does not cover the cases where CHtml is not used to generate the ouput. The following CGridView column for example will not be affected:
array(
'name'=>'price',
'value'=>'$data->price - $data->discount',
),
---
There doesn't seem to be a better way than to format each float value in the view file, direclty before printing/echoing it. At least none that makes sure every type of output is covered.
2. The 'unformatting' of a number ¶
HOW ¶
To transform the user input into a valid numerical value, we remove the thosund-seperators from the input string, and then replace the decimal-separator with a dot (.). In a way, we simply 'unformat' the number.
WHERE/WHEN ¶
--
In this Yii extension, the 'unformatting' is done in the beforeSave()
function of a model
(more specifically, in the beforeSave() function of a behavior, that can be attached to any model).
This is not a good place, because the validating happens before the saving; and since the price is not a valid numerical value at that point, the validation will fail.
--
This wiki article suggests to do the 'unformatting' in the beforeValidate()
function of a model.
This is a good place. But if you want to use the user input in calculations before saving the model, make sure that the price attribute is 'unformatted' beforehand, by validating the model ($product->validate()
).
--
We usually assign the received user input (=POST variables) to the model before performing other operations (calculations, validaton, saving, etc.).
Therefore we sugges to do the 'unformatting' during the assignment process. This way, after one of the following lines is executed:
$product->attributes = $_POST['Product']; // or ...
$product->setAttribute('price', $_POST['Product']['price']); // or ...
$product->setAttributes(array('price'=>$_POST['Product']['price']));
$product->price
will be a valid numerical value and available for further use.
THE CODE ¶
1. The formatting of a number ¶
Extend the CFormatter class, override its numberFormat
attribute and optionally also its formatNumber()
function. Add the new function unformatNumber()
to the class, so it is available application wide.
For this, create the new file components/Formatter.php
with the content:
class Formatter extends CFormatter
{
/**
* @var array the format used to format a number with PHP number_format() function.
* Three elements may be specified: "decimals", "decimalSeparator" and
* "thousandSeparator". They correspond to the number of digits after
* the decimal point, the character displayed as the decimal point,
* and the thousands separator character.
* new: override default value: 2 decimals, a comma (,) before the decimals
* and no separator between groups of thousands
*/
public $numberFormat=array('decimals'=>2, 'decimalSeparator'=>',', 'thousandSeparator'=>'');
/**
* Formats the value as a number using PHP number_format() function.
* new: if the given $value is null/empty, return null/empty string
* @param mixed $value the value to be formatted
* @return string the formatted result
* @see numberFormat
*/
public function formatNumber($value) {
if($value === null) return null; // new
if($value === '') return ''; // new
return number_format($value, $this->numberFormat['decimals'], $this->numberFormat['decimalSeparator'], $this->numberFormat['thousandSeparator']);
}
/*
* new function unformatNumber():
* turns the given formatted number (string) into a float
* @param string $formatted_number A formatted number
* (usually formatted with the formatNumber() function)
* @return float the 'unformatted' number
*/
public function unformatNumber($formatted_number) {
if($formatted_number === null) return null;
if($formatted_number === '') return '';
if(is_float($formatted_number)) return $formatted_number; // only 'unformat' if parameter is not float already
$value = str_replace($this->numberFormat['thousandSeparator'], '', $formatted_number);
$value = str_replace($this->numberFormat['decimalSeparator'], '.', $value);
return (float) $value;
}
}
Adjust main config file config/main.php
to ensure this new class is used instead of the CFormatter class:
'components'=>array(
//...
'format'=>array(
'class'=>'application.components.Formatter',
),
//...
)
In a view file, or anywhere else for that matter, you can format a numerical value, or 'unformat' a formatted number string, simply by calling:
$formatted_number1 = Yii::app()->format->number($numerical_value);
$formatted_number2 = Yii::app()->format->formatNumber($numerical_value); // same as above
$numerical_value2 = Yii::app()->format->unformatNumber($formatted_number1);
NOTE: We declared the format centrally in one file (Formatter.php), so that it can be changed easily. It can also be configured in the main config file, which will override the value declared in the class file:
'components'=>array(
//...
'format'=>array(
'class'=>'application.components.Formatter',
'numberFormat'=>array('decimals'=>3, 'decimalSeparator'=>',', 'thousandSeparator'=>'-'),
),
//...
),
// ...
2. The 'unformatting' of a number ¶
Extend the CActiveRecord class and override its functions setAttribute()
and setAttributes()
to add the 'unformatting' functionality.
For this, create the new file components/ActiveRecord.php
with the following content:
class ActiveRecord extends CActiveRecord
{
public function setAttributes($values,$safeOnly=true) {
if(!is_array($values)) return;
$attributes=array_flip($safeOnly ? $this->getSafeAttributeNames() : $this->attributeNames());
foreach($values as $name=>$value) {
if(isset($attributes[$name])) {
$column = $this->getTableSchema()->getColumn($name); // new
if (stripos($column->dbType, 'decimal') !== false) // new
$value = Yii::app()->format->unformatNumber($value); // new
$this->$name=$value;
}
else if($safeOnly)
$this->onUnsafeAttribute($name,$value);
}
}
public function setAttribute($name,$value) {
$column = $this->getTableSchema()->getColumn($name); // new
if (stripos($column->dbType, 'decimal') !== false) // new
$value = Yii::app()->format->unformatNumber($value); // new
if(property_exists($this,$name))
$this->$name=$value;
else if(isset($this->getMetaData()->columns[$name]))
$this->_attributes[$name]=$value;
else
return false;
return true;
}
}
Derive all models from this new ActiveRecord class. For example in the Product model models/Product.php
edit the following line:
class Product extends ActiveRecord //old version: "class Product extends CActiveRecord"
In the controller, don't use the POST variables directly, but assign them to the model before further use. If there is a POST variable that is not to be assigned to any model and that contains a formatted number string, make sure to 'unformat' it before further use. That is:
$product->attributes = $_POST['Product']; // This massively assigns all received values using the setAttributes() function.
$reduced_price = $product->price - 4.50; // Correct, $product->price is valid numerical value
// DO NOT USE A POST VARIABLE DIRECTLY, SINCE IT IS NOT 'UNFORMATTED'!!
$reduced_price = $_POST['Product']['price']; // Incorrect! $_POST['Product']['price'] is not valid numerical value
// 'Unformat' a formatted number input manually if necessary:
$wish_price = Yii::app()->format->unformatNumber($_POST['wish_price']);
If something is unclear, wrong or incomplete, please let me know. Any suggestions for improvements will be appreciated!
nice.. this is a good wiki
as a Yii beginner, this is a good practise.
I already use the really bad approach and make a new function on every numeric field. I know this is not a good practise until i found this wiki...
thank you..
How to use a custom formatter using 'format' option ?
Some widget, like cgridview, accept a 'format' option.
If I use
format => 'CustomNumber'
where 'CustomNumber' is the formatCustomNumber function, Yii doens't generate any error, but simply doesn't format this. How to ?
I don't want to use ->format->formatBlaBlaBla(..) if it's unnecessary
@realtebo
'format'=>'customNumber'
in CGridView should work for the method
formatCustomNumber
.The problem in your case might be the capital first letter.
Great Guide. One more thing.
check whether the form page doesn't use clientValidation(java script).
beforeValidation() function doesn't not affect clientValidation. becasue validate done in client side.
So use ajax validation.
Small error in unformatNumber
The 3rd line of the unformatNumber function in Formatter checks whether the number is already a float. But it fails, the correct way is:
if(filter_var($formatted_number, FILTER_VALIDATE_FLOAT)!==false) return $formatted_number; // only 'unformat' if parameter is not float already
because is_float() will always return false if it is passed a string (which will be the case).
It could be important if you also have the thousands seperator set:
"99.99" does not pass as a float in is_float(), then the next step will remove the thousands seperator... hence the value becomes 9999. Woops.
Validation related issue
Very good instructions, helped me a lot.
However, I had small issues with validating incorrect numerical values. Inserting e.g. any random string of letters didn't cause validation errors, even though model's rules allowed only numerical values.
At the end I noticed reason to be this in unformatNumber():
return (float) $value;
For $value containing e.g. letters, the returned value is 0 which then validates OK.
I resolved this by adding one line to unformatNumber():
if(!preg_match('/^[0-9,]+$/i', $formatted_number)) return $formatted_number; // only unformat if parameter includes numbers or comma
Looks to be working as intended. Has anybody else seen same issue? If so, was some other solution used?
decimal types
I don't use 'decimal' type, but 'float'.
And my setAttributes function sees other
class ActiveRecord extends CActiveRecord { public function setAttributes($values,$safeOnly=true) { if(!is_array($values)) return; $attributes=array_flip($safeOnly ? $this->getSafeAttributeNames() : $this->attributeNames()); foreach($values as $name=>$value) { if(isset($attributes[$name])) { $column = $this->getTableSchema()->getColumn($name); // new if (stripos($column->dbType, 'decimal') !== false OR stripos($column->dbType, 'float') !== false OR stripos($column->dbType, 'double') !== false OR stripos($column->dbType, 'real') !== false) // new $value = Yii::app()->format->unformatNumber($value); // new $this->$name=$value; } else if($safeOnly) $this->onUnsafeAttribute($name,$value); } } }
Attributes only for form, not for database
I use some attributes for form, but I saven't they in my database. Any of this attributes can be nummerical too.
Hiere is my code for 2. The 'unformatting' of a number
class ActiveRecord extends CActiveRecord { public function setAttributes($values,$safeOnly=true) { if(!is_array($values)) return; $attributes=array_flip($safeOnly ? $this->getSafeAttributeNames() : $this->attributeNames()); foreach($values as $name=>$value) { if(isset($attributes[$name])) { // Get all validation rules for this attribute (as array) $validatorsArray = $this->getValidators($name); // Going each validation class foreach ($validatorsArray as $validatorClass) { // Looking for numbers, not integer if (get_class($validatorClass) == 'CNumberValidator' AND !$validatorClass->integerOnly) $value = Yii::app()->format->unformatNumber($value); } // foreach $this->$name=$value; } else if($safeOnly) $this->onUnsafeAttribute($name,$value); } } }
I don't see at getTableSchema, but i looking in the validations rules of this model for this attribute.
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.