diff --git a/src/Bundle/Builder/Field/CallableField.php b/src/Bundle/Builder/Field/CallableField.php
index bfbff649..e24839d7 100644
--- a/src/Bundle/Builder/Field/CallableField.php
+++ b/src/Bundle/Builder/Field/CallableField.php
@@ -22,4 +22,18 @@ public static function create(string $name, callable $callable, bool $htmlspecia
->setOption('htmlspecialchars', $htmlspecialchars)
;
}
+
+ public static function createForService(string $name, string $service, ?string $method = null, bool $htmlspecialchars = true): FieldInterface
+ {
+ $field = Field::create($name, 'callable')
+ ->setOption('service', $service)
+ ->setOption('htmlspecialchars', $htmlspecialchars)
+ ;
+
+ if ($method !== null) {
+ $field->setOption('method', $method);
+ }
+
+ return $field;
+ }
}
diff --git a/src/Bundle/DependencyInjection/SyliusGridExtension.php b/src/Bundle/DependencyInjection/SyliusGridExtension.php
index 76d1470f..2eae760f 100644
--- a/src/Bundle/DependencyInjection/SyliusGridExtension.php
+++ b/src/Bundle/DependencyInjection/SyliusGridExtension.php
@@ -16,6 +16,7 @@
use Sylius\Bundle\CurrencyBundle\SyliusCurrencyBundle;
use Sylius\Bundle\GridBundle\Grid\GridInterface;
use Sylius\Bundle\GridBundle\SyliusGridBundle;
+use Sylius\Component\Grid\Annotation\AsGridFieldCallableService;
use Sylius\Component\Grid\Attribute\AsFilter;
use Sylius\Component\Grid\Data\DataProviderInterface;
use Sylius\Component\Grid\Filtering\ConfigurableFilterInterface;
@@ -85,6 +86,13 @@ static function (ChildDefinition $definition, AsFilter $attribute, \Reflector $r
$container->registerForAutoconfiguration(DataProviderInterface::class)
->addTag('sylius.grid_data_provider')
;
+
+ $container->registerAttributeForAutoconfiguration(
+ AsGridFieldCallableService::class,
+ static function (ChildDefinition $definition, AsGridFieldCallableService $attribute, \Reflector $reflector): void {
+ $definition->addTag('sylius.grid_field_callable_service');
+ },
+ );
}
public function getConfiguration(array $config, ContainerBuilder $container): Configuration
diff --git a/src/Bundle/Resources/config/services/field_types.xml b/src/Bundle/Resources/config/services/field_types.xml
index 5b535898..38c5bec2 100644
--- a/src/Bundle/Resources/config/services/field_types.xml
+++ b/src/Bundle/Resources/config/services/field_types.xml
@@ -17,6 +17,7 @@
+
diff --git a/src/Bundle/Tests/Functional/GridUiTest.php b/src/Bundle/Tests/Functional/GridUiTest.php
index c6e6e9d3..6a5373d5 100644
--- a/src/Bundle/Tests/Functional/GridUiTest.php
+++ b/src/Bundle/Tests/Functional/GridUiTest.php
@@ -55,6 +55,19 @@ public function it_shows_authors_ids(): void
);
}
+ /** @test */
+ public function it_shows_authors_nationalities(): void
+ {
+ $this->client->request('GET', '/authors/?limit=100');
+
+ $nationalities = $this->getAuthorNationalitiesFromResponse();
+
+ $this->assertCount(3, array_unique($nationalities));
+ $this->assertContains('US', $nationalities);
+ $this->assertContains('EN', $nationalities);
+ $this->assertContains('', $nationalities);
+ }
+
/** @test */
public function it_sorts_authors_by_name_ascending_by_default(): void
{
@@ -313,6 +326,16 @@ private function getAuthorIdsFromResponse(): array
);
}
+ /** @return string[] */
+ private function getAuthorNationalitiesFromResponse(): array
+ {
+ return $this->getCrawler()
+ ->filter('[data-test-nationality]')
+ ->each(
+ fn (Crawler $node): string => $node->text(),
+ );
+ }
+
/** @return string[] */
private function getAuthorNamesFromResponse(): array
{
diff --git a/src/Component/Annotation/AsGridFieldCallableService.php b/src/Component/Annotation/AsGridFieldCallableService.php
new file mode 100644
index 00000000..9a086520
--- /dev/null
+++ b/src/Component/Annotation/AsGridFieldCallableService.php
@@ -0,0 +1,19 @@
+dataExtractor->get($field, $data);
- $value = call_user_func($options['callable'], $value);
+ $value = call_user_func($this->getCallable($options), $value);
try {
$value = (string) $value;
@@ -46,11 +53,45 @@ public function render(Field $field, $data, array $options): string
return $value;
}
+ private function getCallable(array $options): callable
+ {
+ if (isset($options['callable'])) {
+ return $options['callable'];
+ }
+
+ if (!$this->locator->has($options['service'])) {
+ throw new \RuntimeException(sprintf('Service "%s" not found, make sure it is tagged with "sylius.grid_field_callable_service".', $options['service']));
+ }
+
+ $service = $this->locator->get($options['service']);
+ if (isset($options['method'])) {
+ $callable = [$service, $options['method']];
+
+ if (!is_callable($callable)) {
+ throw new \RuntimeException(sprintf('The method "%s" is not callable on service "%s".', $options['method'], $options['service']));
+ }
+
+ return $callable;
+ }
+
+ if (!is_callable($service)) {
+ throw new \RuntimeException(sprintf('The service "%s" is not callable.', $options['service']));
+ }
+
+ return $service;
+ }
+
public function configureOptions(OptionsResolver $resolver): void
{
- $resolver->setRequired('callable');
+ $resolver->setDefined('callable');
$resolver->setAllowedTypes('callable', 'callable');
+ $resolver->setDefined('service');
+ $resolver->setAllowedTypes('service', 'string');
+
+ $resolver->setDefined('method');
+ $resolver->setAllowedTypes('method', 'string');
+
$resolver->setDefault('htmlspecialchars', true);
$resolver->setAllowedTypes('htmlspecialchars', 'bool');
}
diff --git a/src/Component/spec/FieldTypes/CallableFieldTypeSpec.php b/src/Component/spec/FieldTypes/CallableFieldTypeSpec.php
index 0123d21d..a20e47d1 100644
--- a/src/Component/spec/FieldTypes/CallableFieldTypeSpec.php
+++ b/src/Component/spec/FieldTypes/CallableFieldTypeSpec.php
@@ -18,12 +18,30 @@
use Sylius\Component\Grid\Definition\Field;
use Sylius\Component\Grid\Exception\UnexpectedValueException;
use Sylius\Component\Grid\FieldTypes\FieldTypeInterface;
+use Symfony\Contracts\Service\ServiceLocatorTrait;
+use Symfony\Contracts\Service\ServiceProviderInterface;
final class CallableFieldTypeSpec extends ObjectBehavior
{
function let(DataExtractorInterface $dataExtractor): void
{
- $this->beConstructedWith($dataExtractor);
+ $this->beConstructedWith(
+ $dataExtractor,
+ new class(['my_service' => fn () => new class() {
+ public function __invoke(string $value): string
+ {
+ return strtoupper($value);
+ }
+
+ public function concatenate(array $value): string
+ {
+ return implode(', ', $value);
+ }
+ },
+ ]) implements ServiceProviderInterface {
+ use ServiceLocatorTrait;
+ },
+ );
}
function it_is_a_grid_field_type(): void
@@ -79,6 +97,31 @@ function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_static_callabl
])->shouldReturn('bar');
}
+ function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_service(
+ DataExtractorInterface $dataExtractor,
+ Field $field,
+ ): void {
+ $dataExtractor->get($field, ['foo' => 'bar'])->willReturn('bar');
+
+ $this->render($field, ['foo' => 'bar'], [
+ 'service' => 'my_service',
+ 'htmlspecialchars' => true,
+ ])->shouldReturn('BAR');
+ }
+
+ function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_service_and_method(
+ DataExtractorInterface $dataExtractor,
+ Field $field,
+ ): void {
+ $dataExtractor->get($field, ['foo' => ['foo', 'bar', 'foobar']])->willReturn(['foo', 'bar', 'foobar']);
+
+ $this->render($field, ['foo' => ['foo', 'bar', 'foobar']], [
+ 'service' => 'my_service',
+ 'method' => 'concatenate',
+ 'htmlspecialchars' => true,
+ ])->shouldReturn('foo, bar, foobar');
+ }
+
function it_throws_an_exception_when_a_callable_return_value_cannot_be_casted_to_string(
DataExtractorInterface $dataExtractor,
Field $field,
@@ -98,6 +141,35 @@ function it_throws_an_exception_when_a_callable_return_value_cannot_be_casted_to
]);
}
+ function it_throws_an_exception_when_neither_callable_nor_service_options_are_defined(
+ DataExtractorInterface $dataExtractor,
+ Field $field,
+ ): void {
+ $this
+ ->shouldThrow(\RuntimeException::class)
+ ->during('render', [
+ $field,
+ ['foo' => 'bar'],
+ [],
+ ]);
+ }
+
+ function it_throws_an_exception_when_both_callable_and_service_options_are_defined(
+ DataExtractorInterface $dataExtractor,
+ Field $field,
+ ): void {
+ $this
+ ->shouldThrow(\RuntimeException::class)
+ ->during('render', [
+ $field,
+ ['foo' => 'bar'],
+ [
+ 'callable' => fn () => new \stdclass(),
+ 'service' => 'my_service',
+ ],
+ ]);
+ }
+
static function callable(mixed $value): string
{
return strtolower($value);
diff --git a/tests/Application/config/services.yaml b/tests/Application/config/services.yaml
index 99d572df..4c410a97 100644
--- a/tests/Application/config/services.yaml
+++ b/tests/Application/config/services.yaml
@@ -30,3 +30,5 @@ services:
App\BoardGameBlog\:
resource: '../src/BoardGameBlog'
+
+ App\Helper\GridHelper: ~
diff --git a/tests/Application/config/sylius/grids.yaml b/tests/Application/config/sylius/grids.yaml
index 60d29d59..6670740e 100644
--- a/tests/Application/config/sylius/grids.yaml
+++ b/tests/Application/config/sylius/grids.yaml
@@ -79,11 +79,14 @@ sylius_grid:
label: Name
sortable: ~
nationality:
- type: string
+ type: callable
+ options:
+ service: 'App\Helper\GridHelper'
+ method: 'formatNationality'
label: Name
sortable: nationality.name
path: nationality
- limits: [10, 5, 15]
+ limits: [10, 5, 15, 100]
app_book_by_american_authors:
driver:
diff --git a/tests/Application/config/sylius/grids/author.php b/tests/Application/config/sylius/grids/author.php
index e75264f7..b3f8b8a5 100644
--- a/tests/Application/config/sylius/grids/author.php
+++ b/tests/Application/config/sylius/grids/author.php
@@ -32,10 +32,10 @@
->setSortable(true),
)
->addField(
- StringField::create('nationality')
+ CallableField::createForService('nationality', \App\Helper\GridHelper::class)
->setLabel('Nationality')
->setSortable(true, 'nationality.name'),
)
- ->setLimits([10, 5, 15]),
+ ->setLimits([10, 5, 15, 100]),
);
};
diff --git a/tests/Application/src/Grid/AuthorGrid.php b/tests/Application/src/Grid/AuthorGrid.php
index f779f75b..52588b11 100644
--- a/tests/Application/src/Grid/AuthorGrid.php
+++ b/tests/Application/src/Grid/AuthorGrid.php
@@ -55,12 +55,12 @@ public function buildGrid(GridBuilderInterface $gridBuilder): void
->setSortable(true),
)
->addField(
- StringField::create('nationality')
+ CallableField::createForService('nationality', \App\Helper\GridHelper::class, 'formatNationality')
->setLabel('Name')
->setPath('nationality')
->setSortable(true, 'nationality.name'),
)
- ->setLimits([10, 5, 15])
+ ->setLimits([10, 5, 15, 100])
;
}
}
diff --git a/tests/Application/src/Helper/GridHelper.php b/tests/Application/src/Helper/GridHelper.php
index b4761edf..fcc35609 100644
--- a/tests/Application/src/Helper/GridHelper.php
+++ b/tests/Application/src/Helper/GridHelper.php
@@ -13,8 +13,25 @@
namespace App\Helper;
+use Sylius\Component\Grid\Annotation\AsGridFieldCallableService;
+
+#[AsGridFieldCallableService]
final class GridHelper
{
+ public function __invoke(?string $value): string
+ {
+ return $this->formatNationality($value);
+ }
+
+ public function formatNationality(?string $value): string
+ {
+ return match ($value) {
+ 'English' => 'EN',
+ 'American' => 'US',
+ null => '',
+ };
+ }
+
public static function addHashPrefix(string $value): string
{
return '#' . $value;