Description
Q | A |
---|---|
Bug report? | yes |
Feature request? | no |
BC Break report? | no |
RFC? | no |
Symfony version | 3.3.9 |
Context
I have a form based off of an entity that has a big int field:
/**
* @var integer
*
* @ORM\Column(name="ticket_id", type="bigint")
*/
private $ticketId;
MySQL is the database engine and when entering manual queries it works perfectly.
The form is built with this type:
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('ticketId', IntegerType::class, ['scale' => 16]);
}
Note that I also tried the one argument add
method with exactly the same result.
The issue
Now when I submit the form with an input of 201803221011791
it is saved as 201803221011790
.
Analysis
I have debugged the code to this portion of NumberToLocalizedStringTransformer:184
where the data is cast as a float:
if (is_int($result) && $result === (int) $float = (float) $result) {
$result = $float;
}
This looks like a float but it seems that PHP handles that as a double internally:
$ php -r '$result = 201803221011791; var_dump((float) $result);'
Command line code:1:
double(2.0180322101179E+14)
The precision is not lost yet:
$ php -r 'printf("%d\n", (float) 201803221011791);'
201803221011791
So later on in the round
method line 244, it is parsed as a string (scale
is 0
so the roundingCoef
is 1
):
// shift number to maintain the correct scale during rounding
$roundingCoef = pow(10, $this->scale);
// string representation to avoid rounding errors, similar to bcmul()
$number = (string) ($number * $roundingCoef);
So from now on number
is 2
:
$ php -r 'printf("%d\n", (string)((float) 201803221011791 * 1));'
2
Later on line 259 (roundingMode
is ROUND_DOWN
):
$number = $number > 0 ? floor($number) : ceil($number);
So basically we are doing this:
$ php -r 'printf("%d\n", floor((string)(float) 201803221011791));'
201803221011790
Which as you can see causes the last digit to be wrong.
Additional remark
The IntegerType
uses IntegerToLocalizedStringTransformer
, but the constructor erases the scale
option, and calls the parent constructor with scale
=0
hard-coded:
/**
* Constructs a transformer.
*
* @param int $scale Unused
* @param bool $grouping Whether thousands should be grouped
* @param int $roundingMode One of the ROUND_ constants in this class
*/
public function __construct($scale = 0, $grouping = false, $roundingMode = self::ROUND_DOWN)
{
if (null === $roundingMode) {
$roundingMode = self::ROUND_DOWN;
}
parent::__construct(0, $grouping, $roundingMode);
}
The PHPDoc mentions the scale parameter is ignored, but this is not documented here: https://symfony.com/doc/3.4/reference/forms/types/number.html#scale