From 40f25121c3964a8ce72c05727d45954012a910f5 Mon Sep 17 00:00:00 2001 From: Thomas Calvet Date: Thu, 2 Nov 2017 13:33:12 +0100 Subject: [PATCH 1/3] [DoctrineBridge] Add decimal form type --- .../Doctrine/Form/DoctrineOrmTypeGuesser.php | 2 + .../Bridge/Doctrine/Form/Type/DecimalType.php | 57 +++++++++++ .../Bridge/Doctrine/Tests/Fixtures/Price.php | 36 +++++++ .../Tests/Form/Type/DecimalTypeTest.php | 96 +++++++++++++++++++ 4 files changed, 191 insertions(+) create mode 100644 src/Symfony/Bridge/Doctrine/Form/Type/DecimalType.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Fixtures/Price.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Form/Type/DecimalTypeTest.php diff --git a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php index 49dfd9bfbc..78a2ba9510 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php @@ -17,6 +17,7 @@ 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; @@ -75,6 +76,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); case Type::FLOAT: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\NumberType', [], Guess::MEDIUM_CONFIDENCE); case Type::INTEGER: diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DecimalType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DecimalType.php new file mode 100644 index 0000000000..9956c3de10 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DecimalType.php @@ -0,0 +1,57 @@ + + * + * 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\Component\Form\AbstractType; +use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\Core\Type\NumberType; +use Symfony\Component\Form\FormBuilderInterface; + +class DecimalType extends AbstractType +{ + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addModelTransformer(new CallbackTransformer(function ($value) { + if (null === $value) { + return null; + } + + if (!is_string($value)) { + throw new TransformationFailedException('Expected a string.'); + } + + return $value; + }, function ($value) { + if (null === $value) { + return null; + } + + if (!is_int($value) && !is_float($value)) { + throw new TransformationFailedException('Expected an int or a float.'); + } + + return (string) $value; + })); + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return NumberType::class; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Price.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Price.php new file mode 100644 index 0000000000..bd7c645766 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Price.php @@ -0,0 +1,36 @@ + + * + * 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") */ + public $value; + + /** + * @param int $id + * @param float $value + */ + public function __construct(int $id, float $value) + { + $this->id = $id; + $this->value = $value; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/DecimalTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/DecimalTypeTest.php new file mode 100644 index 0000000000..b7601b1a94 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/DecimalTypeTest.php @@ -0,0 +1,96 @@ + + * + * 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; + } + + public function testSubmitWithSameStringValue() + { + $price = new Price(1, 1.23); + $this->em->persist($price); + $this->em->flush(); + + $this->em->refresh($price); + + $this->assertInternalType('string', $price->value); + $stringValue = $price->value; + + $formBuilder = $this->factory->createBuilder(FormType::class, $price, array( + 'data_class' => Price::class + )); + $formBuilder->add('value', static::TESTED_TYPE); + + $form = $formBuilder->getForm(); + $form->submit(array( + 'value' => $stringValue + )); + + $this->assertSame($stringValue, $price->value); + + $unitOfWork = $this->em->getUnitOfWork(); + $unitOfWork->computeChangeSets(); + + $this->assertSame(array(), $unitOfWork->getEntityChangeSet($price)); + } + + public function testSubmitNull($expected = null, $norm = null, $view = null) + { + parent::testSubmitNull($expected, $norm, ''); + } +} From fb2b37a8f3dc1131938bc25cdcdebd944d284f51 Mon Sep 17 00:00:00 2001 From: Thomas Calvet Date: Tue, 13 Mar 2018 16:10:41 +0100 Subject: [PATCH 2/3] add force_full_scale option to handle all cases --- .../NumberToStringTransformer.php | 91 ++++++++++++++++ .../Bridge/Doctrine/Form/Type/DecimalType.php | 37 +++---- .../Bridge/Doctrine/Tests/Fixtures/Price.php | 10 +- .../Tests/Form/Type/DecimalTypeTest.php | 103 ++++++++++++++++-- 4 files changed, 205 insertions(+), 36 deletions(-) create mode 100644 src/Symfony/Bridge/Doctrine/Form/DataTransformer/NumberToStringTransformer.php diff --git a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/NumberToStringTransformer.php b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/NumberToStringTransformer.php new file mode 100644 index 0000000000..803df3c1d9 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/NumberToStringTransformer.php @@ -0,0 +1,91 @@ + + * + * 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(); + } + } +} diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DecimalType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DecimalType.php index 9956c3de10..6c67aacd4b 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DecimalType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DecimalType.php @@ -11,11 +11,11 @@ namespace Symfony\Bridge\Doctrine\Form\Type; +use Symfony\Bridge\Doctrine\Form\DataTransformer\NumberToStringTransformer; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\CallbackTransformer; -use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; class DecimalType extends AbstractType { @@ -24,27 +24,20 @@ class DecimalType extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->addModelTransformer(new CallbackTransformer(function ($value) { - if (null === $value) { - return null; - } + $builder->addModelTransformer(new NumberToStringTransformer($options['force_full_scale'], $options['scale'])); + } - if (!is_string($value)) { - throw new TransformationFailedException('Expected a string.'); - } - - return $value; - }, function ($value) { - if (null === $value) { - return null; - } - - if (!is_int($value) && !is_float($value)) { - throw new TransformationFailedException('Expected an int or a float.'); - } - - return (string) $value; - })); + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'force_full_scale' => false + )); + $resolver->setAllowedTypes('force_full_scale', array( + 'boolean' + )); } /** diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Price.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Price.php index bd7c645766..3601d30d03 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Price.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Price.php @@ -21,8 +21,11 @@ class Price /** @Id @Column(type="integer") */ public $id; - /** @Column(type="decimal") */ - public $value; + /** @Column(type="decimal", scale=2) */ + public $doesNotPreserveFullScaleValue; + + /** @Column(type="string") */ + public $preserveFullScaleValueSimulation; /** * @param int $id @@ -31,6 +34,7 @@ class Price public function __construct(int $id, float $value) { $this->id = $id; - $this->value = $value; + $this->doesNotPreserveFullScaleValue = $value; + $this->preserveFullScaleValueSimulation = number_format($value, 2, '.', ''); } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/DecimalTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/DecimalTypeTest.php index b7601b1a94..f2ae341107 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/DecimalTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/DecimalTypeTest.php @@ -60,33 +60,114 @@ class DecimalTypeTest extends BaseTypeTest $this->em = null; } - public function testSubmitWithSameStringValue() + // 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() { - $price = new Price(1, 1.23); - $this->em->persist($price); + $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($price); + $this->em->refresh($fullScalePrice); + $this->em->refresh($nonFullScalePrice); - $this->assertInternalType('string', $price->value); - $stringValue = $price->value; + $this->assertInternalType('string', $fullScalePrice->doesNotPreserveFullScaleValue); + $fullScalePriceStringValue = $fullScalePrice->doesNotPreserveFullScaleValue; - $formBuilder = $this->factory->createBuilder(FormType::class, $price, array( + $formBuilder = $this->factory->createBuilder(FormType::class, $fullScalePrice, array( 'data_class' => Price::class )); - $formBuilder->add('value', static::TESTED_TYPE); + $formBuilder->add('doesNotPreserveFullScaleValue', static::TESTED_TYPE, array( + 'force_full_scale' => false + )); $form = $formBuilder->getForm(); $form->submit(array( - 'value' => $stringValue + 'doesNotPreserveFullScaleValue' => $fullScalePriceStringValue )); - $this->assertSame($stringValue, $price->value); + $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($price)); + $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) From 3f257346472a815f850fb65cbc81dd82e0b9f55a Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Sat, 6 Apr 2019 15:14:18 +0200 Subject: [PATCH 3/3] Added new option "input" to NumberType --- src/Symfony/Bridge/Doctrine/CHANGELOG.md | 5 + .../NumberToStringTransformer.php | 91 --------- .../Doctrine/Form/DoctrineOrmTypeGuesser.php | 3 +- .../Bridge/Doctrine/Form/Type/DecimalType.php | 50 ----- .../Bridge/Doctrine/Tests/Fixtures/Price.php | 40 ---- .../Tests/Form/Type/DecimalTypeTest.php | 177 ------------------ src/Symfony/Component/Form/CHANGELOG.md | 1 + .../StringToFloatTransformer.php | 65 +++++++ .../Form/Extension/Core/Type/NumberType.php | 8 +- .../StringToFloatTransformerTest.php | 104 ++++++++++ .../Extension/Core/Type/NumberTypeTest.php | 64 ++++++- 11 files changed, 243 insertions(+), 365 deletions(-) delete mode 100644 src/Symfony/Bridge/Doctrine/Form/DataTransformer/NumberToStringTransformer.php delete mode 100644 src/Symfony/Bridge/Doctrine/Form/Type/DecimalType.php delete mode 100644 src/Symfony/Bridge/Doctrine/Tests/Fixtures/Price.php delete mode 100644 src/Symfony/Bridge/Doctrine/Tests/Form/Type/DecimalTypeTest.php create mode 100644 src/Symfony/Component/Form/Extension/Core/DataTransformer/StringToFloatTransformer.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/StringToFloatTransformerTest.php diff --git a/src/Symfony/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Bridge/Doctrine/CHANGELOG.md index c333361d4a..6b617825c9 100644 --- a/src/Symfony/Bridge/Doctrine/CHANGELOG.md +++ b/src/Symfony/Bridge/Doctrine/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +4.3.0 +----- + + * changed guessing of DECIMAL to set the `input` option of `NumberType` to string + 4.2.0 ----- diff --git a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/NumberToStringTransformer.php b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/NumberToStringTransformer.php deleted file mode 100644 index 803df3c1d9..0000000000 --- a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/NumberToStringTransformer.php +++ /dev/null @@ -1,91 +0,0 @@ - - * - * 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(); - } - } -} diff --git a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php index 78a2ba9510..34fb04aed2 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php @@ -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: diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DecimalType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DecimalType.php deleted file mode 100644 index 6c67aacd4b..0000000000 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DecimalType.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * 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; - } -} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Price.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Price.php deleted file mode 100644 index 3601d30d03..0000000000 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Price.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * 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, '.', ''); - } -} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/DecimalTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/DecimalTypeTest.php deleted file mode 100644 index f2ae341107..0000000000 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/DecimalTypeTest.php +++ /dev/null @@ -1,177 +0,0 @@ - - * - * 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, ''); - } -} diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 0d3d468586..ae21b1de9d 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -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 ----- diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/StringToFloatTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/StringToFloatTransformer.php new file mode 100644 index 0000000000..27e60b4306 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/StringToFloatTransformer.php @@ -0,0 +1,65 @@ + + * + * 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; + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php b/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php index a0257f0269..4c1f1fd71f 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php @@ -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'); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/StringToFloatTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/StringToFloatTransformerTest.php new file mode 100644 index 0000000000..5726a217da --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/StringToFloatTransformerTest.php @@ -0,0 +1,104 @@ + + * + * 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'); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/NumberTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/NumberTypeTest.php index 1ab6c8e9fb..91b15ca760 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/NumberTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/NumberTypeTest.php @@ -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, [