Skip to content
Permalink
Browse files

feature #30893 Add "input" option to NumberType (fancyweb, Bernhard S…

…chussek)

This PR was merged into the 4.3-dev branch.

Discussion
----------

Add "input" option to NumberType

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #24793
| License       | MIT
| Doc PR        | TODO

This PR replaces #24793 in (partially) fixing how Doctrine's DECIMAL type is handled by the Form component.

Previously, DECIMAL was mapped to the regular NumberType. That confuses Doctrine's change detection, depending on the DB platform.

Examples:

| DB | DB value | Doctrine entity before submit | Form input | Doctrine entity after submit
| --- | --- | --- | --- | ---
| SQLite | 8.000 | '8' | 8 | 8
| SQLite | 8.123 | '8.123' | 8.123 | 8.123
| PostgreSQL | 8.000 | '8.000' | 8 | 8
| PostgreSQL | 8.123 | '8.123' | 8.123 | 8.123

The value in the Doctrine entity changes before and after submit. Hence Doctrine believes an update is necessary.

This PR introduces an `input` option to NumberType (similar to DateType), that can be set to `'number'` (default) or `'string'`. If set to `'string'`, the conversion is as follows:

| DB | DB value | Doctrine entity before submit | Form input | Doctrine entity after submit
| --- | --- | --- | --- | ---
| SQLite | 8.000 | **'8'** | 8 | **'8.000'**
| SQLite | 8.123 | '8.123' | 8.123 | '8.123'
| PostgreSQL | 8.000 | '8.000' | 8 | '8.000'
| PostgreSQL | 8.123 | '8.123' | 8.123 | '8.123'

You see that this does not completely solve this issue for SQLite. However, @Ocramius and I agree that this is something to be fixed by Doctrine, since Doctrine is providing the database abstraction.

That fix should be done in the SqlitePlatform object in Doctrine as part of another PR and should be backwards compatible with the current Doctrine version (i.e. opt in via configuration).

Compared to #24793, this PR does not introduce a new type but instead makes the NumberType more flexible. Also, this PR does not introduce the `force_full_scale` option since that should be solved by Doctrine as described above.

Commits
-------

3f25734 Added new option "input" to NumberType
fb2b37a add force_full_scale option to handle all cases
40f2512 [DoctrineBridge] Add decimal form type
  • Loading branch information...
fabpot committed Apr 6, 2019
2 parents f3a0555 + 3f25734 commit 4c78e60ad5ba7d62f1cc947d7a108bfe0b9ae86d
@@ -1,6 +1,11 @@
CHANGELOG
=========

4.3.0
-----

* changed guessing of DECIMAL to set the `input` option of `NumberType` to string

4.2.0
-----

@@ -75,6 +75,7 @@ public function guessType($class, $property)
case 'time_immutable':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TimeType', ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE);
case Type::DECIMAL:
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\NumberType', ['input' => 'string'], Guess::MEDIUM_CONFIDENCE);
case Type::FLOAT:
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\NumberType', [], Guess::MEDIUM_CONFIDENCE);
case Type::INTEGER:
@@ -47,6 +47,7 @@ CHANGELOG
* dispatch `PostSubmitEvent` on `form.post_submit`
* dispatch `PreSetDataEvent` on `form.pre_set_data`
* dispatch `PostSetDataEvent` on `form.post_set_data`
* added an `input` option to `NumberType`

4.2.0
-----
@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class StringToFloatTransformer implements DataTransformerInterface
{
private $scale;
public function __construct(int $scale = null)
{
$this->scale = $scale;
}
/**
* @param mixed $value
*
* @return float|null
*/
public function transform($value)
{
if (null === $value) {
return null;
}
if (!\is_string($value) || !is_numeric($value)) {
throw new TransformationFailedException('Expected a numeric string.');
}
return (float) $value;
}
/**
* @param mixed $value
*
* @return string|null
*/
public function reverseTransform($value)
{
if (null === $value) {
return null;
}
if (!\is_int($value) && !\is_float($value)) {
throw new TransformationFailedException('Expected a numeric.');
}
if ($this->scale > 0) {
return number_format((float) $value, $this->scale, '.', '');
}
return (string) $value;
}
}
@@ -14,6 +14,7 @@
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\StringToFloatTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
@@ -33,6 +34,10 @@ public function buildForm(FormBuilderInterface $builder, array $options)
$options['rounding_mode'],
$options['html5'] ? 'en' : null
));
if ('string' === $options['input']) {
$builder->addModelTransformer(new StringToFloatTransformer($options['scale']));
}
}
/**
@@ -56,6 +61,7 @@ public function configureOptions(OptionsResolver $resolver)
'grouping' => false,
'rounding_mode' => NumberToLocalizedStringTransformer::ROUND_HALF_UP,
'compound' => false,
'input' => 'number',
'html5' => false,
]);
@@ -68,7 +74,7 @@ public function configureOptions(OptionsResolver $resolver)
NumberToLocalizedStringTransformer::ROUND_UP,
NumberToLocalizedStringTransformer::ROUND_CEILING,
]);
$resolver->setAllowedValues('input', ['number', 'string']);
$resolver->setAllowedTypes('scale', ['null', 'int']);
$resolver->setAllowedTypes('html5', 'bool');
@@ -0,0 +1,104 @@
<?php
/*
* This file is part of the symfony/symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\Extension\Core\DataTransformer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\Extension\Core\DataTransformer\StringToFloatTransformer;
class StringToFloatTransformerTest extends TestCase
{
private $transformer;
protected function setUp()
{
$this->transformer = new StringToFloatTransformer();
}
protected function tearDown()
{
$this->transformer = null;
}
public function provideTransformations(): array
{
return [
[null, null],
['1', 1.],
['1.', 1.],
['1.0', 1.],
['1.23', 1.23],
];
}
/**
* @dataProvider provideTransformations
*/
public function testTransform($from, $to): void
{
$transformer = new StringToFloatTransformer();
$this->assertSame($to, $transformer->transform($from));
}
/**
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
*/
public function testFailIfTransformingANonString(): void
{
$transformer = new StringToFloatTransformer();
$transformer->transform(1.0);
}
/**
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
*/
public function testFailIfTransformingANonNumericString(): void
{
$transformer = new StringToFloatTransformer();
$transformer->transform('foobar');
}
public function provideReverseTransformations(): array
{
return [
[null, null],
[1, '1'],
[1., '1'],
[1.0, '1'],
[1.23, '1.23'],
[1, '1.000', 3],
[1.0, '1.000', 3],
[1.23, '1.230', 3],
[1.2344, '1.234', 3],
[1.2345, '1.235', 3],
];
}
/**
* @dataProvider provideReverseTransformations
*/
public function testReverseTransform($from, $to, int $scale = null): void
{
$transformer = new StringToFloatTransformer($scale);
$this->assertSame($to, $transformer->reverseTransform($from));
}
/**
* @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
*/
public function testFailIfReverseTransformingANonNumeric(): void
{
$transformer = new StringToFloatTransformer();
$transformer->reverseTransform('foobar');
}
}
@@ -27,31 +27,47 @@ protected function setUp()
\Locale::setDefault('de_DE');
}
public function testDefaultFormatting()
public function testDefaultFormatting(): void
{
$form = $this->factory->create(static::TESTED_TYPE);
$form->setData('12345.67890');
$this->assertSame('12345,679', $form->createView()->vars['value']);
}
public function testDefaultFormattingWithGrouping()
public function testDefaultFormattingWithGrouping(): void
{
$form = $this->factory->create(static::TESTED_TYPE, null, ['grouping' => true]);
$form->setData('12345.67890');
$this->assertSame('12.345,679', $form->createView()->vars['value']);
}
public function testDefaultFormattingWithScale()
public function testDefaultFormattingWithScale(): void
{
$form = $this->factory->create(static::TESTED_TYPE, null, ['scale' => 2]);
$form->setData('12345.67890');
$this->assertSame('12345,68', $form->createView()->vars['value']);
}
public function testDefaultFormattingWithRounding()
public function testDefaultFormattingWithScaleFloat(): void
{
$form = $this->factory->create(static::TESTED_TYPE, null, ['scale' => 2]);
$form->setData(12345.67890);
$this->assertSame('12345,68', $form->createView()->vars['value']);
}
public function testDefaultFormattingWithScaleAndStringInput(): void
{
$form = $this->factory->create(static::TESTED_TYPE, null, ['scale' => 2, 'input' => 'string']);
$form->setData('12345.67890');
$this->assertSame('12345,68', $form->createView()->vars['value']);
}
public function testDefaultFormattingWithRounding(): void
{
$form = $this->factory->create(static::TESTED_TYPE, null, ['scale' => 0, 'rounding_mode' => \NumberFormatter::ROUND_UP]);
$form->setData('12345.54321');
@@ -76,6 +92,46 @@ public function testSubmitNullUsesDefaultEmptyData($emptyData = '10', $expectedD
$this->assertSame($expectedData, $form->getData());
}
public function testSubmitNumericInput(): void
{
$form = $this->factory->create(static::TESTED_TYPE, null, ['input' => 'number']);
$form->submit('1,234');
$this->assertSame(1.234, $form->getData());
$this->assertSame(1.234, $form->getNormData());
$this->assertSame('1,234', $form->getViewData());
}
public function testSubmitNumericInputWithScale(): void
{
$form = $this->factory->create(static::TESTED_TYPE, null, ['input' => 'number', 'scale' => 2]);
$form->submit('1,234');
$this->assertSame(1.23, $form->getData());
$this->assertSame(1.23, $form->getNormData());
$this->assertSame('1,23', $form->getViewData());
}
public function testSubmitStringInput(): void
{
$form = $this->factory->create(static::TESTED_TYPE, null, ['input' => 'string']);
$form->submit('1,234');
$this->assertSame('1.234', $form->getData());
$this->assertSame(1.234, $form->getNormData());
$this->assertSame('1,234', $form->getViewData());
}
public function testSubmitStringInputWithScale(): void
{
$form = $this->factory->create(static::TESTED_TYPE, null, ['input' => 'string', 'scale' => 2]);
$form->submit('1,234');
$this->assertSame('1.23', $form->getData());
$this->assertSame(1.23, $form->getNormData());
$this->assertSame('1,23', $form->getViewData());
}
public function testIgnoresDefaultLocaleToRenderHtml5NumberWidgets()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [

0 comments on commit 4c78e60

Please sign in to comment.
You can’t perform that action at this time.