From 7f9689b187b210be10ad56e45fab7df808db26ee Mon Sep 17 00:00:00 2001 From: Florian Merle Date: Fri, 22 Mar 2024 14:28:15 +0100 Subject: [PATCH 1/8] add callback field --- docs/field_types.md | 86 +++++++++++++++++++ src/Bundle/Builder/Field/CallbackField.php | 25 ++++++ .../Resources/config/services/field_types.xml | 6 ++ .../FieldTypes/CallbackFieldType.php | 46 ++++++++++ .../spec/FieldTypes/CallbackFieldTypeSpec.php | 85 ++++++++++++++++++ 5 files changed, 248 insertions(+) create mode 100644 src/Bundle/Builder/Field/CallbackField.php create mode 100644 src/Component/FieldTypes/CallbackFieldType.php create mode 100644 src/Component/spec/FieldTypes/CallbackFieldTypeSpec.php diff --git a/docs/field_types.md b/docs/field_types.md index f12d664d..dd69877a 100644 --- a/docs/field_types.md +++ b/docs/field_types.md @@ -319,3 +319,89 @@ $field->setOptions([ // Your options here ]); ``` + +Callback +-------- + +The Callback column aims to offer almost as much flexibility as the Twig column, but without requiring the creation of a template. +You simply need to specify a callback, which allows you to transform the 'data' variable on the fly. + +By default it uses the name of the field, but you can specify the path +alternatively. For example: + +
PHP + +```php +addGrid(GridBuilder::create('app_user', '%app.model.user.class%') + ->addField( + CallbackField::create('roles' fn (array $roles): string => implode(', ', $roles)) + ->setLabel('app.ui.roles') // # each filed type can have a label, we suggest using translation keys instead of messages + ->setPath('roles') + ) + ->addField( + CallbackField::create('status' fn (array $status): string => "$status", false) // the third argument allows to disable htmlspecialchars if set to false + ->setLabel('app.ui.status') // # each filed type can have a label, we suggest using translation keys instead of messages + ->setPath('status') + ) + ) +}; +``` + +OR + +```php +addField( + CallbackField::create('roles' fn (array $roles): string => implode(', ', $roles)) + ->setLabel('app.ui.roles') // # each filed type can have a label, we suggest using translation keys instead of messages + ->setPath('roles') + ) + ->addField( + CallbackField::create('status' fn (array $status): string => "$status", false) // the third argument allows to disable htmlspecialchars if set to false + ->setLabel('app.ui.status') // # each filed type can have a label, we suggest using translation keys instead of messages + ->setPath('status') + ) + ; + } + + public function getResourceClass(): string + { + return User::class; + } +} +``` + +
+ +This configuration will display each role of a customer separated with a comma. + diff --git a/src/Bundle/Builder/Field/CallbackField.php b/src/Bundle/Builder/Field/CallbackField.php new file mode 100644 index 00000000..b47c2a6d --- /dev/null +++ b/src/Bundle/Builder/Field/CallbackField.php @@ -0,0 +1,25 @@ +setOption('callback', $callback) + ->setOption('htmlspecialchars', $htmlspecialchars) + ; + } +} diff --git a/src/Bundle/Resources/config/services/field_types.xml b/src/Bundle/Resources/config/services/field_types.xml index dc07f242..5cf2daae 100644 --- a/src/Bundle/Resources/config/services/field_types.xml +++ b/src/Bundle/Resources/config/services/field_types.xml @@ -15,6 +15,12 @@ + + + + + + %sylius_grid.timezone% diff --git a/src/Component/FieldTypes/CallbackFieldType.php b/src/Component/FieldTypes/CallbackFieldType.php new file mode 100644 index 00000000..ce4833ea --- /dev/null +++ b/src/Component/FieldTypes/CallbackFieldType.php @@ -0,0 +1,46 @@ +dataExtractor->get($field, $data); + $value = (string) call_user_func($options['callback'], $value); + + if ($options['htmlspecialchars']) { + $value = htmlspecialchars($value); + } + + return $value; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired('callback'); + $resolver->setAllowedTypes('callback', 'callable'); + + $resolver->setDefault('htmlspecialchars', true); + $resolver->setAllowedTypes('htmlspecialchars', 'bool'); + } +} diff --git a/src/Component/spec/FieldTypes/CallbackFieldTypeSpec.php b/src/Component/spec/FieldTypes/CallbackFieldTypeSpec.php new file mode 100644 index 00000000..ed7e60e7 --- /dev/null +++ b/src/Component/spec/FieldTypes/CallbackFieldTypeSpec.php @@ -0,0 +1,85 @@ +beConstructedWith($dataExtractor); + } + + function it_is_a_grid_field_type(): void + { + $this->shouldImplement(FieldTypeInterface::class); + } + + function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_callback_with_htmlspecialchars( + DataExtractorInterface $dataExtractor, + Field $field, + ): void { + $dataExtractor->get($field, ['foo' => 'bar'])->willReturn('bar'); + + $this->render($field, ['foo' => 'bar'], [ + 'callback' => fn (string $value): string => "$value", + 'htmlspecialchars' => true, + ])->shouldReturn('<strong>bar</strong>'); + } + + function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_callback_without_htmlspecialchars( + DataExtractorInterface $dataExtractor, + Field $field, + ): void { + $dataExtractor->get($field, ['foo' => 'bar'])->willReturn('bar'); + + $this->render($field, ['foo' => 'bar'], [ + 'callback' => fn (string $value): string => "$value", + 'htmlspecialchars' => false, + ])->shouldReturn('bar'); + } + + function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_function_callback( + DataExtractorInterface $dataExtractor, + Field $field, + ): void { + $dataExtractor->get($field, ['foo' => 'bar'])->willReturn('bar'); + + $this->render($field, ['foo' => 'bar'], [ + 'callback' => 'strtoupper', + 'htmlspecialchars' => true, + ])->shouldReturn('BAR'); + } + + function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_static_callback( + DataExtractorInterface $dataExtractor, + Field $field, + ): void { + $dataExtractor->get($field, ['foo' => 'bar'])->willReturn('BAR'); + + $this->render($field, ['foo' => 'bar'], [ + 'callback' => [self::class, 'callable'], + 'htmlspecialchars' => true, + ])->shouldReturn('bar'); + } + + static function callable(mixed $value): string + { + return strtolower($value); + } +} From f8f212fe3771159fa2281b6f7453e9ca3e062897 Mon Sep 17 00:00:00 2001 From: Florian Merle Date: Tue, 21 Jan 2025 15:29:35 +0100 Subject: [PATCH 2/8] make callback field compatible with yaml config --- docs/field_types.md | 63 ++++++++++++----- src/Bundle/Parser/OptionsParser.php | 63 +++++++++++++++++ src/Bundle/Parser/OptionsParserInterface.php | 19 +++++ src/Bundle/Renderer/TwigGridRenderer.php | 8 ++- src/Bundle/Resources/config/services.xml | 3 + src/Bundle/Resources/config/services/twig.xml | 1 + src/Bundle/Tests/Functional/GridUiTest.php | 36 ++++++++-- src/Bundle/spec/Parser/OptionsParserSpec.php | 70 +++++++++++++++++++ .../spec/Renderer/TwigGridRendererSpec.php | 5 ++ tests/Application/config/sylius/grids.yaml | 9 ++- .../config/sylius/grids/author.php | 6 +- .../Application/config/sylius/grids/book.php | 3 +- tests/Application/src/Grid/AuthorGrid.php | 7 +- tests/Application/src/Grid/BookGrid.php | 3 +- tests/Application/src/Helper/GridHelper.php | 22 ++++++ 15 files changed, 283 insertions(+), 35 deletions(-) create mode 100644 src/Bundle/Parser/OptionsParser.php create mode 100644 src/Bundle/Parser/OptionsParserInterface.php create mode 100644 src/Bundle/spec/Parser/OptionsParserSpec.php create mode 100644 tests/Application/src/Helper/GridHelper.php diff --git a/docs/field_types.md b/docs/field_types.md index dd69877a..673179f5 100644 --- a/docs/field_types.md +++ b/docs/field_types.md @@ -326,8 +326,34 @@ Callback The Callback column aims to offer almost as much flexibility as the Twig column, but without requiring the creation of a template. You simply need to specify a callback, which allows you to transform the 'data' variable on the fly. -By default it uses the name of the field, but you can specify the path -alternatively. For example: +When defining callbacks in YAML, only string representations of callables are supported. +When configuring grids using PHP (as opposed to service grid configuration), both string and array callables are supported. However, closures cannot be used due to restrictions in Symfony's configuration (values of type "Closure" are not permitted in service configuration files). +By contrast, when configuring grids with service definitions, you can use both callables and closures. + +Here are some examples of what you can do: + +
Yaml + +```yaml +# config/packages/sylius_grid.yaml + +sylius_grid: + grids: + app_user: + fields: + id: + type: callback + options: + callback: "callback:App\\Helper\\GridHelper::addHashPrefix" + label: app.ui.id + name: + type: callback + options: + callback: "callback:strtoupper" + label: app.ui.name +``` + +
PHP @@ -342,14 +368,18 @@ use Sylius\Bundle\GridBundle\Config\GridConfig; return static function (GridConfig $grid): void { $grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%') ->addField( - CallbackField::create('roles' fn (array $roles): string => implode(', ', $roles)) - ->setLabel('app.ui.roles') // # each filed type can have a label, we suggest using translation keys instead of messages - ->setPath('roles') + CallbackField::create('id', 'App\\Helper\\GridHelper::addHashPrefix') + ->setLabel('app.ui.id') ) + // or ->addField( - CallbackField::create('status' fn (array $status): string => "$status", false) // the third argument allows to disable htmlspecialchars if set to false - ->setLabel('app.ui.status') // # each filed type can have a label, we suggest using translation keys instead of messages - ->setPath('status') + CallbackField::create('id', ['App\\Helper\\GridHelper', 'addHashPrefix']) + ->setLabel('app.ui.id') + ) + + ->addField( + CallbackField::create('name', 'strtoupper') + ->setLabel('app.ui.name') ) ) }; @@ -382,14 +412,16 @@ final class UserGrid extends AbstractGrid implements ResourceAwareGridInterface { $gridBuilder ->addField( - CallbackField::create('roles' fn (array $roles): string => implode(', ', $roles)) - ->setLabel('app.ui.roles') // # each filed type can have a label, we suggest using translation keys instead of messages - ->setPath('roles') + CallbackField::create('id', GridHelper::addHashPrefix(...)) + ->setLabel('app.ui.id') ) ->addField( - CallbackField::create('status' fn (array $status): string => "$status", false) // the third argument allows to disable htmlspecialchars if set to false - ->setLabel('app.ui.status') // # each filed type can have a label, we suggest using translation keys instead of messages - ->setPath('status') + CallbackField::create('name', 'strtoupper') + ->setLabel('app.ui.name') + ) + ->addField( + CallbackField::create('roles' fn (array $roles): string => implode(', ', $roles)) + ->setLabel('app.ui.roles') ) ; } @@ -402,6 +434,3 @@ final class UserGrid extends AbstractGrid implements ResourceAwareGridInterface ```
- -This configuration will display each role of a customer separated with a comma. - diff --git a/src/Bundle/Parser/OptionsParser.php b/src/Bundle/Parser/OptionsParser.php new file mode 100644 index 00000000..4e835e1d --- /dev/null +++ b/src/Bundle/Parser/OptionsParser.php @@ -0,0 +1,63 @@ +parseOptions($parameter); + } + + return $this->parseOption($parameter); + }, + $parameters, + ); + } + + /** + * @param mixed $parameter + * + * @return mixed + */ + private function parseOption($parameter) + { + if (!is_string($parameter)) { + return $parameter; + } + + if (0 === strpos($parameter, 'callback:')) { + return $this->parseOptionCallback(substr($parameter, 9)); + } + + return $parameter; + } + + private function parseOptionCallback(string $callback): \Closure + { + if (!is_callable($callback)) { + throw new \RuntimeException(\sprintf('%s is not a callable.', $callback)); + } + + return $callback(...); + } +} diff --git a/src/Bundle/Parser/OptionsParserInterface.php b/src/Bundle/Parser/OptionsParserInterface.php new file mode 100644 index 00000000..2e988190 --- /dev/null +++ b/src/Bundle/Parser/OptionsParserInterface.php @@ -0,0 +1,19 @@ +fieldsRegistry = $fieldsRegistry; $this->formFactory = $formFactory; $this->formTypeRegistry = $formTypeRegistry; + $this->optionsParser = $optionsParser; $this->defaultTemplate = $defaultTemplate; $this->actionTemplates = $actionTemplates; $this->filterTemplates = $filterTemplates; @@ -71,7 +76,8 @@ public function renderField(GridViewInterface $gridView, Field $field, $data) $fieldType = $this->fieldsRegistry->get($field->getType()); $resolver = new OptionsResolver(); $fieldType->configureOptions($resolver); - $options = $resolver->resolve($field->getOptions()); + + $options = $resolver->resolve($this->optionsParser->parseOptions($field->getOptions())); return $fieldType->render($field, $data, $options); } diff --git a/src/Bundle/Resources/config/services.xml b/src/Bundle/Resources/config/services.xml index 348f777b..b88489f7 100644 --- a/src/Bundle/Resources/config/services.xml +++ b/src/Bundle/Resources/config/services.xml @@ -128,5 +128,8 @@
+ + +
diff --git a/src/Bundle/Resources/config/services/twig.xml b/src/Bundle/Resources/config/services/twig.xml index daabcc3a..987e95cf 100644 --- a/src/Bundle/Resources/config/services/twig.xml +++ b/src/Bundle/Resources/config/services/twig.xml @@ -20,6 +20,7 @@ + @SyliusGrid/_grid.html.twig %sylius.grid.templates.action% %sylius.grid.templates.filter% diff --git a/src/Bundle/Tests/Functional/GridUiTest.php b/src/Bundle/Tests/Functional/GridUiTest.php index 627768e1..90723e68 100644 --- a/src/Bundle/Tests/Functional/GridUiTest.php +++ b/src/Bundle/Tests/Functional/GridUiTest.php @@ -41,6 +41,20 @@ public function it_shows_authors_grid(): void $this->assertCount(10, $this->getAuthorNamesFromResponse()); } + /** @test */ + public function it_shows_authors_ids(): void + { + $this->client->request('GET', '/authors/?limit=100'); + + $ids = $this->getAuthorIdsFromResponse(); + + $this->assertNotEmpty($ids); + $this->assertSame( + array_filter($ids, fn (string $id) => str_starts_with($id, '#')), + $ids, + ); + } + /** @test */ public function it_sorts_authors_by_name_ascending_by_default(): void { @@ -98,7 +112,7 @@ public function it_filters_books_by_title(): void $titles = $this->getBookTitlesFromResponse(); $this->assertCount(1, $titles); - $this->assertSame('Book 5', $titles[0]); + $this->assertSame('BOOK 5', $titles[0]); } /** @test */ @@ -112,7 +126,7 @@ public function it_filters_books_by_title_with_contains(): void $titles = $this->getBookTitlesFromResponse(); $this->assertCount(1, $titles); - $this->assertSame('Jurassic Park', $titles[0]); + $this->assertSame('JURASSIC PARK', $titles[0]); } /** @test */ @@ -125,7 +139,7 @@ public function it_filters_books_by_author(): void $titles = $this->getBookTitlesFromResponse(); $this->assertCount(2, $titles); - $this->assertSame('Jurassic Park', $titles[0]); + $this->assertSame('JURASSIC PARK', $titles[0]); } /** @test */ @@ -139,7 +153,7 @@ public function it_filters_books_by_authors(): void $titles = $this->getBookTitlesFromResponse(); $this->assertCount(3, $titles); - $this->assertSame('A Study in Scarlet', $titles[0]); + $this->assertSame('A STUDY IN SCARLET', $titles[0]); } /** @test */ @@ -152,7 +166,7 @@ public function it_filters_books_by_authors_nationality(): void $titles = $this->getBookTitlesFromResponse(); $this->assertCount(2, $titles); - $this->assertSame('Jurassic Park', $titles[0]); + $this->assertSame('JURASSIC PARK', $titles[0]); } /** @test */ @@ -165,7 +179,7 @@ public function it_filters_books_by_author_and_currency(): void $titles = $this->getBookTitlesFromResponse(); $this->assertCount(1, $titles); - $this->assertSame('Jurassic Park', $titles[0]); + $this->assertSame('JURASSIC PARK', $titles[0]); } /** @test */ @@ -274,6 +288,16 @@ private function getBookAuthorNationalitiesFromResponse(): array ); } + /** @return string[] */ + private function getAuthorIdsFromResponse(): array + { + return $this->getCrawler() + ->filter('[data-test-id]') + ->each( + fn (Crawler $node): string => $node->text(), + ); + } + /** @return string[] */ private function getAuthorNamesFromResponse(): array { diff --git a/src/Bundle/spec/Parser/OptionsParserSpec.php b/src/Bundle/spec/Parser/OptionsParserSpec.php new file mode 100644 index 00000000..e31ee42d --- /dev/null +++ b/src/Bundle/spec/Parser/OptionsParserSpec.php @@ -0,0 +1,70 @@ +shouldImplement(OptionsParserInterface::class); + } + + function it_parses_options_with_callback(): void + { + $this + ->parseOptions([ + 'type' => 'callback', + 'option' => [ + 'callback' => 'callback:App\\Helper\\GridHelper::addHashPrefix', + ], + 'label' => 'app.ui.id', + ]) + ->shouldBeAValidConfig([ + 'type' => 'callback', + 'option' => [], + 'label' => 'app.ui.id', + ]) + ; + } + + public function getMatchers(): array + { + return [ + 'beAValidConfig' => function ($subject, $subset) { + if ([] !== array_diff($subject, $subset)) { + return false; + } + + return is_callable($subject['option']['callback'] ?? null); + }, + ]; + } + + function it_fails_while_parsing_options_with_invalid_callback(): void + { + $this + ->shouldThrow(\RuntimeException::class) + ->during('parseOptions', [[ + 'type' => 'callback', + 'option' => [ + 'callback' => 'callback:foobar', + ], + 'label' => 'app.ui.id', + ]]) + ; + } +} diff --git a/src/Bundle/spec/Renderer/TwigGridRendererSpec.php b/src/Bundle/spec/Renderer/TwigGridRendererSpec.php index 1aeedb45..1f22f54a 100644 --- a/src/Bundle/spec/Renderer/TwigGridRendererSpec.php +++ b/src/Bundle/spec/Renderer/TwigGridRendererSpec.php @@ -16,6 +16,7 @@ use PhpSpec\ObjectBehavior; use Prophecy\Argument; use Sylius\Bundle\GridBundle\Form\Registry\FormTypeRegistryInterface; +use Sylius\Bundle\GridBundle\Parser\OptionsParserInterface; use Sylius\Component\Grid\Definition\Action; use Sylius\Component\Grid\Definition\Field; use Sylius\Component\Grid\FieldTypes\FieldTypeInterface; @@ -35,6 +36,7 @@ function let( ServiceRegistryInterface $fieldsRegistry, FormFactoryInterface $formFactory, FormTypeRegistryInterface $formTypeRegistry, + OptionsParserInterface $optionsParser, ): void { $actionTemplates = [ 'link' => '@SyliusGrid/Action/_link.html.twig', @@ -49,6 +51,7 @@ function let( $fieldsRegistry, $formFactory, $formTypeRegistry, + $optionsParser, '"@SyliusGrid/default"', $actionTemplates, $filterTemplates, @@ -94,6 +97,7 @@ function it_renders_a_field_with_data_via_appropriate_field_type( Field $field, ServiceRegistryInterface $fieldsRegistry, FieldTypeInterface $fieldType, + OptionsParserInterface $optionsParser, ): void { $field->getType()->willReturn('string'); $fieldsRegistry->get('string')->willReturn($fieldType); @@ -106,6 +110,7 @@ function it_renders_a_field_with_data_via_appropriate_field_type( $field->getOptions()->willReturn([ 'foo' => 'bar', ]); + $optionsParser->parseOptions(['foo' => 'bar'])->willReturn(['foo' => 'bar']); $fieldType->render($field, 'Value', ['foo' => 'bar'])->willReturn('Value'); $this->renderField($gridView, $field, 'Value')->shouldReturn('Value'); diff --git a/tests/Application/config/sylius/grids.yaml b/tests/Application/config/sylius/grids.yaml index beb008f6..cd5affa1 100644 --- a/tests/Application/config/sylius/grids.yaml +++ b/tests/Application/config/sylius/grids.yaml @@ -33,7 +33,9 @@ sylius_grid: title: asc fields: title: - type: string + type: callback + options: + callback: "callback:strtoupper" label: Title sortable: ~ author: @@ -60,10 +62,11 @@ sylius_grid: name: asc fields: id: - type: string + type: callback + options: + callback: "callback:App\\Helper\\GridHelper::addHashPrefix" label: ID sortable: ~ - enabled: false name: type: string label: Name diff --git a/tests/Application/config/sylius/grids/author.php b/tests/Application/config/sylius/grids/author.php index 89e7203f..93c26c29 100644 --- a/tests/Application/config/sylius/grids/author.php +++ b/tests/Application/config/sylius/grids/author.php @@ -11,6 +11,7 @@ declare(strict_types=1); +use Sylius\Bundle\GridBundle\Builder\Field\CallbackField; use Sylius\Bundle\GridBundle\Builder\Field\StringField; use Sylius\Bundle\GridBundle\Builder\Filter\StringFilter; use Sylius\Bundle\GridBundle\Builder\GridBuilder; @@ -22,9 +23,8 @@ ->addFilter(StringFilter::create('name')) ->orderBy('name', 'asc') ->addField( - StringField::create('id') - ->setSortable(true) - ->setEnabled(false), + CallbackField::create('id', ['App\\Helper\\GridHelper', 'addHashPrefix']) + ->setSortable(true), ) ->addField( StringField::create('name') diff --git a/tests/Application/config/sylius/grids/book.php b/tests/Application/config/sylius/grids/book.php index 460521a3..66d929ad 100644 --- a/tests/Application/config/sylius/grids/book.php +++ b/tests/Application/config/sylius/grids/book.php @@ -14,6 +14,7 @@ use App\Entity\Author; use App\Entity\Book; use App\Grid\Builder\NationalityFilter; +use Sylius\Bundle\GridBundle\Builder\Field\CallbackField; use Sylius\Bundle\GridBundle\Builder\Field\StringField; use Sylius\Bundle\GridBundle\Builder\Filter\EntityFilter; use Sylius\Bundle\GridBundle\Builder\Filter\SelectFilter; @@ -48,7 +49,7 @@ ) ->orderBy('title', 'asc') ->addField( - StringField::create('title') + CallbackField::create('title', 'strtoupper') ->setLabel('Title') ->setSortable(true), ) diff --git a/tests/Application/src/Grid/AuthorGrid.php b/tests/Application/src/Grid/AuthorGrid.php index f930efab..fbc05eec 100644 --- a/tests/Application/src/Grid/AuthorGrid.php +++ b/tests/Application/src/Grid/AuthorGrid.php @@ -13,6 +13,8 @@ namespace App\Grid; +use App\Helper\GridHelper; +use Sylius\Bundle\GridBundle\Builder\Field\CallbackField; use Sylius\Bundle\GridBundle\Builder\Field\StringField; use Sylius\Bundle\GridBundle\Builder\Filter\Filter; use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface; @@ -44,9 +46,8 @@ public function buildGrid(GridBuilderInterface $gridBuilder): void ->addFilter(Filter::create('name', 'string')) ->orderBy('name', 'asc') ->addField( - StringField::create('id') - ->setSortable(true) - ->setEnabled(false), + CallbackField::create('id', GridHelper::addHashPrefix(...)) + ->setSortable(true), ) ->addField( StringField::create('name') diff --git a/tests/Application/src/Grid/BookGrid.php b/tests/Application/src/Grid/BookGrid.php index 9c0ee24c..fa9e080d 100644 --- a/tests/Application/src/Grid/BookGrid.php +++ b/tests/Application/src/Grid/BookGrid.php @@ -22,6 +22,7 @@ use Sylius\Bundle\GridBundle\Builder\Action\UpdateAction; use Sylius\Bundle\GridBundle\Builder\ActionGroup\ItemActionGroup; use Sylius\Bundle\GridBundle\Builder\ActionGroup\MainActionGroup; +use Sylius\Bundle\GridBundle\Builder\Field\CallbackField; use Sylius\Bundle\GridBundle\Builder\Field\StringField; use Sylius\Bundle\GridBundle\Builder\Filter\Filter; use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface; @@ -73,7 +74,7 @@ public function buildGrid(GridBuilderInterface $gridBuilder): void ) ->orderBy('title', 'asc') ->addField( - StringField::create('title') + CallbackField::create('title', 'strtoupper') ->setLabel('Title') ->setSortable(true), ) diff --git a/tests/Application/src/Helper/GridHelper.php b/tests/Application/src/Helper/GridHelper.php new file mode 100644 index 00000000..b4761edf --- /dev/null +++ b/tests/Application/src/Helper/GridHelper.php @@ -0,0 +1,22 @@ + Date: Tue, 21 Jan 2025 16:33:44 +0100 Subject: [PATCH 3/8] refactor callback into callable --- docs/field_types.md | 32 +++++++++---------- .../{CallbackField.php => CallableField.php} | 8 ++--- src/Bundle/Parser/OptionsParser.php | 12 +++---- .../Resources/config/services/field_types.xml | 6 ++-- src/Bundle/spec/Parser/OptionsParserSpec.php | 16 +++++----- ...ackFieldType.php => CallableFieldType.php} | 8 ++--- ...TypeSpec.php => CallableFieldTypeSpec.php} | 18 +++++------ tests/Application/config/sylius/grids.yaml | 8 ++--- .../config/sylius/grids/author.php | 4 +-- .../Application/config/sylius/grids/book.php | 4 +-- tests/Application/src/Grid/AuthorGrid.php | 4 +-- tests/Application/src/Grid/BookGrid.php | 4 +-- 12 files changed, 62 insertions(+), 62 deletions(-) rename src/Bundle/Builder/Field/{CallbackField.php => CallableField.php} (61%) rename src/Component/FieldTypes/{CallbackFieldType.php => CallableFieldType.php} (81%) rename src/Component/spec/FieldTypes/{CallbackFieldTypeSpec.php => CallableFieldTypeSpec.php} (85%) diff --git a/docs/field_types.md b/docs/field_types.md index 673179f5..2408b16d 100644 --- a/docs/field_types.md +++ b/docs/field_types.md @@ -320,13 +320,13 @@ $field->setOptions([ ]); ``` -Callback +Callable -------- -The Callback column aims to offer almost as much flexibility as the Twig column, but without requiring the creation of a template. -You simply need to specify a callback, which allows you to transform the 'data' variable on the fly. +The Callable column aims to offer almost as much flexibility as the Twig column, but without requiring the creation of a template. +You simply need to specify a callable, which allows you to transform the 'data' variable on the fly. -When defining callbacks in YAML, only string representations of callables are supported. +When defining callables in YAML, only string representations of callables are supported. When configuring grids using PHP (as opposed to service grid configuration), both string and array callables are supported. However, closures cannot be used due to restrictions in Symfony's configuration (values of type "Closure" are not permitted in service configuration files). By contrast, when configuring grids with service definitions, you can use both callables and closures. @@ -342,14 +342,14 @@ sylius_grid: app_user: fields: id: - type: callback + type: callable options: - callback: "callback:App\\Helper\\GridHelper::addHashPrefix" + callable: "callable:App\\Helper\\GridHelper::addHashPrefix" label: app.ui.id name: - type: callback + type: callable options: - callback: "callback:strtoupper" + callable: "callable:strtoupper" label: app.ui.name ``` @@ -361,24 +361,24 @@ sylius_grid: addGrid(GridBuilder::create('app_user', '%app.model.user.class%') ->addField( - CallbackField::create('id', 'App\\Helper\\GridHelper::addHashPrefix') + CallableField::create('id', 'App\\Helper\\GridHelper::addHashPrefix') ->setLabel('app.ui.id') ) // or ->addField( - CallbackField::create('id', ['App\\Helper\\GridHelper', 'addHashPrefix']) + CallableField::create('id', ['App\\Helper\\GridHelper', 'addHashPrefix']) ->setLabel('app.ui.id') ) ->addField( - CallbackField::create('name', 'strtoupper') + CallableField::create('name', 'strtoupper') ->setLabel('app.ui.name') ) ) @@ -396,7 +396,7 @@ declare(strict_types=1); namespace App\Grid; use App\Entity\User; -use Sylius\Bundle\GridBundle\Builder\Field\CallbackField; +use Sylius\Bundle\GridBundle\Builder\Field\CallableField; use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface; use Sylius\Bundle\GridBundle\Grid\AbstractGrid; use Sylius\Bundle\GridBundle\Grid\ResourceAwareGridInterface; @@ -412,15 +412,15 @@ final class UserGrid extends AbstractGrid implements ResourceAwareGridInterface { $gridBuilder ->addField( - CallbackField::create('id', GridHelper::addHashPrefix(...)) + CallableField::create('id', GridHelper::addHashPrefix(...)) ->setLabel('app.ui.id') ) ->addField( - CallbackField::create('name', 'strtoupper') + CallableField::create('name', 'strtoupper') ->setLabel('app.ui.name') ) ->addField( - CallbackField::create('roles' fn (array $roles): string => implode(', ', $roles)) + CallableField::create('roles' fn (array $roles): string => implode(', ', $roles)) ->setLabel('app.ui.roles') ) ; diff --git a/src/Bundle/Builder/Field/CallbackField.php b/src/Bundle/Builder/Field/CallableField.php similarity index 61% rename from src/Bundle/Builder/Field/CallbackField.php rename to src/Bundle/Builder/Field/CallableField.php index b47c2a6d..bfbff649 100644 --- a/src/Bundle/Builder/Field/CallbackField.php +++ b/src/Bundle/Builder/Field/CallableField.php @@ -13,12 +13,12 @@ namespace Sylius\Bundle\GridBundle\Builder\Field; -final class CallbackField +final class CallableField { - public static function create(string $name, callable $callback, bool $htmlspecialchars = true): FieldInterface + public static function create(string $name, callable $callable, bool $htmlspecialchars = true): FieldInterface { - return Field::create($name, 'callback') - ->setOption('callback', $callback) + return Field::create($name, 'callable') + ->setOption('callable', $callable) ->setOption('htmlspecialchars', $htmlspecialchars) ; } diff --git a/src/Bundle/Parser/OptionsParser.php b/src/Bundle/Parser/OptionsParser.php index 4e835e1d..2e8d4c9f 100644 --- a/src/Bundle/Parser/OptionsParser.php +++ b/src/Bundle/Parser/OptionsParser.php @@ -45,19 +45,19 @@ private function parseOption($parameter) return $parameter; } - if (0 === strpos($parameter, 'callback:')) { - return $this->parseOptionCallback(substr($parameter, 9)); + if (0 === strpos($parameter, 'callable:')) { + return $this->parseOptionCallable(substr($parameter, 9)); } return $parameter; } - private function parseOptionCallback(string $callback): \Closure + private function parseOptionCallable(string $callable): \Closure { - if (!is_callable($callback)) { - throw new \RuntimeException(\sprintf('%s is not a callable.', $callback)); + if (!is_callable($callable)) { + throw new \RuntimeException(\sprintf('%s is not a callable.', $callable)); } - return $callback(...); + return $callable(...); } } diff --git a/src/Bundle/Resources/config/services/field_types.xml b/src/Bundle/Resources/config/services/field_types.xml index 5cf2daae..5b535898 100644 --- a/src/Bundle/Resources/config/services/field_types.xml +++ b/src/Bundle/Resources/config/services/field_types.xml @@ -15,11 +15,11 @@ - + - + - + diff --git a/src/Bundle/spec/Parser/OptionsParserSpec.php b/src/Bundle/spec/Parser/OptionsParserSpec.php index e31ee42d..8b13bb47 100644 --- a/src/Bundle/spec/Parser/OptionsParserSpec.php +++ b/src/Bundle/spec/Parser/OptionsParserSpec.php @@ -23,18 +23,18 @@ function it_is_an_options_parser(): void $this->shouldImplement(OptionsParserInterface::class); } - function it_parses_options_with_callback(): void + function it_parses_options_with_callable(): void { $this ->parseOptions([ - 'type' => 'callback', + 'type' => 'callable', 'option' => [ - 'callback' => 'callback:App\\Helper\\GridHelper::addHashPrefix', + 'callable' => 'callable:App\\Helper\\GridHelper::addHashPrefix', ], 'label' => 'app.ui.id', ]) ->shouldBeAValidConfig([ - 'type' => 'callback', + 'type' => 'callable', 'option' => [], 'label' => 'app.ui.id', ]) @@ -49,19 +49,19 @@ public function getMatchers(): array return false; } - return is_callable($subject['option']['callback'] ?? null); + return is_callable($subject['option']['callable'] ?? null); }, ]; } - function it_fails_while_parsing_options_with_invalid_callback(): void + function it_fails_while_parsing_options_with_invalid_callable(): void { $this ->shouldThrow(\RuntimeException::class) ->during('parseOptions', [[ - 'type' => 'callback', + 'type' => 'callable', 'option' => [ - 'callback' => 'callback:foobar', + 'callable' => 'callable:foobar', ], 'label' => 'app.ui.id', ]]) diff --git a/src/Component/FieldTypes/CallbackFieldType.php b/src/Component/FieldTypes/CallableFieldType.php similarity index 81% rename from src/Component/FieldTypes/CallbackFieldType.php rename to src/Component/FieldTypes/CallableFieldType.php index ce4833ea..c6d64dfd 100644 --- a/src/Component/FieldTypes/CallbackFieldType.php +++ b/src/Component/FieldTypes/CallableFieldType.php @@ -17,7 +17,7 @@ use Sylius\Component\Grid\Definition\Field; use Symfony\Component\OptionsResolver\OptionsResolver; -final class CallbackFieldType implements FieldTypeInterface +final class CallableFieldType implements FieldTypeInterface { public function __construct(private DataExtractorInterface $dataExtractor) { @@ -26,7 +26,7 @@ public function __construct(private DataExtractorInterface $dataExtractor) public function render(Field $field, $data, array $options): string { $value = $this->dataExtractor->get($field, $data); - $value = (string) call_user_func($options['callback'], $value); + $value = (string) call_user_func($options['callable'], $value); if ($options['htmlspecialchars']) { $value = htmlspecialchars($value); @@ -37,8 +37,8 @@ public function render(Field $field, $data, array $options): string public function configureOptions(OptionsResolver $resolver): void { - $resolver->setRequired('callback'); - $resolver->setAllowedTypes('callback', 'callable'); + $resolver->setRequired('callable'); + $resolver->setAllowedTypes('callable', 'callable'); $resolver->setDefault('htmlspecialchars', true); $resolver->setAllowedTypes('htmlspecialchars', 'bool'); diff --git a/src/Component/spec/FieldTypes/CallbackFieldTypeSpec.php b/src/Component/spec/FieldTypes/CallableFieldTypeSpec.php similarity index 85% rename from src/Component/spec/FieldTypes/CallbackFieldTypeSpec.php rename to src/Component/spec/FieldTypes/CallableFieldTypeSpec.php index ed7e60e7..3c845d71 100644 --- a/src/Component/spec/FieldTypes/CallbackFieldTypeSpec.php +++ b/src/Component/spec/FieldTypes/CallableFieldTypeSpec.php @@ -18,7 +18,7 @@ use Sylius\Component\Grid\Definition\Field; use Sylius\Component\Grid\FieldTypes\FieldTypeInterface; -final class CallbackFieldTypeSpec extends ObjectBehavior +final class CallableFieldTypeSpec extends ObjectBehavior { function let(DataExtractorInterface $dataExtractor): void { @@ -30,50 +30,50 @@ function it_is_a_grid_field_type(): void $this->shouldImplement(FieldTypeInterface::class); } - function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_callback_with_htmlspecialchars( + function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_callable_with_htmlspecialchars( DataExtractorInterface $dataExtractor, Field $field, ): void { $dataExtractor->get($field, ['foo' => 'bar'])->willReturn('bar'); $this->render($field, ['foo' => 'bar'], [ - 'callback' => fn (string $value): string => "$value", + 'callable' => fn (string $value): string => "$value", 'htmlspecialchars' => true, ])->shouldReturn('<strong>bar</strong>'); } - function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_callback_without_htmlspecialchars( + function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_callable_without_htmlspecialchars( DataExtractorInterface $dataExtractor, Field $field, ): void { $dataExtractor->get($field, ['foo' => 'bar'])->willReturn('bar'); $this->render($field, ['foo' => 'bar'], [ - 'callback' => fn (string $value): string => "$value", + 'callable' => fn (string $value): string => "$value", 'htmlspecialchars' => false, ])->shouldReturn('bar'); } - function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_function_callback( + function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_function_callable( DataExtractorInterface $dataExtractor, Field $field, ): void { $dataExtractor->get($field, ['foo' => 'bar'])->willReturn('bar'); $this->render($field, ['foo' => 'bar'], [ - 'callback' => 'strtoupper', + 'callable' => 'strtoupper', 'htmlspecialchars' => true, ])->shouldReturn('BAR'); } - function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_static_callback( + function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_static_callable( DataExtractorInterface $dataExtractor, Field $field, ): void { $dataExtractor->get($field, ['foo' => 'bar'])->willReturn('BAR'); $this->render($field, ['foo' => 'bar'], [ - 'callback' => [self::class, 'callable'], + 'callable' => [self::class, 'callable'], 'htmlspecialchars' => true, ])->shouldReturn('bar'); } diff --git a/tests/Application/config/sylius/grids.yaml b/tests/Application/config/sylius/grids.yaml index cd5affa1..b234503c 100644 --- a/tests/Application/config/sylius/grids.yaml +++ b/tests/Application/config/sylius/grids.yaml @@ -33,9 +33,9 @@ sylius_grid: title: asc fields: title: - type: callback + type: callable options: - callback: "callback:strtoupper" + callable: "callable:strtoupper" label: Title sortable: ~ author: @@ -62,9 +62,9 @@ sylius_grid: name: asc fields: id: - type: callback + type: callable options: - callback: "callback:App\\Helper\\GridHelper::addHashPrefix" + callable: "callable:App\\Helper\\GridHelper::addHashPrefix" label: ID sortable: ~ name: diff --git a/tests/Application/config/sylius/grids/author.php b/tests/Application/config/sylius/grids/author.php index 93c26c29..e75264f7 100644 --- a/tests/Application/config/sylius/grids/author.php +++ b/tests/Application/config/sylius/grids/author.php @@ -11,7 +11,7 @@ declare(strict_types=1); -use Sylius\Bundle\GridBundle\Builder\Field\CallbackField; +use Sylius\Bundle\GridBundle\Builder\Field\CallableField; use Sylius\Bundle\GridBundle\Builder\Field\StringField; use Sylius\Bundle\GridBundle\Builder\Filter\StringFilter; use Sylius\Bundle\GridBundle\Builder\GridBuilder; @@ -23,7 +23,7 @@ ->addFilter(StringFilter::create('name')) ->orderBy('name', 'asc') ->addField( - CallbackField::create('id', ['App\\Helper\\GridHelper', 'addHashPrefix']) + CallableField::create('id', ['App\\Helper\\GridHelper', 'addHashPrefix']) ->setSortable(true), ) ->addField( diff --git a/tests/Application/config/sylius/grids/book.php b/tests/Application/config/sylius/grids/book.php index 66d929ad..7f1e1b74 100644 --- a/tests/Application/config/sylius/grids/book.php +++ b/tests/Application/config/sylius/grids/book.php @@ -14,7 +14,7 @@ use App\Entity\Author; use App\Entity\Book; use App\Grid\Builder\NationalityFilter; -use Sylius\Bundle\GridBundle\Builder\Field\CallbackField; +use Sylius\Bundle\GridBundle\Builder\Field\CallableField; use Sylius\Bundle\GridBundle\Builder\Field\StringField; use Sylius\Bundle\GridBundle\Builder\Filter\EntityFilter; use Sylius\Bundle\GridBundle\Builder\Filter\SelectFilter; @@ -49,7 +49,7 @@ ) ->orderBy('title', 'asc') ->addField( - CallbackField::create('title', 'strtoupper') + CallableField::create('title', 'strtoupper') ->setLabel('Title') ->setSortable(true), ) diff --git a/tests/Application/src/Grid/AuthorGrid.php b/tests/Application/src/Grid/AuthorGrid.php index fbc05eec..f779f75b 100644 --- a/tests/Application/src/Grid/AuthorGrid.php +++ b/tests/Application/src/Grid/AuthorGrid.php @@ -14,7 +14,7 @@ namespace App\Grid; use App\Helper\GridHelper; -use Sylius\Bundle\GridBundle\Builder\Field\CallbackField; +use Sylius\Bundle\GridBundle\Builder\Field\CallableField; use Sylius\Bundle\GridBundle\Builder\Field\StringField; use Sylius\Bundle\GridBundle\Builder\Filter\Filter; use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface; @@ -46,7 +46,7 @@ public function buildGrid(GridBuilderInterface $gridBuilder): void ->addFilter(Filter::create('name', 'string')) ->orderBy('name', 'asc') ->addField( - CallbackField::create('id', GridHelper::addHashPrefix(...)) + CallableField::create('id', GridHelper::addHashPrefix(...)) ->setSortable(true), ) ->addField( diff --git a/tests/Application/src/Grid/BookGrid.php b/tests/Application/src/Grid/BookGrid.php index fa9e080d..4359d619 100644 --- a/tests/Application/src/Grid/BookGrid.php +++ b/tests/Application/src/Grid/BookGrid.php @@ -22,7 +22,7 @@ use Sylius\Bundle\GridBundle\Builder\Action\UpdateAction; use Sylius\Bundle\GridBundle\Builder\ActionGroup\ItemActionGroup; use Sylius\Bundle\GridBundle\Builder\ActionGroup\MainActionGroup; -use Sylius\Bundle\GridBundle\Builder\Field\CallbackField; +use Sylius\Bundle\GridBundle\Builder\Field\CallableField; use Sylius\Bundle\GridBundle\Builder\Field\StringField; use Sylius\Bundle\GridBundle\Builder\Filter\Filter; use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface; @@ -74,7 +74,7 @@ public function buildGrid(GridBuilderInterface $gridBuilder): void ) ->orderBy('title', 'asc') ->addField( - CallbackField::create('title', 'strtoupper') + CallableField::create('title', 'strtoupper') ->setLabel('Title') ->setSortable(true), ) From 84925decfdd59875a2b62a588d88e14830a965bc Mon Sep 17 00:00:00 2001 From: Florian Merle Date: Wed, 22 Jan 2025 11:41:40 +0100 Subject: [PATCH 4/8] fix --- phpstan-baseline.neon | 6 ++ phpstan.neon | 2 + src/Bundle/Renderer/TwigGridRenderer.php | 24 +++++-- src/Bundle/Resources/config/services/twig.xml | 2 +- .../Tests/Unit/Parser/OptionsParserTest.php | 56 +++++++++++++++ src/Bundle/spec/Parser/OptionsParserSpec.php | 70 ------------------- .../spec/Renderer/TwigGridRendererSpec.php | 46 +++++++++++- .../FieldTypes/CallableFieldType.php | 8 ++- .../spec/FieldTypes/CallableFieldTypeSpec.php | 19 +++++ 9 files changed, 155 insertions(+), 78 deletions(-) create mode 100644 phpstan-baseline.neon create mode 100644 src/Bundle/Tests/Unit/Parser/OptionsParserTest.php delete mode 100644 src/Bundle/spec/Parser/OptionsParserSpec.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..55506027 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^Dead catch \\- Throwable is never thrown in the try block\\.$#" + count: 1 + path: src/Component/FieldTypes/CallableFieldType.php diff --git a/phpstan.neon b/phpstan.neon index 659daca6..8d3c31e5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,4 +1,6 @@ includes: + - phpstan-baseline.neon + - vendor/phpstan/phpstan-webmozart-assert/extension.neon - vendor/phpstan/phpstan-phpunit/extension.neon diff --git a/src/Bundle/Renderer/TwigGridRenderer.php b/src/Bundle/Renderer/TwigGridRenderer.php index e761b6d1..9dd2a475 100644 --- a/src/Bundle/Renderer/TwigGridRenderer.php +++ b/src/Bundle/Renderer/TwigGridRenderer.php @@ -37,32 +37,42 @@ final class TwigGridRenderer implements GridRendererInterface private FormTypeRegistryInterface $formTypeRegistry; - private OptionsParserInterface $optionsParser; - private string $defaultTemplate; private array $actionTemplates; private array $filterTemplates; + private ?OptionsParserInterface $optionsParser; + public function __construct( Environment $twig, ServiceRegistryInterface $fieldsRegistry, FormFactoryInterface $formFactory, FormTypeRegistryInterface $formTypeRegistry, - OptionsParserInterface $optionsParser, string $defaultTemplate, array $actionTemplates = [], array $filterTemplates = [], + ?OptionsParserInterface $optionsParser = null, ) { $this->twig = $twig; $this->fieldsRegistry = $fieldsRegistry; $this->formFactory = $formFactory; $this->formTypeRegistry = $formTypeRegistry; - $this->optionsParser = $optionsParser; $this->defaultTemplate = $defaultTemplate; $this->actionTemplates = $actionTemplates; $this->filterTemplates = $filterTemplates; + $this->optionsParser = $optionsParser; + + if (null === $optionsParser) { + trigger_deprecation( + 'sylius/grid-bundle', + '1.14', + 'Not passing an instance of "%s" as the eighth constructor argument of "%s" is deprecated.', + OptionsParserInterface::class, + self::class, + ); + } } public function render(GridViewInterface $gridView, ?string $template = null) @@ -77,7 +87,11 @@ public function renderField(GridViewInterface $gridView, Field $field, $data) $resolver = new OptionsResolver(); $fieldType->configureOptions($resolver); - $options = $resolver->resolve($this->optionsParser->parseOptions($field->getOptions())); + $options = $field->getOptions(); + if (null !== $this->optionsParser) { + $options = $this->optionsParser->parseOptions($options); + } + $options = $resolver->resolve($options); return $fieldType->render($field, $data, $options); } diff --git a/src/Bundle/Resources/config/services/twig.xml b/src/Bundle/Resources/config/services/twig.xml index 987e95cf..30f0dc54 100644 --- a/src/Bundle/Resources/config/services/twig.xml +++ b/src/Bundle/Resources/config/services/twig.xml @@ -20,10 +20,10 @@ - @SyliusGrid/_grid.html.twig %sylius.grid.templates.action% %sylius.grid.templates.filter% + diff --git a/src/Bundle/Tests/Unit/Parser/OptionsParserTest.php b/src/Bundle/Tests/Unit/Parser/OptionsParserTest.php new file mode 100644 index 00000000..a6321347 --- /dev/null +++ b/src/Bundle/Tests/Unit/Parser/OptionsParserTest.php @@ -0,0 +1,56 @@ +assertInstanceOf(OptionsParserInterface::class, new OptionsParser()); + } + + public function testItParserOptionsWithCallable(): void + { + $options = (new OptionsParser())->parseOptions([ + 'type' => 'callable', + 'option' => [ + 'callable' => 'callable:strtoupper', + ], + 'label' => 'app.ui.id', + ]); + + $this->assertArrayHasKey('type', $options); + $this->assertArrayHasKey('option', $options); + $this->assertArrayHasKey('label', $options); + + $this->assertIsCallable($options['option']['callable'] ?? null); + } + + public function testItFailsWhileParsingOptionsWithInvalidCallable(): void + { + $this->expectException(\RuntimeException::class); + + $options = (new OptionsParser())->parseOptions([ + 'type' => 'callable', + 'option' => [ + 'callable' => 'callable:foobar', + ], + 'label' => 'app.ui.id', + ]); + } +} diff --git a/src/Bundle/spec/Parser/OptionsParserSpec.php b/src/Bundle/spec/Parser/OptionsParserSpec.php deleted file mode 100644 index 8b13bb47..00000000 --- a/src/Bundle/spec/Parser/OptionsParserSpec.php +++ /dev/null @@ -1,70 +0,0 @@ -shouldImplement(OptionsParserInterface::class); - } - - function it_parses_options_with_callable(): void - { - $this - ->parseOptions([ - 'type' => 'callable', - 'option' => [ - 'callable' => 'callable:App\\Helper\\GridHelper::addHashPrefix', - ], - 'label' => 'app.ui.id', - ]) - ->shouldBeAValidConfig([ - 'type' => 'callable', - 'option' => [], - 'label' => 'app.ui.id', - ]) - ; - } - - public function getMatchers(): array - { - return [ - 'beAValidConfig' => function ($subject, $subset) { - if ([] !== array_diff($subject, $subset)) { - return false; - } - - return is_callable($subject['option']['callable'] ?? null); - }, - ]; - } - - function it_fails_while_parsing_options_with_invalid_callable(): void - { - $this - ->shouldThrow(\RuntimeException::class) - ->during('parseOptions', [[ - 'type' => 'callable', - 'option' => [ - 'callable' => 'callable:foobar', - ], - 'label' => 'app.ui.id', - ]]) - ; - } -} diff --git a/src/Bundle/spec/Renderer/TwigGridRendererSpec.php b/src/Bundle/spec/Renderer/TwigGridRendererSpec.php index 1f22f54a..717a3c19 100644 --- a/src/Bundle/spec/Renderer/TwigGridRendererSpec.php +++ b/src/Bundle/spec/Renderer/TwigGridRendererSpec.php @@ -51,10 +51,10 @@ function let( $fieldsRegistry, $formFactory, $formTypeRegistry, - $optionsParser, '"@SyliusGrid/default"', $actionTemplates, $filterTemplates, + $optionsParser, ); } @@ -116,6 +116,50 @@ function it_renders_a_field_with_data_via_appropriate_field_type( $this->renderField($gridView, $field, 'Value')->shouldReturn('Value'); } + function it_renders_a_field_with_data_via_appropriate_field_type_when_no_option_parser_is_provided( + Environment $twig, + ServiceRegistryInterface $fieldsRegistry, + FormFactoryInterface $formFactory, + FormTypeRegistryInterface $formTypeRegistry, + GridViewInterface $gridView, + Field $field, + FieldTypeInterface $fieldType, + ): void { + $actionTemplates = [ + 'link' => '@SyliusGrid/Action/_link.html.twig', + 'form' => '@SyliusGrid/Action/_form.html.twig', + ]; + $filterTemplates = [ + StringFilter::NAME => '@SyliusGrid/Filter/_string.html.twig', + ]; + + $this->beConstructedWith( + $twig, + $fieldsRegistry, + $formFactory, + $formTypeRegistry, + '"@SyliusGrid/default"', + $actionTemplates, + $filterTemplates, + null, + ); + + $field->getType()->willReturn('string'); + $fieldsRegistry->get('string')->willReturn($fieldType); + $fieldType->configureOptions(Argument::type(OptionsResolver::class)) + ->will(function ($args) { + $args[0]->setRequired('foo'); + }) + ; + + $field->getOptions()->willReturn([ + 'foo' => 'bar', + ]); + $fieldType->render($field, 'Value', ['foo' => 'bar'])->willReturn('Value'); + + $this->renderField($gridView, $field, 'Value')->shouldReturn('Value'); + } + function it_throws_an_exception_if_template_is_not_configured_for_given_action_type( GridViewInterface $gridView, Action $action, diff --git a/src/Component/FieldTypes/CallableFieldType.php b/src/Component/FieldTypes/CallableFieldType.php index c6d64dfd..2bdd986e 100644 --- a/src/Component/FieldTypes/CallableFieldType.php +++ b/src/Component/FieldTypes/CallableFieldType.php @@ -26,7 +26,13 @@ public function __construct(private DataExtractorInterface $dataExtractor) public function render(Field $field, $data, array $options): string { $value = $this->dataExtractor->get($field, $data); - $value = (string) call_user_func($options['callable'], $value); + $value = call_user_func($options['callable'], $value); + + try { + $value = (string) $value; + } catch (\Throwable $e) { + throw new \RuntimeException(\sprintf('The callback for field "%s" returned a value that could not be converted to string.', $field->getName())); + } if ($options['htmlspecialchars']) { $value = htmlspecialchars($value); diff --git a/src/Component/spec/FieldTypes/CallableFieldTypeSpec.php b/src/Component/spec/FieldTypes/CallableFieldTypeSpec.php index 3c845d71..43fbaf46 100644 --- a/src/Component/spec/FieldTypes/CallableFieldTypeSpec.php +++ b/src/Component/spec/FieldTypes/CallableFieldTypeSpec.php @@ -78,6 +78,25 @@ function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_static_callabl ])->shouldReturn('bar'); } + function it_throws_an_exception_when_a_callable_return_value_cannot_be_casted_to_string( + DataExtractorInterface $dataExtractor, + Field $field, + ): void { + $field->getName()->willReturn('id'); + $dataExtractor->get($field, ['foo' => 'bar'])->willReturn('BAR'); + + $this + ->shouldThrow(\RuntimeException::class) + ->during('render', [ + $field, + ['foo' => 'bar'], + [ + 'callable' => fn () => new \stdclass(), + 'htmlspecialchars' => true, + ], + ]); + } + static function callable(mixed $value): string { return strtolower($value); From 432e63db8d1642d07d86b1a67d31b47659be7fb9 Mon Sep 17 00:00:00 2001 From: Florian Merle Date: Mon, 27 Jan 2025 14:28:43 +0100 Subject: [PATCH 5/8] fix --- src/Bundle/Parser/OptionsParser.php | 14 ++------------ src/Bundle/Resources/config/services.xml | 4 ++-- src/Bundle/Resources/config/services/twig.xml | 2 +- src/Component/FieldTypes/CallableFieldType.php | 2 +- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/Bundle/Parser/OptionsParser.php b/src/Bundle/Parser/OptionsParser.php index 2e8d4c9f..25f3e17d 100644 --- a/src/Bundle/Parser/OptionsParser.php +++ b/src/Bundle/Parser/OptionsParser.php @@ -18,12 +18,7 @@ final class OptionsParser implements OptionsParserInterface public function parseOptions(array $parameters): array { return array_map( - /** - * @param mixed $parameter - * - * @return mixed - */ - function ($parameter) { + function (mixed $parameter): mixed { if (is_array($parameter)) { return $this->parseOptions($parameter); } @@ -34,12 +29,7 @@ function ($parameter) { ); } - /** - * @param mixed $parameter - * - * @return mixed - */ - private function parseOption($parameter) + private function parseOption(mixed $parameter): mixed { if (!is_string($parameter)) { return $parameter; diff --git a/src/Bundle/Resources/config/services.xml b/src/Bundle/Resources/config/services.xml index b88489f7..7305cd5e 100644 --- a/src/Bundle/Resources/config/services.xml +++ b/src/Bundle/Resources/config/services.xml @@ -129,7 +129,7 @@ - - + + diff --git a/src/Bundle/Resources/config/services/twig.xml b/src/Bundle/Resources/config/services/twig.xml index 30f0dc54..aad1b61a 100644 --- a/src/Bundle/Resources/config/services/twig.xml +++ b/src/Bundle/Resources/config/services/twig.xml @@ -23,7 +23,7 @@ @SyliusGrid/_grid.html.twig %sylius.grid.templates.action% %sylius.grid.templates.filter% - + diff --git a/src/Component/FieldTypes/CallableFieldType.php b/src/Component/FieldTypes/CallableFieldType.php index 2bdd986e..375bdad7 100644 --- a/src/Component/FieldTypes/CallableFieldType.php +++ b/src/Component/FieldTypes/CallableFieldType.php @@ -30,7 +30,7 @@ public function render(Field $field, $data, array $options): string try { $value = (string) $value; - } catch (\Throwable $e) { + } catch (\Throwable) { throw new \RuntimeException(\sprintf('The callback for field "%s" returned a value that could not be converted to string.', $field->getName())); } From bfe9951b5e5401bf42995363e9d6dbd9d58d7280 Mon Sep 17 00:00:00 2001 From: Florian Merle Date: Thu, 30 Jan 2025 10:37:23 +0100 Subject: [PATCH 6/8] remove doc --- docs/field_types.md | 115 -------------------------------------------- 1 file changed, 115 deletions(-) diff --git a/docs/field_types.md b/docs/field_types.md index 2408b16d..f12d664d 100644 --- a/docs/field_types.md +++ b/docs/field_types.md @@ -319,118 +319,3 @@ $field->setOptions([ // Your options here ]); ``` - -Callable --------- - -The Callable column aims to offer almost as much flexibility as the Twig column, but without requiring the creation of a template. -You simply need to specify a callable, which allows you to transform the 'data' variable on the fly. - -When defining callables in YAML, only string representations of callables are supported. -When configuring grids using PHP (as opposed to service grid configuration), both string and array callables are supported. However, closures cannot be used due to restrictions in Symfony's configuration (values of type "Closure" are not permitted in service configuration files). -By contrast, when configuring grids with service definitions, you can use both callables and closures. - -Here are some examples of what you can do: - -
Yaml - -```yaml -# config/packages/sylius_grid.yaml - -sylius_grid: - grids: - app_user: - fields: - id: - type: callable - options: - callable: "callable:App\\Helper\\GridHelper::addHashPrefix" - label: app.ui.id - name: - type: callable - options: - callable: "callable:strtoupper" - label: app.ui.name -``` - -
- -
PHP - -```php -addGrid(GridBuilder::create('app_user', '%app.model.user.class%') - ->addField( - CallableField::create('id', 'App\\Helper\\GridHelper::addHashPrefix') - ->setLabel('app.ui.id') - ) - // or - ->addField( - CallableField::create('id', ['App\\Helper\\GridHelper', 'addHashPrefix']) - ->setLabel('app.ui.id') - ) - - ->addField( - CallableField::create('name', 'strtoupper') - ->setLabel('app.ui.name') - ) - ) -}; -``` - -OR - -```php -addField( - CallableField::create('id', GridHelper::addHashPrefix(...)) - ->setLabel('app.ui.id') - ) - ->addField( - CallableField::create('name', 'strtoupper') - ->setLabel('app.ui.name') - ) - ->addField( - CallableField::create('roles' fn (array $roles): string => implode(', ', $roles)) - ->setLabel('app.ui.roles') - ) - ; - } - - public function getResourceClass(): string - { - return User::class; - } -} -``` - -
From 2f8f6d2546dd08e861fcf6ddd4e61df98825b3a3 Mon Sep 17 00:00:00 2001 From: Florian Merle Date: Thu, 30 Jan 2025 10:44:37 +0100 Subject: [PATCH 7/8] fix --- src/Bundle/Parser/OptionsParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/Parser/OptionsParser.php b/src/Bundle/Parser/OptionsParser.php index 25f3e17d..7d74267d 100644 --- a/src/Bundle/Parser/OptionsParser.php +++ b/src/Bundle/Parser/OptionsParser.php @@ -35,7 +35,7 @@ private function parseOption(mixed $parameter): mixed return $parameter; } - if (0 === strpos($parameter, 'callable:')) { + if (str_starts_with($parameter, 'callable:')) { return $this->parseOptionCallable(substr($parameter, 9)); } From 58f0dbcc316f4309b5c2351c2d70c297f6972b3e Mon Sep 17 00:00:00 2001 From: Florian Merle Date: Tue, 4 Feb 2025 18:52:24 +0100 Subject: [PATCH 8/8] fix --- phpstan.neon | 2 -- src/Bundle/Parser/OptionsParser.php | 4 +++- src/Bundle/Resources/config/services.xml | 4 ++-- src/Bundle/Resources/config/services/twig.xml | 2 +- .../Tests/Unit/Parser/OptionsParserTest.php | 5 +++-- src/Component/Exception/ExceptionInterface.php | 18 ++++++++++++++++++ .../Exception/InvalidArgumentException.php | 18 ++++++++++++++++++ .../Exception/UnexpectedValueException.php | 18 ++++++++++++++++++ src/Component/FieldTypes/CallableFieldType.php | 9 +++++++-- .../spec/FieldTypes/CallableFieldTypeSpec.php | 7 ++++--- 10 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 src/Component/Exception/ExceptionInterface.php create mode 100644 src/Component/Exception/InvalidArgumentException.php create mode 100644 src/Component/Exception/UnexpectedValueException.php diff --git a/phpstan.neon b/phpstan.neon index 8d3c31e5..1b942ec4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,9 +1,7 @@ includes: - phpstan-baseline.neon - - vendor/phpstan/phpstan-webmozart-assert/extension.neon - vendor/phpstan/phpstan-phpunit/extension.neon - - vendor/phpstan/phpstan-phpunit/rules.neon parameters: diff --git a/src/Bundle/Parser/OptionsParser.php b/src/Bundle/Parser/OptionsParser.php index 7d74267d..e9e8bc53 100644 --- a/src/Bundle/Parser/OptionsParser.php +++ b/src/Bundle/Parser/OptionsParser.php @@ -13,6 +13,8 @@ namespace Sylius\Bundle\GridBundle\Parser; +use Sylius\Component\Grid\Exception\InvalidArgumentException; + final class OptionsParser implements OptionsParserInterface { public function parseOptions(array $parameters): array @@ -45,7 +47,7 @@ private function parseOption(mixed $parameter): mixed private function parseOptionCallable(string $callable): \Closure { if (!is_callable($callable)) { - throw new \RuntimeException(\sprintf('%s is not a callable.', $callable)); + throw new InvalidArgumentException(\sprintf('%s is not a callable.', $callable)); } return $callable(...); diff --git a/src/Bundle/Resources/config/services.xml b/src/Bundle/Resources/config/services.xml index 7305cd5e..f364d935 100644 --- a/src/Bundle/Resources/config/services.xml +++ b/src/Bundle/Resources/config/services.xml @@ -129,7 +129,7 @@
- - + + diff --git a/src/Bundle/Resources/config/services/twig.xml b/src/Bundle/Resources/config/services/twig.xml index aad1b61a..30f0dc54 100644 --- a/src/Bundle/Resources/config/services/twig.xml +++ b/src/Bundle/Resources/config/services/twig.xml @@ -23,7 +23,7 @@ @SyliusGrid/_grid.html.twig %sylius.grid.templates.action% %sylius.grid.templates.filter% - + diff --git a/src/Bundle/Tests/Unit/Parser/OptionsParserTest.php b/src/Bundle/Tests/Unit/Parser/OptionsParserTest.php index a6321347..c3e33864 100644 --- a/src/Bundle/Tests/Unit/Parser/OptionsParserTest.php +++ b/src/Bundle/Tests/Unit/Parser/OptionsParserTest.php @@ -16,6 +16,7 @@ use PHPUnit\Framework\TestCase; use Sylius\Bundle\GridBundle\Parser\OptionsParser; use Sylius\Bundle\GridBundle\Parser\OptionsParserInterface; +use Sylius\Component\Grid\Exception\InvalidArgumentException; final class OptionsParserTest extends TestCase { @@ -24,7 +25,7 @@ public function testItImplementsOptionsParserInterface(): void $this->assertInstanceOf(OptionsParserInterface::class, new OptionsParser()); } - public function testItParserOptionsWithCallable(): void + public function testItParsesOptionsWithCallable(): void { $options = (new OptionsParser())->parseOptions([ 'type' => 'callable', @@ -43,7 +44,7 @@ public function testItParserOptionsWithCallable(): void public function testItFailsWhileParsingOptionsWithInvalidCallable(): void { - $this->expectException(\RuntimeException::class); + $this->expectException(InvalidArgumentException::class); $options = (new OptionsParser())->parseOptions([ 'type' => 'callable', diff --git a/src/Component/Exception/ExceptionInterface.php b/src/Component/Exception/ExceptionInterface.php new file mode 100644 index 00000000..b449cba7 --- /dev/null +++ b/src/Component/Exception/ExceptionInterface.php @@ -0,0 +1,18 @@ +getName())); + } catch (\Throwable $e) { + throw new UnexpectedValueException(\sprintf( + 'Callable field (name "%s") returned value could not be converted to string: "%s".', + $field->getName(), + $e->getMessage(), + )); } if ($options['htmlspecialchars']) { diff --git a/src/Component/spec/FieldTypes/CallableFieldTypeSpec.php b/src/Component/spec/FieldTypes/CallableFieldTypeSpec.php index 43fbaf46..0123d21d 100644 --- a/src/Component/spec/FieldTypes/CallableFieldTypeSpec.php +++ b/src/Component/spec/FieldTypes/CallableFieldTypeSpec.php @@ -16,6 +16,7 @@ use PhpSpec\ObjectBehavior; use Sylius\Component\Grid\DataExtractor\DataExtractorInterface; use Sylius\Component\Grid\Definition\Field; +use Sylius\Component\Grid\Exception\UnexpectedValueException; use Sylius\Component\Grid\FieldTypes\FieldTypeInterface; final class CallableFieldTypeSpec extends ObjectBehavior @@ -70,9 +71,9 @@ function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_static_callabl DataExtractorInterface $dataExtractor, Field $field, ): void { - $dataExtractor->get($field, ['foo' => 'bar'])->willReturn('BAR'); + $dataExtractor->get($field, ['foo' => 'BAR'])->willReturn('BAR'); - $this->render($field, ['foo' => 'bar'], [ + $this->render($field, ['foo' => 'BAR'], [ 'callable' => [self::class, 'callable'], 'htmlspecialchars' => true, ])->shouldReturn('bar'); @@ -86,7 +87,7 @@ function it_throws_an_exception_when_a_callable_return_value_cannot_be_casted_to $dataExtractor->get($field, ['foo' => 'bar'])->willReturn('BAR'); $this - ->shouldThrow(\RuntimeException::class) + ->shouldThrow(UnexpectedValueException::class) ->during('render', [ $field, ['foo' => 'bar'],