Added new option "input" to NumberType

This commit is contained in:
Bernhard Schussek 2019-04-06 15:14:18 +02:00
parent fb2b37a8f3
commit 3f25734647
11 changed files with 243 additions and 365 deletions

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
4.3.0
-----
* changed guessing of DECIMAL to set the `input` option of `NumberType` to string
4.2.0
-----

View File

@ -1,91 +0,0 @@
<?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\Bridge\Doctrine\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class NumberToStringTransformer implements DataTransformerInterface
{
/**
* @var bool
*/
private $forceFullScale;
/**
* @var int|null
*/
private $scale;
/**
* @param bool $forceFullScale
* @param int|null $scale
*/
public function __construct($forceFullScale = false, $scale = null)
{
$this->forceFullScale = $forceFullScale;
$this->scale = $scale;
}
/**
* @param mixed $value
*
* @return string|null
*/
public function transform($value)
{
if (null === $value) {
return null;
}
if (!is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
return $value;
}
/**
* @param mixed $value
*
* @return string|null
*/
public function reverseTransform($value)
{
if (null === $value) {
return null;
}
if (is_string($value)) {
return $value;
}
$valueIsInt = is_int($value);
if (!$valueIsInt && !is_float($value)) {
throw new TransformationFailedException('Expected an int or a float.');
}
if ($this->forceFullScale && is_int($this->scale)) {
if ($valueIsInt) {
$value = floatval($value);
}
return number_format($value, $this->scale, '.', '');
}
try {
return (string) $value;
} catch (\Exception $e) {
throw new TransformationFailedException();
}
}
}

View File

@ -17,7 +17,6 @@ use Doctrine\Common\Persistence\Proxy;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Mapping\MappingException as LegacyMappingException;
use Symfony\Bridge\Doctrine\Form\Type\DecimalType;
use Symfony\Component\Form\FormTypeGuesserInterface;
use Symfony\Component\Form\Guess\Guess;
use Symfony\Component\Form\Guess\TypeGuess;
@ -76,7 +75,7 @@ class DoctrineOrmTypeGuesser implements FormTypeGuesserInterface
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(DecimalType::class, array(), Guess::HIGH_CONFIDENCE);
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:

View File

@ -1,50 +0,0 @@
<?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\Bridge\Doctrine\Form\Type;
use Symfony\Bridge\Doctrine\Form\DataTransformer\NumberToStringTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class DecimalType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addModelTransformer(new NumberToStringTransformer($options['force_full_scale'], $options['scale']));
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'force_full_scale' => false
));
$resolver->setAllowedTypes('force_full_scale', array(
'boolean'
));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return NumberType::class;
}
}

View File

@ -1,40 +0,0 @@
<?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\Bridge\Doctrine\Tests\Fixtures;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Column;
/** @Entity */
class Price
{
/** @Id @Column(type="integer") */
public $id;
/** @Column(type="decimal", scale=2) */
public $doesNotPreserveFullScaleValue;
/** @Column(type="string") */
public $preserveFullScaleValueSimulation;
/**
* @param int $id
* @param float $value
*/
public function __construct(int $id, float $value)
{
$this->id = $id;
$this->doesNotPreserveFullScaleValue = $value;
$this->preserveFullScaleValueSimulation = number_format($value, 2, '.', '');
}
}

View File

@ -1,177 +0,0 @@
<?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\Bridge\Doctrine\Tests\Form\Type;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\SchemaTool;
use Symfony\Bridge\Doctrine\Form\Type\DecimalType;
use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper;
use Symfony\Bridge\Doctrine\Tests\Fixtures\Price;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Tests\Extension\Core\Type\BaseTypeTest;
class DecimalTypeTest extends BaseTypeTest
{
/**
* @var string
*/
const TESTED_TYPE = DecimalType::class;
/**
* @var EntityManager
*/
private $em;
protected function setUp()
{
$this->em = DoctrineTestHelper::createTestEntityManager();
parent::setUp();
$schemaTool = new SchemaTool($this->em);
$classes = array(
$this->em->getClassMetadata(Price::class)
);
try {
$schemaTool->dropSchema($classes);
} catch (\Exception $e) {
}
try {
$schemaTool->createSchema($classes);
} catch (\Exception $e) {
}
}
protected function tearDown()
{
parent::tearDown();
$this->em = null;
}
// On some platforms, fetched decimal values are rounded (the full scale is not preserved)
// eg : on SQLite, inserted float value 4.50 will be fetched as string value "4.5"
public function testSubmitWithSameStringValueOnAPlatformThatDoesNotPreserveFullScaleValueWithoutForceFullScale()
{
$fullScalePrice = new Price(1, 1.23);
$nonFullScalePrice = new Price(2, 4.50);
$this->em->persist($fullScalePrice);
$this->em->persist($nonFullScalePrice);
$this->em->flush();
$this->em->refresh($fullScalePrice);
$this->em->refresh($nonFullScalePrice);
$this->assertInternalType('string', $fullScalePrice->doesNotPreserveFullScaleValue);
$fullScalePriceStringValue = $fullScalePrice->doesNotPreserveFullScaleValue;
$formBuilder = $this->factory->createBuilder(FormType::class, $fullScalePrice, array(
'data_class' => Price::class
));
$formBuilder->add('doesNotPreserveFullScaleValue', static::TESTED_TYPE, array(
'force_full_scale' => false
));
$form = $formBuilder->getForm();
$form->submit(array(
'doesNotPreserveFullScaleValue' => $fullScalePriceStringValue
));
$this->assertSame($fullScalePriceStringValue, $fullScalePrice->doesNotPreserveFullScaleValue);
$this->assertInternalType('string', $nonFullScalePrice->doesNotPreserveFullScaleValue);
$nonFullScalePriceStringValue = $nonFullScalePrice->doesNotPreserveFullScaleValue;
$formBuilder = $this->factory->createBuilder(FormType::class, $nonFullScalePrice, array(
'data_class' => Price::class
));
$formBuilder->add('doesNotPreserveFullScaleValue', static::TESTED_TYPE, array(
'force_full_scale' => false
));
$form = $formBuilder->getForm();
$form->submit(array(
'doesNotPreserveFullScaleValue' => $nonFullScalePriceStringValue
));
$this->assertSame($nonFullScalePriceStringValue, $nonFullScalePrice->doesNotPreserveFullScaleValue);
$unitOfWork = $this->em->getUnitOfWork();
$unitOfWork->computeChangeSets();
$this->assertSame(array(), $unitOfWork->getEntityChangeSet($fullScalePrice));
$this->assertSame(array(), $unitOfWork->getEntityChangeSet($nonFullScalePrice));
}
// On some platforms, fetched decimal values are not rounded at all (the full scale is preserved)
// eg : on PostgreSQL, inserted float value 4.50 will be fetched as string value "4.50"
public function testSubmitWithSameStringValueOnAPlatformThatPreserveFullScaleValueWithForceFullScale()
{
$fullScalePrice = new Price(1, 1.23);
$nonFullScalePrice = new Price(2, 4.50);
$this->em->persist($fullScalePrice);
$this->em->persist($nonFullScalePrice);
$this->em->flush();
$this->em->refresh($fullScalePrice);
$this->em->refresh($nonFullScalePrice);
$this->assertInternalType('string', $fullScalePrice->preserveFullScaleValueSimulation);
$fullScalePriceStringValue = $fullScalePrice->preserveFullScaleValueSimulation;
$formBuilder = $this->factory->createBuilder(FormType::class, $fullScalePrice, array(
'data_class' => Price::class
));
$formBuilder->add('preserveFullScaleValueSimulation', static::TESTED_TYPE, array(
'force_full_scale' => true,
'scale' => 2
));
$form = $formBuilder->getForm();
$form->submit(array(
'preserveFullScaleValueSimulation' => $fullScalePriceStringValue
));
$this->assertSame($fullScalePriceStringValue, $fullScalePrice->preserveFullScaleValueSimulation);
$this->assertInternalType('string', $nonFullScalePrice->preserveFullScaleValueSimulation);
$nonFullScalePriceStringValue = $nonFullScalePrice->preserveFullScaleValueSimulation;
$formBuilder = $this->factory->createBuilder(FormType::class, $nonFullScalePrice, array(
'data_class' => Price::class
));
$formBuilder->add('preserveFullScaleValueSimulation', static::TESTED_TYPE, array(
'force_full_scale' => true,
'scale' => 2
));
$form = $formBuilder->getForm();
$form->submit(array(
'preserveFullScaleValueSimulation' => $nonFullScalePriceStringValue
));
$this->assertSame($nonFullScalePriceStringValue, $nonFullScalePrice->preserveFullScaleValueSimulation);
$unitOfWork = $this->em->getUnitOfWork();
$unitOfWork->computeChangeSets();
$this->assertSame(array(), $unitOfWork->getEntityChangeSet($fullScalePrice));
$this->assertSame(array(), $unitOfWork->getEntityChangeSet($nonFullScalePrice));
}
public function testSubmitNull($expected = null, $norm = null, $view = null)
{
parent::testSubmitNull($expected, $norm, '');
}
}

View File

@ -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
-----

View File

@ -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;
}
}

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\Form\Extension\Core\Type;
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 @@ class NumberType extends AbstractType
$options['rounding_mode'],
$options['html5'] ? 'en' : null
));
if ('string' === $options['input']) {
$builder->addModelTransformer(new StringToFloatTransformer($options['scale']));
}
}
/**
@ -56,6 +61,7 @@ class NumberType extends AbstractType
'grouping' => false,
'rounding_mode' => NumberToLocalizedStringTransformer::ROUND_HALF_UP,
'compound' => false,
'input' => 'number',
'html5' => false,
]);
@ -68,7 +74,7 @@ class NumberType extends AbstractType
NumberToLocalizedStringTransformer::ROUND_UP,
NumberToLocalizedStringTransformer::ROUND_CEILING,
]);
$resolver->setAllowedValues('input', ['number', 'string']);
$resolver->setAllowedTypes('scale', ['null', 'int']);
$resolver->setAllowedTypes('html5', 'bool');

View File

@ -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');
}
}

View File

@ -27,7 +27,7 @@ class NumberTypeTest extends BaseTypeTest
\Locale::setDefault('de_DE');
}
public function testDefaultFormatting()
public function testDefaultFormatting(): void
{
$form = $this->factory->create(static::TESTED_TYPE);
$form->setData('12345.67890');
@ -35,7 +35,7 @@ class NumberTypeTest extends BaseTypeTest
$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');
@ -43,7 +43,7 @@ class NumberTypeTest extends BaseTypeTest
$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');
@ -51,7 +51,23 @@ class NumberTypeTest extends BaseTypeTest
$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 @@ class NumberTypeTest extends BaseTypeTest
$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, [