diff --git a/docs/field_types.md b/docs/field_types.md
index 8741e31d..ee427a2e 100644
--- a/docs/field_types.md
+++ b/docs/field_types.md
@@ -312,3 +312,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 ad91127b..9e158497 100644
--- a/src/Bundle/Resources/config/services/field_types.xml
+++ b/src/Bundle/Resources/config/services/field_types.xml
@@ -15,6 +15,12 @@
+
+
+
+
+
+
diff --git a/src/Component/FieldTypes/CallbackFieldType.php b/src/Component/FieldTypes/CallbackFieldType.php
new file mode 100644
index 00000000..8eecfc81
--- /dev/null
+++ b/src/Component/FieldTypes/CallbackFieldType.php
@@ -0,0 +1,49 @@
+dataExtractor = $dataExtractor;
+ }
+
+ public function render(Field $field, $data, array $options): string
+ {
+ $value = $this->dataExtractor->get($field, $data);
+ $value = call_user_func($options['callback'], $value);
+
+ if ($options['htmlspecialchars'] !== true) {
+ return $value;
+ }
+
+ return htmlspecialchars((string) $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..954bc9ce
--- /dev/null
+++ b/src/Component/spec/FieldTypes/CallbackFieldTypeSpec.php
@@ -0,0 +1,97 @@
+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_closure_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($value)
+ {
+ return strtolower($value);
+ }
+}