diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md index 18f3910a40..f49dcab00b 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -9,7 +9,7 @@ If you still need it, please set it up on your own! Frontend dependencies are handled with NPM. Bower is not used anymore. -A lot of assets that were previously public are handled with NPM and placed in a private `node_modules/` directory. +A lot of assets that were previously public are handled with NPM and placed in a private `node_modules/` directory. From these dependencies, only the necessary files are exposed publicly through Webpack Encore. Please check the `src/Resources/public/dist` and the documentation to see the used CSS, JavaScript, images and fonts. @@ -173,6 +173,19 @@ When there is no searchable filters, `SearchHandler::search()` returns `null`. P ## Sonata\AdminBundle\Controller\CRUDController When the service `security.csrf.token_manager` is not available, `getCsrfToken()` returns `null`. Previously, it was returning `false`. +## FilterInterface + +The type for argument 4 in `apply()` method has been changed from `array` to `Sonata\AdminBundle\Filter\Model\FilterData`. + +Before: +```php +public function apply(ProxyQueryInterface $query, array $filterData): void; +``` +After: +```php +public function apply(ProxyQueryInterface $query, FilterData $filterData): void; +``` + ## FormMapper labels The form label are now correctly using the label translator strategy for field with `.` (which won't be replaced by `__`). For instance, with the underscore label strategy, the diff --git a/docs/reference/action_list.rst b/docs/reference/action_list.rst index 9a64b51294..84841d95a1 100644 --- a/docs/reference/action_list.rst +++ b/docs/reference/action_list.rst @@ -537,6 +537,7 @@ If you have the **SonataDoctrineORMAdminBundle** installed you can use the ``CallbackFilter`` filter type e.g. for creating a full text filter:: use Sonata\AdminBundle\Datagrid\DatagridMapper; + use Sonata\AdminBundle\Filter\Model\FilterData; final class UserAdmin extends Sonata\UserBundle\Admin\Model\UserAdmin { @@ -549,17 +550,17 @@ If you have the **SonataDoctrineORMAdminBundle** installed you can use the ]); } - public function getFullTextFilter(ProxyQueryInterface $query, string $alias, string $field, array $data): void + public function getFullTextFilter(ProxyQueryInterface $query, string $alias, string $field, FilterData $data): void { - if (!$data['value']) { + if (!$data->hasValue()) { return false; } // Use `andWhere` instead of `where` to prevent overriding existing `where` conditions $query->andWhere($query->expr()->orX( - $query->expr()->like($alias.'.username', $query->expr()->literal('%' . $data['value'] . '%')), - $query->expr()->like($alias.'.firstName', $query->expr()->literal('%' . $data['value'] . '%')), - $query->expr()->like($alias.'.lastName', $query->expr()->literal('%' . $data['value'] . '%')) + $query->expr()->like($alias.'.username', $query->expr()->literal('%' . $data->getValue() . '%')), + $query->expr()->like($alias.'.firstName', $query->expr()->literal('%' . $data->getValue() . '%')), + $query->expr()->like($alias.'.lastName', $query->expr()->literal('%' . $data->getValue() . '%')) )); return true; @@ -571,21 +572,22 @@ The callback function should return a boolean indicating whether it is active. You can also get the filter type which can be helpful to change the operator type of your condition(s):: + use Sonata\AdminBundle\Filter\Model\FilterData; use Sonata\Form\Type\EqualType; final class UserAdmin extends Sonata\UserBundle\Admin\Model\UserAdmin { - public function getFullTextFilter(ProxyQueryInterface $query, string $alias, string $field, array $data): void + public function getFullTextFilter(ProxyQueryInterface $query, string $alias, string $field, FilterData $data): void { - if (!$data['value']) { + if (!$data->hasValue()) { return; } - $operator = $data['type'] == EqualType::TYPE_IS_EQUAL ? '=' : '!='; + $operator = $data->isType(EqualType::TYPE_IS_EQUAL) ? '=' : '!='; $query ->andWhere($alias.'.username '.$operator.' :username') - ->setParameter('username', $data['value']) + ->setParameter('username', $data->getValue()) ; return true; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fe00546185..ed5a5f1995 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -9,3 +9,7 @@ parameters: - # This error is made on purpose for php version < 8 message: '#^Method Sonata\\AdminBundle\\Tests\\Fixtures\\Entity\\FooToStringNull\:\:__toString\(\) should return string but returns null\.$#' path: tests/Fixtures/Entity/FooToStringNull.php + - # It is on purpose because it throws an exception + message: "#^Call to static method Sonata\\\\AdminBundle\\\\Filter\\\\Model\\\\FilterData\\:\\:fromArray\\(\\) on a separate line has no effect\\.$#" + count: 1 + path: tests/Filter/Model/FilterDataTest.php diff --git a/psalm.xml b/psalm.xml index ce14de2ede..c504cfcbe6 100644 --- a/psalm.xml +++ b/psalm.xml @@ -16,5 +16,11 @@ + + + + + + diff --git a/src/Filter/FilterInterface.php b/src/Filter/FilterInterface.php index 8c2a1a8930..5817a6052d 100644 --- a/src/Filter/FilterInterface.php +++ b/src/Filter/FilterInterface.php @@ -14,6 +14,7 @@ namespace Sonata\AdminBundle\Filter; use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; +use Sonata\AdminBundle\Filter\Model\FilterData; /** * @author Thomas Rabaix @@ -24,10 +25,7 @@ interface FilterInterface public const CONDITION_AND = 'AND'; - /** - * @phpstan-param array{type?: int|null, value?: mixed} $filterData - */ - public function apply(ProxyQueryInterface $query, array $filterData): void; + public function apply(ProxyQueryInterface $query, FilterData $filterData): void; /** * @throws \LogicException if the filter is not initialized diff --git a/src/Filter/Model/FilterData.php b/src/Filter/Model/FilterData.php new file mode 100644 index 0000000000..2f882c5360 --- /dev/null +++ b/src/Filter/Model/FilterData.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Filter\Model; + +/** + * @psalm-immutable + */ +final class FilterData +{ + /** + * @var ?int + */ + private $type; + + /** + * @var mixed + */ + private $value; + + /** + * @var bool + */ + private $hasValue; + + private function __construct() + { + $this->hasValue = false; + } + + /** + * @psalm-pure + * + * @phpstan-param array{type?: int|numeric-string|null, value?: mixed} $data + */ + public static function fromArray(array $data): self + { + $filterData = new self(); + + if (isset($data['type'])) { + if (!\is_int($data['type']) && (!\is_string($data['type']) || !is_numeric($data['type']))) { + throw new \InvalidArgumentException(sprintf( + 'The "type" parameter MUST be of type "integer" or "null", %s given.', + \is_object($data['type']) ? 'instance of "'.\get_class($data['type']).'"' : '"'.\gettype($data['type']).'"' + )); + } + + $filterData->type = (int) $data['type']; + } + + if (\array_key_exists('value', $data)) { + $filterData->value = $data['value']; + $filterData->hasValue = true; + } + + return $filterData; + } + + /** + * @return mixed + */ + public function getValue() + { + if (!$this->hasValue) { + throw new \LogicException('The FilterData object does not have a value.'); + } + + return $this->value; + } + + /** + * @param mixed $value + */ + public function changeValue($value): self + { + return self::fromArray([ + 'type' => $this->getType(), + 'value' => $value, + ]); + } + + public function getType(): ?int + { + return $this->type; + } + + public function isType(int $type): bool + { + return $this->type === $type; + } + + public function hasValue(): bool + { + return $this->hasValue; + } +} diff --git a/src/Form/DataTransformer/FilterDataTransformer.php b/src/Form/DataTransformer/FilterDataTransformer.php new file mode 100644 index 0000000000..18d0f0f258 --- /dev/null +++ b/src/Form/DataTransformer/FilterDataTransformer.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Form\DataTransformer; + +use Sonata\AdminBundle\Filter\Model\FilterData; +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +final class FilterDataTransformer implements DataTransformerInterface +{ + public function reverseTransform($value): FilterData + { + if (!\is_array($value)) { + throw new UnexpectedTypeException($value, 'array'); + } + + return FilterData::fromArray($value); + } + + /** + * @param FilterData|null $value + */ + public function transform($value) + { + if (null === $value) { + return null; + } + + $data = [ + 'type' => $value->getType(), + ]; + + if ($value->hasValue()) { + $data['value'] = $value->getValue(); + } + + return $data; + } +} diff --git a/src/Form/Type/Filter/ChoiceType.php b/src/Form/Type/Filter/ChoiceType.php index d14238a036..2351c2d02d 100644 --- a/src/Form/Type/Filter/ChoiceType.php +++ b/src/Form/Type/Filter/ChoiceType.php @@ -16,7 +16,6 @@ use Sonata\AdminBundle\Form\Type\Operator\ContainsOperatorType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType as FormChoiceType; -use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** @@ -29,23 +28,16 @@ public function getBlockPrefix(): string return 'sonata_type_filter_choice'; } - /** - * @param array $options - */ - public function buildForm(FormBuilderInterface $builder, array $options): void + public function getParent(): string { - $builder - ->add('type', $options['operator_type'], $options['operator_options'] + ['required' => false]) - ->add('value', $options['field_type'], $options['field_options'] + ['required' => false]); + return FilterDataType::class; } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'field_type' => FormChoiceType::class, - 'field_options' => [], 'operator_type' => ContainsOperatorType::class, - 'operator_options' => [], ]); } } diff --git a/src/Form/Type/Filter/DateRangeType.php b/src/Form/Type/Filter/DateRangeType.php index e2c6f475a2..066cca45cb 100644 --- a/src/Form/Type/Filter/DateRangeType.php +++ b/src/Form/Type/Filter/DateRangeType.php @@ -17,7 +17,6 @@ use Sonata\Form\Type\DateRangeType as FormDateRangeType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\DateType; -use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** @@ -30,19 +29,15 @@ public function getBlockPrefix(): string return 'sonata_type_filter_date_range'; } - /** - * @param array $options - */ - public function buildForm(FormBuilderInterface $builder, array $options): void + public function getParent(): string { - $builder - ->add('type', DateRangeOperatorType::class, ['required' => false]) - ->add('value', $options['field_type'], $options['field_options'] + ['required' => false]); + return FilterDataType::class; } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ + 'operator_type' => DateRangeOperatorType::class, 'field_type' => FormDateRangeType::class, 'field_options' => [ 'field_options' => [ diff --git a/src/Form/Type/Filter/DateTimeRangeType.php b/src/Form/Type/Filter/DateTimeRangeType.php index fef9c2b86a..19dfcee803 100644 --- a/src/Form/Type/Filter/DateTimeRangeType.php +++ b/src/Form/Type/Filter/DateTimeRangeType.php @@ -17,7 +17,6 @@ use Sonata\Form\Type\DateTimeRangeType as FormDateTimeRangeType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType; -use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** @@ -30,19 +29,15 @@ public function getBlockPrefix(): string return 'sonata_type_filter_datetime_range'; } - /** - * @param array $options - */ - public function buildForm(FormBuilderInterface $builder, array $options): void + public function getParent(): string { - $builder - ->add('type', DateRangeOperatorType::class, ['required' => false]) - ->add('value', $options['field_type'], $options['field_options'] + ['required' => false]); + return FilterDataType::class; } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ + 'operator_type' => DateRangeOperatorType::class, 'field_type' => FormDateTimeRangeType::class, 'field_options' => [ 'field_options' => ['date_format' => DateTimeType::HTML5_FORMAT], diff --git a/src/Form/Type/Filter/DateTimeType.php b/src/Form/Type/Filter/DateTimeType.php index a71152fa08..b90a5d42ff 100644 --- a/src/Form/Type/Filter/DateTimeType.php +++ b/src/Form/Type/Filter/DateTimeType.php @@ -17,7 +17,6 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType as FormDateTimeType; use Symfony\Component\Form\Extension\Core\Type\DateType; -use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** @@ -30,19 +29,15 @@ public function getBlockPrefix(): string return 'sonata_type_filter_datetime'; } - /** - * @param array $options - */ - public function buildForm(FormBuilderInterface $builder, array $options): void + public function getParent(): string { - $builder - ->add('type', DateOperatorType::class, ['required' => false]) - ->add('value', $options['field_type'], $options['field_options'] + ['required' => false]); + return FilterDataType::class; } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ + 'operator_type' => DateOperatorType::class, 'field_type' => FormDateTimeType::class, 'field_options' => ['date_format' => DateType::HTML5_FORMAT], ]); diff --git a/src/Form/Type/Filter/DateType.php b/src/Form/Type/Filter/DateType.php index 8b102b6420..8db939704a 100644 --- a/src/Form/Type/Filter/DateType.php +++ b/src/Form/Type/Filter/DateType.php @@ -16,7 +16,6 @@ use Sonata\AdminBundle\Form\Type\Operator\DateOperatorType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\DateType as FormDateType; -use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** @@ -29,19 +28,15 @@ public function getBlockPrefix(): string return 'sonata_type_filter_date'; } - /** - * @param array $options - */ - public function buildForm(FormBuilderInterface $builder, array $options): void + public function getParent(): string { - $builder - ->add('type', DateOperatorType::class, ['required' => false]) - ->add('value', $options['field_type'], $options['field_options'] + ['required' => false]); + return FilterDataType::class; } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ + 'operator_type' => DateOperatorType::class, 'field_type' => FormDateType::class, 'field_options' => ['format' => FormDateType::HTML5_FORMAT], ]); diff --git a/src/Form/Type/Filter/DefaultType.php b/src/Form/Type/Filter/DefaultType.php index 9b5a9d6853..c7e73f5495 100644 --- a/src/Form/Type/Filter/DefaultType.php +++ b/src/Form/Type/Filter/DefaultType.php @@ -16,7 +16,6 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\TextType; -use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** @@ -29,23 +28,16 @@ public function getBlockPrefix(): string return 'sonata_type_filter_default'; } - /** - * @param array $options - */ - public function buildForm(FormBuilderInterface $builder, array $options): void + public function getParent(): string { - $builder - ->add('type', $options['operator_type'], $options['operator_options'] + ['required' => false]) - ->add('value', $options['field_type'], $options['field_options'] + ['required' => false]); + return FilterDataType::class; } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'operator_type' => HiddenType::class, - 'operator_options' => [], 'field_type' => TextType::class, - 'field_options' => [], ]); } } diff --git a/src/Form/Type/Filter/FilterDataType.php b/src/Form/Type/Filter/FilterDataType.php new file mode 100644 index 0000000000..35c4e8c13d --- /dev/null +++ b/src/Form/Type/Filter/FilterDataType.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Form\Type\Filter; + +use Sonata\AdminBundle\Form\DataTransformer\FilterDataTransformer; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +final class FilterDataType extends AbstractType +{ + /** + * @param array $options + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('type', $options['operator_type'], $options['operator_options'] + ['required' => false]) + ->add('value', $options['field_type'], $options['field_options'] + ['required' => false]); + + $builder + ->addModelTransformer(new FilterDataTransformer()); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'operator_options' => [], + 'field_options' => [], + ]); + $resolver + ->setRequired(['operator_type', 'field_type']) + ->setAllowedTypes('operator_type', 'string') + ->setAllowedTypes('field_type', 'string') + ->setAllowedTypes('operator_options', 'array') + ->setAllowedTypes('field_options', 'array'); + } +} diff --git a/src/Form/Type/Filter/NumberType.php b/src/Form/Type/Filter/NumberType.php index 14439069a7..4b38f1f98f 100644 --- a/src/Form/Type/Filter/NumberType.php +++ b/src/Form/Type/Filter/NumberType.php @@ -16,7 +16,6 @@ use Sonata\AdminBundle\Form\Type\Operator\NumberOperatorType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\NumberType as FormNumberType; -use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** @@ -29,19 +28,15 @@ public function getBlockPrefix(): string return 'sonata_type_filter_number'; } - /** - * @param array $options - */ - public function buildForm(FormBuilderInterface $builder, array $options): void + public function getParent(): string { - $builder - ->add('type', NumberOperatorType::class, ['required' => false]) - ->add('value', $options['field_type'], $options['field_options'] + ['required' => false]); + return FilterDataType::class; } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ + 'operator_type' => NumberOperatorType::class, 'field_type' => FormNumberType::class, 'field_options' => [], ]); diff --git a/tests/Datagrid/DatagridTest.php b/tests/Datagrid/DatagridTest.php index 1f1b36f6d6..3d53814bf1 100644 --- a/tests/Datagrid/DatagridTest.php +++ b/tests/Datagrid/DatagridTest.php @@ -374,13 +374,13 @@ public function testApplyFilter(?string $type, ?string $value, int $applyCallNum */ public function applyFilterDataProvider(): iterable { - yield ['fakeType', 'fakeValue', 1]; - yield ['', 'fakeValue', 1]; + yield ['3', 'fakeValue', 1]; yield [null, 'fakeValue', 1]; - yield ['fakeType', '', 1]; - yield ['fakeType', null, 1]; - yield ['', '', 0]; - yield ['', null, 0]; + yield [null, 'fakeValue', 1]; + yield ['3', '', 1]; + yield ['3', null, 1]; + yield [null, '', 0]; + yield [null, null, 0]; yield [null, '', 0]; yield [null, null, 0]; } diff --git a/tests/Filter/Model/FilterDataTest.php b/tests/Filter/Model/FilterDataTest.php new file mode 100644 index 0000000000..3ec6e90518 --- /dev/null +++ b/tests/Filter/Model/FilterDataTest.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Tests\Filter\Model; + +use PHPUnit\Framework\TestCase; +use Sonata\AdminBundle\Filter\Model\FilterData; + +final class FilterDataTest extends TestCase +{ + /** + * @dataProvider getInvalidTypes + * + * @param mixed $type + */ + public function testTypeMustBeNumericOrNull($type): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf( + 'The "type" parameter MUST be of type "integer" or "null", %s given.', + \is_object($type) ? 'instance of "'.\get_class($type).'"' : '"'.\gettype($type).'"' + )); + + FilterData::fromArray(['type' => $type]); + } + + /** + * @return iterable> + */ + public function getInvalidTypes(): iterable + { + yield ['string']; + yield [new \stdClass()]; + yield [[]]; + yield [42.0]; + } + + public function testEmptyArray(): void + { + $filterData = FilterData::fromArray([]); + $this->assertFalse($filterData->hasValue()); + $this->assertNull($filterData->getType()); + } + + public function testHasValue(): void + { + $this->assertFalse(FilterData::fromArray([])->hasValue()); + $this->assertTrue(FilterData::fromArray(['value' => ''])->hasValue()); + $this->assertTrue(FilterData::fromArray(['value' => null])->hasValue()); + } + + /** + * @dataProvider getTypes + * + * @param int|string|null $type + */ + public function testGetType(?int $expected, $type): void + { + $this->assertSame($expected, FilterData::fromArray(['type' => $type])->getType()); + } + + /** + * @return iterable> + */ + public function getTypes(): iterable + { + yield 'nullable' => [null, null]; + yield 'int' => [3, 3]; + yield 'numeric string' => [3, '3']; + } + + /** + * @dataProvider getValues + * + * @param mixed $value + */ + public function testGetValue($value): void + { + $this->assertSame($value, FilterData::fromArray(['value' => $value])->getValue()); + } + + /** + * @return iterable> + */ + public function getValues(): iterable + { + yield [null]; + yield [new \stdClass()]; + yield [3]; + yield ['3']; + } + + public function testSetValue(): void + { + $filterData = FilterData::fromArray(['type' => 1, 'value' => 'value']); + $newFilterData = $filterData->changeValue('new_value'); + + $this->assertSame(1, $newFilterData->getType()); + $this->assertSame('new_value', $newFilterData->getValue()); + } + + public function testGetValueThrowsExceptionIfValueNotPresent(): void + { + $filterData = FilterData::fromArray([]); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The FilterData object does not have a value.'); + + $filterData->getValue(); + } +} diff --git a/tests/Fixtures/Filter/BarFilter.php b/tests/Fixtures/Filter/BarFilter.php index 21c811b77d..e8a5da98ee 100644 --- a/tests/Fixtures/Filter/BarFilter.php +++ b/tests/Fixtures/Filter/BarFilter.php @@ -15,11 +15,12 @@ use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; use Sonata\AdminBundle\Filter\Filter; +use Sonata\AdminBundle\Filter\Model\FilterData; use Sonata\AdminBundle\Form\Type\Filter\DefaultType; final class BarFilter extends Filter { - public function apply(ProxyQueryInterface $query, array $filterData): void + public function apply(ProxyQueryInterface $query, FilterData $filterData): void { } diff --git a/tests/Fixtures/Filter/FooFilter.php b/tests/Fixtures/Filter/FooFilter.php index f8d3d95aa6..89a3d84963 100644 --- a/tests/Fixtures/Filter/FooFilter.php +++ b/tests/Fixtures/Filter/FooFilter.php @@ -15,10 +15,11 @@ use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; use Sonata\AdminBundle\Filter\Filter; +use Sonata\AdminBundle\Filter\Model\FilterData; class FooFilter extends Filter { - public function apply(ProxyQueryInterface $query, array $filterData): void + public function apply(ProxyQueryInterface $query, FilterData $filterData): void { } diff --git a/tests/Form/DataTransformer/FilterDataTransformerTest.php b/tests/Form/DataTransformer/FilterDataTransformerTest.php new file mode 100644 index 0000000000..f5036730e9 --- /dev/null +++ b/tests/Form/DataTransformer/FilterDataTransformerTest.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\AdminBundle\Tests\Form\DataTransformer; + +use PHPUnit\Framework\TestCase; +use Sonata\AdminBundle\Filter\Model\FilterData; +use Sonata\AdminBundle\Form\DataTransformer\FilterDataTransformer; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +final class FilterDataTransformerTest extends TestCase +{ + public function testReverseTransformThrowsExceptionIfValueIsNotArray(): void + { + $transformer = new FilterDataTransformer(); + + $this->expectException(UnexpectedTypeException::class); + + $transformer->reverseTransform(1); + } + + /** + * @dataProvider getDataValues + * @phpstan-param array{type: int, value: mixed} $value + */ + public function testReverseTransform(array $value): void + { + $transformer = new FilterDataTransformer(); + + $filterData = $transformer->reverseTransform($value); + + $this->assertSame($value['type'], $filterData->getType()); + $this->assertSame($value['value'], $filterData->getValue()); + } + + public function testTransformReturnsNullOnNull(): void + { + $transformer = new FilterDataTransformer(); + + $this->assertNull($transformer->transform(null)); + } + + /** + * @dataProvider getDataValues + * @phpstan-param array{type: int, value: mixed} $value + */ + public function testTransform(array $value): void + { + $transformer = new FilterDataTransformer(); + + $this->assertSame($value, $transformer->transform(FilterData::fromArray($value))); + } + + /** + * @phpstan-return iterable> + */ + public function getDataValues(): iterable + { + yield [[ + 'type' => 1, + 'value' => 'value', + ]]; + + yield [[ + 'type' => 1, + 'value' => null, + ]]; + } +} diff --git a/tests/Form/Type/Filter/DateTimeRangeTypeTest.php b/tests/Form/Type/Filter/DateTimeRangeTypeTest.php index ef9b45650e..f62df5a59a 100644 --- a/tests/Form/Type/Filter/DateTimeRangeTypeTest.php +++ b/tests/Form/Type/Filter/DateTimeRangeTypeTest.php @@ -14,6 +14,7 @@ namespace Sonata\AdminBundle\Tests\Form\Type\Filter; use Sonata\AdminBundle\Form\Type\Filter\DateTimeRangeType; +use Sonata\AdminBundle\Form\Type\Operator\DateRangeOperatorType; use Sonata\Form\Type\DateTimeRangeType as FormDateTimeRangeType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -41,6 +42,7 @@ public function testGetDefaultOptions(): void $options = $optionsResolver->resolve(); $expected = [ + 'operator_type' => DateRangeOperatorType::class, 'field_type' => FormDateTimeRangeType::class, 'field_options' => ['field_options' => ['date_format' => DateTimeType::HTML5_FORMAT]], ];