Skip to content

Commit

Permalink
Added NumericRangeFilter which gives the possibility of filtering i…
Browse files Browse the repository at this point in the history
…n the range (gt|gte|lt|lte) from to on numeric type fields.
  • Loading branch information
christopherhero committed Feb 15, 2024
1 parent d317ced commit f7bf422
Show file tree
Hide file tree
Showing 4 changed files with 376 additions and 0 deletions.
59 changes: 59 additions & 0 deletions src/Bundle/Form/Type/Filter/NumericRangeFilterType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Sylius\Bundle\GridBundle\Form\Type\Filter;

use Sylius\Component\Grid\Filter\NumericRangeFilter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

final class NumericRangeFilterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->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';
}
}
10 changes: 10 additions & 0 deletions src/Bundle/Resources/config/services/filters.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@
</service>
<service id="sylius.form.type.grid_filter.exists" alias="Sylius\Bundle\GridBundle\Form\Type\Filter\ExistsFilterType" />

<service id="Sylius\Component\Grid\Filter\NumericRangeFilter">
<tag name="sylius.grid_filter" type="numeric_range" form-type="Sylius\Bundle\GridBundle\Form\Type\Filter\NumericRangeFilterType" />
</service>
<service id="sylius.grid_filter.numeric_range" alias="Sylius\Component\Grid\Filter\NumericRangeFilter" />

<service id="Sylius\Bundle\GridBundle\Form\Type\Filter\NumericRangeFilterType">
<tag name="form.type" />
</service>
<service id="sylius.form.type.grid_filter.numeric_range" alias="Sylius\Bundle\GridBundle\Form\Type\Filter\NumericRangeFilterType" />

<service id="Sylius\Component\Grid\Filter\SelectFilter">
<tag name="sylius.grid_filter" type="select" form-type="Sylius\Bundle\GridBundle\Form\Type\Filter\SelectFilterType" />
</service>
Expand Down
78 changes: 78 additions & 0 deletions src/Component/Filter/NumericRangeFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Sylius\Component\Grid\Filter;

use Sylius\Component\Grid\Data\DataSourceInterface;
use Sylius\Component\Grid\Filtering\FilterInterface;

final class NumericRangeFilter implements FilterInterface
{
public const DEFAULT_SCALE = 0;

public const DEFAULT_ROUNDING_MODE = \NumberFormatter::ROUND_HALFUP;

public const DEFAULT_INCLUSIVE_FROM = true;

public const DEFAULT_INCLUSIVE_TO = true;

/** @param array<array-key, string> $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<array-key, string> $data */
private function getDataValue(array $data, string $key): string
{
return $data[$key] ?? '';
}
}
229 changes: 229 additions & 0 deletions src/Component/spec/Filter/NumericRangeFilterSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace spec\Sylius\Component\Grid\Filter;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Component\Grid\Data\DataSourceInterface;
use Sylius\Component\Grid\Data\ExpressionBuilderInterface;
use Sylius\Component\Grid\Filtering\FilterInterface;

final class NumericRangeFilterSpec extends ObjectBehavior
{
function it_implements_a_filter_interface(): void
{
$this->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,
],
);
}
}

0 comments on commit f7bf422

Please sign in to comment.