From f7bf422811525e7af14e0c9d1235c37b560813ee Mon Sep 17 00:00:00 2001 From: christopherhero Date: Mon, 29 Mar 2021 03:38:20 +0200 Subject: [PATCH] Added `NumericRangeFilter` which gives the possibility of filtering in the range (gt|gte|lt|lte) from to on numeric type fields. --- .../Type/Filter/NumericRangeFilterType.php | 59 +++++ .../Resources/config/services/filters.xml | 10 + src/Component/Filter/NumericRangeFilter.php | 78 ++++++ .../spec/Filter/NumericRangeFilterSpec.php | 229 ++++++++++++++++++ 4 files changed, 376 insertions(+) create mode 100644 src/Bundle/Form/Type/Filter/NumericRangeFilterType.php create mode 100644 src/Component/Filter/NumericRangeFilter.php create mode 100644 src/Component/spec/Filter/NumericRangeFilterSpec.php diff --git a/src/Bundle/Form/Type/Filter/NumericRangeFilterType.php b/src/Bundle/Form/Type/Filter/NumericRangeFilterType.php new file mode 100644 index 00000000..0d852d22 --- /dev/null +++ b/src/Bundle/Form/Type/Filter/NumericRangeFilterType.php @@ -0,0 +1,59 @@ +add('greaterThan', NumberType::class, [ + 'label' => 'sylius.ui.greater_than', + 'required' => false, + 'scale' => $options['scale'], + 'rounding_mode' => $options['rounding_mode'], + ]) + ->add('lessThan', NumberType::class, [ + 'label' => 'sylius.ui.less_than', + 'required' => false, + 'scale' => $options['scale'], + 'rounding_mode' => $options['rounding_mode'], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefaults([ + 'data_class' => null, + 'scale' => NumericRangeFilter::DEFAULT_SCALE, + 'rounding_mode' => NumericRangeFilter::DEFAULT_ROUNDING_MODE, + ]) + ->setAllowedTypes('scale', ['string', 'int']) + ->setAllowedTypes('rounding_mode', ['string', 'int']) + ; + } + + public function getBlockPrefix(): string + { + return 'sylius_grid_filter_numeric_range'; + } +} diff --git a/src/Bundle/Resources/config/services/filters.xml b/src/Bundle/Resources/config/services/filters.xml index 69587978..75413e0a 100644 --- a/src/Bundle/Resources/config/services/filters.xml +++ b/src/Bundle/Resources/config/services/filters.xml @@ -67,6 +67,16 @@ + + + + + + + + + + diff --git a/src/Component/Filter/NumericRangeFilter.php b/src/Component/Filter/NumericRangeFilter.php new file mode 100644 index 00000000..3b33b14a --- /dev/null +++ b/src/Component/Filter/NumericRangeFilter.php @@ -0,0 +1,78 @@ + $data */ + public function apply(DataSourceInterface $dataSource, string $name, $data, array $options): void + { + if (empty($data)) { + return; + } + + $field = (string) ($options['field'] ?? $name); + $scale = (int) ($options['scale'] ?? self::DEFAULT_SCALE); + $mode = (int) ($options['rounding_mode'] ?? self::DEFAULT_ROUNDING_MODE); + + $greaterThan = $this->getDataValue($data, 'greaterThan'); + $lessThan = $this->getDataValue($data, 'lessThan'); + + $expressionBuilder = $dataSource->getExpressionBuilder(); + + if ('' !== $greaterThan) { + $inclusive = (bool) ($options['inclusive_from'] ?? self::DEFAULT_INCLUSIVE_FROM); + $amount = $this->normalizeAmount((float) $greaterThan, $scale, $mode); + + if (true === $inclusive) { + $dataSource->restrict($expressionBuilder->greaterThanOrEqual($field, $amount)); + } else { + $dataSource->restrict($expressionBuilder->greaterThan($field, $amount)); + } + } + + if ('' !== $lessThan) { + $inclusive = (bool) ($options['inclusive_to'] ?? self::DEFAULT_INCLUSIVE_TO); + $amount = $this->normalizeAmount((float) $lessThan, $scale, $mode); + + if (true === $inclusive) { + $dataSource->restrict($expressionBuilder->lessThanOrEqual($field, $amount)); + } else { + $dataSource->restrict($expressionBuilder->lessThan($field, $amount)); + } + } + } + + private function normalizeAmount(float $amount, int $scale, int $mode): int + { + return (int) round($amount * (10 ** $scale), $mode); + } + + /** @param array $data */ + private function getDataValue(array $data, string $key): string + { + return $data[$key] ?? ''; + } +} diff --git a/src/Component/spec/Filter/NumericRangeFilterSpec.php b/src/Component/spec/Filter/NumericRangeFilterSpec.php new file mode 100644 index 00000000..c98fd7ac --- /dev/null +++ b/src/Component/spec/Filter/NumericRangeFilterSpec.php @@ -0,0 +1,229 @@ +shouldImplement(FilterInterface::class); + } + + function it_does_nothing_when_there_is_no_data(DataSourceInterface $dataSource): void + { + $dataSource->restrict(Argument::any())->shouldNotBeCalled(); + + $this->apply( + $dataSource, + 'number', + [], + [], + ); + } + + function it_filters_number_from( + DataSourceInterface $dataSource, + ExpressionBuilderInterface $expressionBuilder, + ): void { + $expressionBuilder->greaterThanOrEqual('number', 3)->willReturn('EXPR'); + $dataSource->getExpressionBuilder()->willReturn($expressionBuilder); + + $dataSource->getExpressionBuilder()->shouldBeCalledOnce(); + $dataSource->restrict('EXPR')->shouldBeCalledOnce(); + + $this->apply( + $dataSource, + 'number', + [ + 'greaterThan' => '3', + ], + [], + ); + } + + function it_filters_number_from_without_inclusive_from( + DataSourceInterface $dataSource, + ExpressionBuilderInterface $expressionBuilder, + ): void { + $expressionBuilder->greaterThan('number', 7)->willReturn('EXPR'); + $dataSource->getExpressionBuilder()->willReturn($expressionBuilder); + + $dataSource->getExpressionBuilder()->shouldBeCalledOnce(); + $dataSource->restrict('EXPR')->shouldBeCalledOnce(); + + $this->apply( + $dataSource, + 'number', + [ + 'greaterThan' => '7', + ], + [ + 'inclusive_from' => false, + ], + ); + } + + function it_filters_number_to( + DataSourceInterface $dataSource, + ExpressionBuilderInterface $expressionBuilder, + ): void { + $dataSource->getExpressionBuilder()->willReturn($expressionBuilder); + $expressionBuilder->lessThanOrEqual('number', 8)->willReturn('EXPR'); + + $dataSource->getExpressionBuilder()->shouldBeCalledOnce(); + $dataSource->restrict('EXPR')->shouldBeCalled(); + + $this->apply( + $dataSource, + 'number', + [ + 'lessThan' => '8', + ], + [], + ); + } + + function it_filters_number_to_without_inclusive_to( + DataSourceInterface $dataSource, + ExpressionBuilderInterface $expressionBuilder, + ): void { + $dataSource->getExpressionBuilder()->willReturn($expressionBuilder); + $expressionBuilder->lessThan('number', 9)->willReturn('EXPR'); + + $dataSource->getExpressionBuilder()->shouldBeCalledOnce(); + $dataSource->restrict('EXPR')->shouldBeCalled(); + + $this->apply( + $dataSource, + 'number', + [ + 'lessThan' => '9', + ], + [ + 'inclusive_to' => false, + ], + ); + } + + function it_filters_money_in_specified_range( + DataSourceInterface $dataSource, + ExpressionBuilderInterface $expressionBuilder, + ): void { + $dataSource->getExpressionBuilder()->willReturn($expressionBuilder); + $expressionBuilder->greaterThanOrEqual('number', 12)->willReturn('EXPR2'); + $expressionBuilder->lessThanOrEqual('number', 120)->willReturn('EXPR3'); + + $dataSource->getExpressionBuilder()->shouldBeCalledOnce(); + $dataSource->restrict('EXPR2')->shouldBeCalledOnce(); + $dataSource->restrict('EXPR3')->shouldBeCalledOnce(); + + $this->apply( + $dataSource, + 'number', + [ + 'greaterThan' => '12.00', + 'lessThan' => '120.00', + ], + [], + ); + } + + function its_amount_scale_and_mode_can_be_configured( + DataSourceInterface $dataSource, + ExpressionBuilderInterface $expressionBuilder, + ): void { + $dataSource->getExpressionBuilder()->willReturn($expressionBuilder); + $expressionBuilder->greaterThanOrEqual('number', 121)->willReturn('EXPR'); + $expressionBuilder->lessThanOrEqual('number', 259)->willReturn('EXPR1'); + + $dataSource->getExpressionBuilder()->shouldBeCalledOnce(); + $dataSource->restrict('EXPR')->shouldBeCalledOnce(); + $dataSource->restrict('EXPR1')->shouldBeCalledOnce(); + + $this->apply( + $dataSource, + 'number', + [ + 'greaterThan' => '120.78', + 'lessThan' => '258.51', + ], + [ + 'scale' => 0, + 'rounding_mode' => \NumberFormatter::ROUND_CEILING, + ], + ); + } + + function it_filters_with_all_available_configurations( + DataSourceInterface $dataSource, + ExpressionBuilderInterface $expressionBuilder, + ): void { + $dataSource->getExpressionBuilder()->willReturn($expressionBuilder); + $expressionBuilder->greaterThan('number', 121)->willReturn('EXPR'); + $expressionBuilder->lessThanOrEqual('number', 259)->willReturn('EXPR1'); + + $dataSource->getExpressionBuilder()->shouldBeCalledOnce(); + $dataSource->restrict('EXPR')->shouldBeCalledOnce(); + $dataSource->restrict('EXPR1')->shouldBeCalledOnce(); + + $this->apply( + $dataSource, + 'number', + [ + 'greaterThan' => '120.78', + 'lessThan' => '258.51', + ], + [ + 'scale' => 0, + 'rounding_mode' => \NumberFormatter::ROUND_CEILING, + 'inclusive_to' => true, + 'inclusive_from' => false, + ], + ); + } + + function its_amount_scale_can_be_configured( + DataSourceInterface $dataSource, + ExpressionBuilderInterface $expressionBuilder, + ): void { + $dataSource->getExpressionBuilder()->willReturn($expressionBuilder); + $expressionBuilder->greaterThan('number', 234520)->willReturn('EXPR'); + $expressionBuilder->lessThanOrEqual('number', 122120)->willReturn('EXPR1'); + + $dataSource->getExpressionBuilder()->shouldBeCalledOnce(); + $dataSource->restrict('EXPR')->shouldBeCalledOnce(); + $dataSource->restrict('EXPR1')->shouldBeCalledOnce(); + + $this->apply( + $dataSource, + 'number', + [ + 'greaterThan' => '234.52', + 'lessThan' => '122.12', + ], + [ + 'scale' => 3, + 'rounding_mode' => \NumberFormatter::ROUND_CEILING, + 'inclusive_to' => true, + 'inclusive_from' => false, + ], + ); + } +}