Skip to content

Commit

Permalink
Remove dependency to symfony form + add localized number transformer
Browse files Browse the repository at this point in the history
  • Loading branch information
vincent4vx committed Mar 2, 2021
1 parent 0d4dfeb commit 99ef146
Show file tree
Hide file tree
Showing 9 changed files with 672 additions and 26 deletions.
4 changes: 2 additions & 2 deletions composer.json
Expand Up @@ -22,7 +22,6 @@
"require": {
"php": ">=7.1",
"psr/container": "~1.0",
"symfony/form": "~4.3|~5.0",
"symfony/property-access": "~4.3|~5.0",
"symfony/validator": "~4.3|~5.0",
"symfony/polyfill-php80": "~1.22"
Expand All @@ -32,7 +31,8 @@
"giggsey/libphonenumber-for-php": "~8.0",
"phpunit/phpunit": "~7.0|~8.0",
"vimeo/psalm": "~4.0@stable",
"symfony/http-foundation": "~4.3|~5.0"
"symfony/http-foundation": "~4.3|~5.0",
"symfony/form": "~4.3|~5.0"
},
"suggest": {
"symfony/security-csrf": "For enable CSRF element",
Expand Down
17 changes: 6 additions & 11 deletions src/Leaf/FloatElementBuilder.php
Expand Up @@ -4,10 +4,10 @@

use Bdf\Form\Aggregate\FormBuilderInterface;
use Bdf\Form\ElementInterface;
use Bdf\Form\Transformer\DataTransformerAdapter;
use Bdf\Form\Leaf\Transformer\LocalizedNumberTransformer;
use Bdf\Form\Transformer\TransformerInterface;
use Bdf\Form\Validator\ValueValidatorInterface;
use Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer;
use NumberFormatter;

/**
* Builder for a float element
Expand Down Expand Up @@ -38,9 +38,9 @@ class FloatElementBuilder extends NumberElementBuilder
private $grouping = false;

/**
* @var int
* @var NumberFormatter::ROUND_*
*/
private $roundingMode = NumberToLocalizedStringTransformer::ROUND_DOWN;
private $roundingMode = NumberFormatter::ROUND_DOWN;


/**
Expand All @@ -64,7 +64,7 @@ public function grouping(bool $flag = true): self
*
* Note: The element must not be in raw() mode to works
*
* @param int $mode One of the NumberToLocalizedStringTransformer::ROUND_ constant
* @param NumberFormatter::ROUND_* $mode One of the NumberFormatter::ROUND_ constant
*
* @return $this
*/
Expand Down Expand Up @@ -104,11 +104,6 @@ protected function createElement(ValueValidatorInterface $validator, Transformer
*/
protected function numberTransformer(): TransformerInterface
{
return new DataTransformerAdapter(new class($this->scale, $this->grouping, $this->roundingMode) extends NumberToLocalizedStringTransformer {
public function reverseTransform($value)
{
return parent::reverseTransform(is_scalar($value) || $value === null ? (string) $value : $value);
}
});
return new LocalizedNumberTransformer($this->scale, $this->grouping, $this->roundingMode);
}
}
18 changes: 6 additions & 12 deletions src/Leaf/IntegerElementBuilder.php
Expand Up @@ -4,10 +4,10 @@

use Bdf\Form\Aggregate\FormBuilderInterface;
use Bdf\Form\ElementInterface;
use Bdf\Form\Transformer\DataTransformerAdapter;
use Bdf\Form\Leaf\Transformer\LocalizedIntegerTransformer;
use Bdf\Form\Transformer\TransformerInterface;
use Bdf\Form\Validator\ValueValidatorInterface;
use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer;
use NumberFormatter;

/**
* Builder for an integer element
Expand All @@ -33,9 +33,9 @@ class IntegerElementBuilder extends NumberElementBuilder
private $grouping = false;

/**
* @var int
* @var NumberFormatter::ROUND_*
*/
private $roundingMode = IntegerToLocalizedStringTransformer::ROUND_DOWN;
private $roundingMode = NumberFormatter::ROUND_DOWN;


/**
Expand All @@ -59,7 +59,7 @@ public function grouping(bool $flag = true): self
*
* Note: The element must not in raw() mode to works
*
* @param int $mode One of the IntegerToLocalizedStringTransformer::ROUND_ constant
* @param NumberFormatter::ROUND_* $mode One of the IntegerToLocalizedStringTransformer::ROUND_ constant
*
* @return $this
*/
Expand All @@ -83,12 +83,6 @@ protected function createElement(ValueValidatorInterface $validator, Transformer
*/
protected function numberTransformer(): TransformerInterface
{
// Handle null and scalar types
return new DataTransformerAdapter(new class($this->grouping, $this->roundingMode) extends IntegerToLocalizedStringTransformer {
public function reverseTransform($value)
{
return parent::reverseTransform(is_scalar($value) || $value === null ? (string) $value : $value);
}
});
return new LocalizedIntegerTransformer($this->grouping, $this->roundingMode);
}
}
33 changes: 33 additions & 0 deletions src/Leaf/Transformer/LocalizedIntegerTransformer.php
@@ -0,0 +1,33 @@
<?php

namespace Bdf\Form\Leaf\Transformer;

use NumberFormatter;

/**
* Localized number transformer for integer value
*
* @extends LocalizedNumberTransformer<int>
*/
class LocalizedIntegerTransformer extends LocalizedNumberTransformer
{
/**
* LocalizedIntegerTransformer constructor.
*
* @param bool $grouping Group by thousand or not
* @param NumberFormatter::ROUND_* $roundingMode
* @param string|null $locale The locale to use. null for use the current locale
*/
public function __construct(bool $grouping = false, int $roundingMode = NumberFormatter::ROUND_HALFUP, ?string $locale = null)
{
parent::__construct(0, $grouping, $roundingMode, $locale);
}

/**
* {@inheritdoc}
*/
protected function cast($value): int
{
return $value;
}
}
210 changes: 210 additions & 0 deletions src/Leaf/Transformer/LocalizedNumberTransformer.php
@@ -0,0 +1,210 @@
<?php

namespace Bdf\Form\Leaf\Transformer;

use Bdf\Form\ElementInterface;
use Bdf\Form\Transformer\TransformerInterface;
use InvalidArgumentException;
use Locale;
use NumberFormatter;

/**
* Transformer localized string number to native PHP number (int or double)
*
* Inspired from : https://github.com/symfony/symfony/blob/5.x/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php
*
* @template T as numeric
*/
class LocalizedNumberTransformer implements TransformerInterface
{
/**
* Number of digit to keep after the comma
*
* @var int|null
*/
private $scale;

/**
* @var int
* @psalm-var NumberFormatter::ROUND_*
*/
private $roundingMode;

/**
* Group by thousand or not
*
* @var bool
*/
private $grouping;

/**
* The locale to use
* null for use the current locale
*
* @var string|null
*/
private $locale;

/**
* LocalizedNumberTransformer constructor.
*
* @param int|null $scale Number of digit to keep after the comma. Null to keep all digits (do not round)
* @param bool $grouping Group by thousand or not
* @param NumberFormatter::ROUND_* $roundingMode
* @param string|null $locale The locale to use. null for use the current locale
*/
public function __construct(?int $scale = null, bool $grouping = false, int $roundingMode = NumberFormatter::ROUND_HALFUP, ?string $locale = null)
{
$this->scale = $scale;
$this->grouping = $grouping;
$this->roundingMode = $roundingMode;
$this->locale = $locale;
}

/**
* {@inheritdoc}
*
* @throws InvalidArgumentException If the given value is not numeric or cannot be formatted
*/
final public function transformToHttp($value, ElementInterface $input): ?string
{
if ($value === null) {
return null;
}

if (!is_numeric($value)) {
throw new InvalidArgumentException('Expected a numeric or null.');
}

$formatter = $this->getNumberFormatter();
$value = $formatter->format($value);

$this->checkError($formatter);

return $value;
}

/**
* {@inheritdoc}
*
* @return T|null The numeric value
*
* @throws InvalidArgumentException If the given value is not scalar or cannot be parsed
*/
final public function transformFromHttp($value, ElementInterface $input)
{
if ($value !== null && !is_scalar($value)) {
throw new InvalidArgumentException('Expected a scalar or null.');
}

if ($value === null || $value === '') {
return null;
}

$value = (string) $value;

$formatter = $this->getNumberFormatter();
$decSep = $formatter->getSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);

// Normalize "standard" decimal format to locale format
if ($decSep !== '.') {
$value = str_replace('.', $decSep, $value);
}

if (str_contains($value, $decSep)) {
$type = NumberFormatter::TYPE_DOUBLE;
} else {
$type = PHP_INT_SIZE === 8 ? NumberFormatter::TYPE_INT64 : NumberFormatter::TYPE_INT32;
}

$result = $formatter->parse($value, $type);
$this->checkError($formatter);

return $this->cast($this->round($result));
}

/**
* Create the NumberFormatter instance
*
* @return NumberFormatter
*/
private function getNumberFormatter(): NumberFormatter
{
$formatter = new NumberFormatter($this->locale ?? Locale::getDefault(), NumberFormatter::DECIMAL);

if (null !== $this->scale) {
$formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $this->scale);
$formatter->setAttribute(NumberFormatter::ROUNDING_MODE, $this->roundingMode);
}

$formatter->setAttribute(NumberFormatter::GROUPING_USED, $this->grouping);

return $formatter;
}

/**
* Cast the number to the desired type
*
* @return T
*/
protected function cast($value)
{
return $value;
}

/**
* Rounds a number according to the configured scale and rounding mode
*
* @param int|float $number A number
*
* @return int|float The rounded number
*/
private function round($number)
{
if (is_int($number) || $this->scale === null) {
return $number;
}

switch ($this->roundingMode) {
case NumberFormatter::ROUND_HALFEVEN:
return round($number, $this->scale, PHP_ROUND_HALF_EVEN);
case NumberFormatter::ROUND_HALFUP:
return round($number, $this->scale, PHP_ROUND_HALF_UP);
case NumberFormatter::ROUND_HALFDOWN:
return round($number, $this->scale, PHP_ROUND_HALF_DOWN);
}

$coef = 10 ** $this->scale;
$number *= $coef;

switch ($this->roundingMode) {
case NumberFormatter::ROUND_CEILING:
$number = ceil($number);
break;
case NumberFormatter::ROUND_FLOOR:
$number = floor($number);
break;
case NumberFormatter::ROUND_UP:
$number = $number > 0 ? ceil($number) : floor($number);
break;
case NumberFormatter::ROUND_DOWN:
$number = $number > 0 ? floor($number) : ceil($number);
break;
}

return $number / $coef;
}

/**
* Check if the formatter is in error state, and throw exception
*
* @param NumberFormatter $formatter
* @throws InvalidArgumentException If the formatter has an error
*/
private function checkError(NumberFormatter $formatter): void
{
if (intl_is_failure($formatter->getErrorCode())) {
throw new InvalidArgumentException($formatter->getErrorMessage());
}
}
}
10 changes: 10 additions & 0 deletions tests/Leaf/FloatElementBuilderTest.php
Expand Up @@ -3,6 +3,7 @@
namespace Bdf\Form\Leaf;

use Bdf\Form\Choice\ArrayChoice;
use Locale;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer;
use Symfony\Component\Validator\Constraints\NotEqualTo;
Expand All @@ -18,9 +19,18 @@ class FloatElementBuilderTest extends TestCase
*/
private $builder;

private $lastLocale;

protected function setUp(): void
{
$this->builder = new FloatElementBuilder();
$this->lastLocale = Locale::getDefault();
Locale::setDefault('fr');
}

protected function tearDown(): void
{
Locale::setDefault($this->lastLocale);
}

/**
Expand Down

0 comments on commit 99ef146

Please sign in to comment.