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 49dfd9bfbce6e..78a2ba9510b2e 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php @@ -17,6 +17,7 @@ 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 @@ 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(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 0000000000000..9956c3de10d33 --- /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 0000000000000..bd7c645766af6 --- /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 0000000000000..b7601b1a9466e --- /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 0000000000000..803df3c1d9935 --- /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 9956c3de10d33..6c67aacd4bcbb 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; - } - - 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.'); - } + $builder->addModelTransformer(new NumberToStringTransformer($options['force_full_scale'], $options['scale'])); + } - 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 bd7c645766af6..3601d30d03fa2 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 b7601b1a9466e..f2ae341107941 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 @@ protected function tearDown() $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( + '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( - 'value' => $stringValue + 'preserveFullScaleValueSimulation' => $nonFullScalePriceStringValue )); - $this->assertSame($stringValue, $price->value); + $this->assertSame($nonFullScalePriceStringValue, $nonFullScalePrice->preserveFullScaleValueSimulation); $unitOfWork = $this->em->getUnitOfWork(); $unitOfWork->computeChangeSets(); - $this->assertSame(array(), $unitOfWork->getEntityChangeSet($price)); + $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 c333361d4a37f..6b617825c9190 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 803df3c1d9935..0000000000000 --- 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 78a2ba9510b2e..34fb04aed283e 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php @@ -17,7 +17,6 @@ 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 @@ 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(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 6c67aacd4bcbb..0000000000000 --- 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 3601d30d03fa2..0000000000000 --- 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 f2ae341107941..0000000000000 --- 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 0d3d468586f6b..ae21b1de9d590 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 0000000000000..27e60b4306336 --- /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 a0257f0269628..4c1f1fd71f16b 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 @@ 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'); 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 0000000000000..5726a217da3d9 --- /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 1ab6c8e9fbc76..91b15ca7609ea 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 @@ 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'); @@ -35,7 +35,7 @@ public function testDefaultFormatting() $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 @@ public function testDefaultFormattingWithGrouping() $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 @@ public function testDefaultFormattingWithScale() $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, [ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy