diff --git a/composer.json b/composer.json
index befba289..946bea89 100644
--- a/composer.json
+++ b/composer.json
@@ -35,6 +35,7 @@
"symfony/dependency-injection": "^5.4 || ^6.4 || ^7.0",
"symfony/deprecation-contracts": "^2.2 || ^3.1",
"symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0",
+ "symfony/expression-language": "^5.4 || ^6.0 || ^7.0",
"symfony/form": "^5.4 || ^6.4 || ^7.0",
"symfony/framework-bundle": "^5.4 || ^6.4 || ^7.0",
"symfony/http-kernel": "^5.4 || ^6.4 || ^7.0",
@@ -97,8 +98,8 @@
},
"autoload-dev": {
"psr-4": {
- "Sylius\\Bundle\\GridBundle\\spec\\": "src/Bundle/spec/",
- "Sylius\\Component\\Grid\\spec\\": "src/Component/spec/",
+ "spec\\Sylius\\Bundle\\GridBundle\\": "src/Bundle/spec/",
+ "spec\\Sylius\\Component\\Grid\\": "src/Component/spec/",
"App\\": "tests/Application/src/",
"AppBundle\\": "src/Bundle/test/src/AppBundle/",
"App\\Tests\\Tmp\\": "tests/Application/tmp/"
diff --git a/docs/field_types.md b/docs/field_types.md
index f12d664d..80a83da9 100644
--- a/docs/field_types.md
+++ b/docs/field_types.md
@@ -319,3 +319,80 @@ $field->setOptions([
// Your options here
]);
```
+
+Expression
+----------
+
+The **Expression** column provides flexibility by allowing you to specify an expression that will be evaluated on the fly.
+This feature uses Symfony's expression language, making it versatile and powerful without needing to create additional callbacks or templates.
+
+The expression will be evaluated with the following context:
+
+- `value`: The value of the row.
+
+You can also control whether special characters in the output are escaped by setting the `htmlspecialchars` option.
+By default, this is enabled, but you can disable it if the output contains HTML elements that should render as HTML.
+
+Yaml
+
+```yaml
+# config/packages/sylius_grid.yaml
+
+sylius_grid:
+ grids:
+ app_user:
+ fields:
+ price:
+ type: expression
+ label: app.ui.price
+ options:
+ expression: 'value ~ "$"'
+
+ role:
+ type: expression
+ label: app.ui.price
+ options:
+ expression: '"" ~ value ~ ""'
+ htmlspecialchars: false
+
+ most_exensive_order_total:
+ type: expression
+ label: app.ui.most_exensive_order_total
+ options:
+ expression: 'container.get("sylius.repository.order").findMostExpensiveOrder(value).getTotal()'
+```
+
+
+
+PHP
+
+```php
+addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
+ ->addField(
+ ExpressionField::create('price', 'value ~ "$"')
+ ->setLabel('app.ui.price')
+ )
+ ->addField(
+ ExpressionField::create('role', '"" ~ value ~ ""', htmlspecialchars: false)
+ ->setLabel('app.ui.role')
+ )
+ ->addField(
+ ExpressionField::create(
+ 'most_expensive_order_total',
+ 'container.get("sylius.repository.order").findMostExpensiveOrder(value).getTotal()',
+ )
+ ->setLabel('app.ui.most_exensive_order_total')
+ )
+ );
+};
+```
+
+
diff --git a/src/Bundle/Builder/Field/ExpressionField.php b/src/Bundle/Builder/Field/ExpressionField.php
new file mode 100644
index 00000000..03fd93f3
--- /dev/null
+++ b/src/Bundle/Builder/Field/ExpressionField.php
@@ -0,0 +1,28 @@
+setOption('expression', $expression)
+ ->setOption('htmlspecialchars', $htmlspecialchars)
+ ;
+ }
+}
diff --git a/src/Bundle/DependencyInjection/SyliusGridExtension.php b/src/Bundle/DependencyInjection/SyliusGridExtension.php
index 493e1dab..e23fd320 100644
--- a/src/Bundle/DependencyInjection/SyliusGridExtension.php
+++ b/src/Bundle/DependencyInjection/SyliusGridExtension.php
@@ -16,9 +16,12 @@
use Sylius\Bundle\CurrencyBundle\SyliusCurrencyBundle;
use Sylius\Bundle\GridBundle\Grid\GridInterface;
use Sylius\Bundle\GridBundle\SyliusGridBundle;
+use Sylius\Component\Grid\Attribute\AsExpressionProvider;
+use Sylius\Component\Grid\Attribute\AsExpressionVariables;
use Sylius\Component\Grid\Data\DataProviderInterface;
use Sylius\Component\Grid\Filtering\FilterInterface;
use Symfony\Component\Config\FileLocator;
+use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
@@ -67,6 +70,20 @@ public function load(array $configs, ContainerBuilder $container): void
$container->registerForAutoconfiguration(DataProviderInterface::class)
->addTag('sylius.grid_data_provider')
;
+
+ $container->registerAttributeForAutoconfiguration(
+ AsExpressionVariables::class,
+ static function (ChildDefinition $definition, AsExpressionVariables $attribute, \Reflector $reflector): void {
+ $definition->addTag(AsExpressionVariables::SERVICE_TAG);
+ },
+ );
+
+ $container->registerAttributeForAutoconfiguration(
+ AsExpressionProvider::class,
+ static function (ChildDefinition $definition, AsExpressionProvider $attribute, \Reflector $reflector): void {
+ $definition->addTag(AsExpressionProvider::SERVICE_TAG);
+ },
+ );
}
public function getConfiguration(array $config, ContainerBuilder $container): Configuration
diff --git a/src/Bundle/Resources/config/services.xml b/src/Bundle/Resources/config/services.xml
index b281b1f3..853b82d4 100644
--- a/src/Bundle/Resources/config/services.xml
+++ b/src/Bundle/Resources/config/services.xml
@@ -13,6 +13,7 @@
+
diff --git a/src/Bundle/Resources/config/services/expression_language.xml b/src/Bundle/Resources/config/services/expression_language.xml
new file mode 100644
index 00000000..b5c23505
--- /dev/null
+++ b/src/Bundle/Resources/config/services/expression_language.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Bundle/Resources/config/services/field_types.xml b/src/Bundle/Resources/config/services/field_types.xml
index cd5c4199..26a8c3ee 100644
--- a/src/Bundle/Resources/config/services/field_types.xml
+++ b/src/Bundle/Resources/config/services/field_types.xml
@@ -22,6 +22,13 @@
+
+
+
+
+
+
+
diff --git a/src/Bundle/Tests/Functional/GridUiTest.php b/src/Bundle/Tests/Functional/GridUiTest.php
index 627768e1..a4baa596 100644
--- a/src/Bundle/Tests/Functional/GridUiTest.php
+++ b/src/Bundle/Tests/Functional/GridUiTest.php
@@ -101,6 +101,19 @@ public function it_filters_books_by_title(): void
$this->assertSame('Book 5', $titles[0]);
}
+ /** @test */
+ public function it_shows_books_prices(): void
+ {
+ $this->client->request('GET', '/books/');
+
+ $prices = $this->getBookPriceFromResponse();
+
+ $this->assertListContainsOnly($prices, [
+ '42 €',
+ '10 £',
+ ]);
+ }
+
/** @test */
public function it_filters_books_by_title_with_contains(): void
{
@@ -274,6 +287,16 @@ private function getBookAuthorNationalitiesFromResponse(): array
);
}
+ /** @return string[] */
+ private function getBookPriceFromResponse(): array
+ {
+ return $this->getCrawler()
+ ->filter('[data-test-price]')
+ ->each(
+ fn (Crawler $node): string => $node->text(),
+ );
+ }
+
/** @return string[] */
private function getAuthorNamesFromResponse(): array
{
@@ -293,4 +316,14 @@ protected function buildMatcher(): Matcher
{
return $this->matcherFactory->createMatcher(new VoidBacktrace());
}
+
+ private function assertListContainsOnly(array $list, array $allowedValues)
+ {
+ foreach ($list as $item) {
+ $this->assertTrue(
+ in_array($item, $allowedValues, true),
+ "Item '$item' is not in the allowed list.",
+ );
+ }
+ }
}
diff --git a/src/Bundle/Tests/Provider/ServiceGridProviderTest.php b/src/Bundle/Tests/Provider/ServiceGridProviderTest.php
index 16bfe5e1..dfccc78f 100644
--- a/src/Bundle/Tests/Provider/ServiceGridProviderTest.php
+++ b/src/Bundle/Tests/Provider/ServiceGridProviderTest.php
@@ -35,6 +35,7 @@ public function test_grids_inheritance(): void
$this->assertEquals([
'title',
'author',
+ 'price',
'id',
], array_keys($gridDefinition->getFields()));
diff --git a/src/Component/Attribute/AsExpressionProvider.php b/src/Component/Attribute/AsExpressionProvider.php
new file mode 100644
index 00000000..3c07ea36
--- /dev/null
+++ b/src/Component/Attribute/AsExpressionProvider.php
@@ -0,0 +1,20 @@
+expressionLanguage->evaluate(
+ $expression,
+ array_merge(
+ $this->variablesCollection->getCollection(),
+ $variables,
+ ),
+ );
+ }
+}
diff --git a/src/Component/ExpressionLanguage/ExpressionEvaluatorInterface.php b/src/Component/ExpressionLanguage/ExpressionEvaluatorInterface.php
new file mode 100644
index 00000000..e190f402
--- /dev/null
+++ b/src/Component/ExpressionLanguage/ExpressionEvaluatorInterface.php
@@ -0,0 +1,22 @@
+ $providers */
+ public function __construct(private $providers)
+ {
+ Assert::allIsInstanceOf($this->providers, ExpressionFunctionProviderInterface::class);
+ }
+
+ public function __invoke(): ExpressionLanguage
+ {
+ $expressionLanguage = new ExpressionLanguage();
+
+ foreach ($this->providers as $provider) {
+ $expressionLanguage->registerProvider($provider);
+ }
+
+ return $expressionLanguage;
+ }
+}
diff --git a/src/Component/ExpressionLanguage/VariablesCollectionAggregate.php b/src/Component/ExpressionLanguage/VariablesCollectionAggregate.php
new file mode 100644
index 00000000..0dd49bcd
--- /dev/null
+++ b/src/Component/ExpressionLanguage/VariablesCollectionAggregate.php
@@ -0,0 +1,38 @@
+ $variablesCollection */
+ public function __construct(private $variablesCollection)
+ {
+ Assert::allIsInstanceOf($this->variablesCollection, VariablesCollectionInterface::class);
+ }
+
+ public function getCollection(): array
+ {
+ $collections = [];
+ foreach ($this->variablesCollection as $collection) {
+ $collections[] = $collection->getCollection();
+ }
+
+ return array_merge(...$collections);
+ }
+}
diff --git a/src/Component/ExpressionLanguage/VariablesCollectionInterface.php b/src/Component/ExpressionLanguage/VariablesCollectionInterface.php
new file mode 100644
index 00000000..05dc2dad
--- /dev/null
+++ b/src/Component/ExpressionLanguage/VariablesCollectionInterface.php
@@ -0,0 +1,25 @@
+
+ */
+ public function getCollection(): array;
+}
diff --git a/src/Component/FieldTypes/ExpressionFieldType.php b/src/Component/FieldTypes/ExpressionFieldType.php
new file mode 100644
index 00000000..614eeb0c
--- /dev/null
+++ b/src/Component/FieldTypes/ExpressionFieldType.php
@@ -0,0 +1,50 @@
+expressionEvaluator->evaluateExpression($options['expression'], [
+ 'value' => $this->dataExtractor->get($field, $data),
+ ]);
+
+ if ($options['htmlspecialchars']) {
+ $value = htmlspecialchars($value);
+ }
+
+ return $value;
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setRequired('expression');
+ $resolver->setAllowedTypes('expression', 'string');
+
+ $resolver->setDefault('htmlspecialchars', true);
+ $resolver->setAllowedTypes('htmlspecialchars', 'bool');
+ }
+}
diff --git a/src/Component/composer.json b/src/Component/composer.json
index e649542f..0204448d 100644
--- a/src/Component/composer.json
+++ b/src/Component/composer.json
@@ -34,6 +34,7 @@
"symfony/config": "^5.4 || ^6.4 || ^7.0",
"symfony/deprecation-contracts": "^2.2",
"symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0",
+ "symfony/expression-language": "^5.4 || ^6.0 || ^7.0",
"symfony/http-kernel": "^5.4 || ^6.4 || ^7.0",
"symfony/options-resolver": "^5.4 || ^6.0 || ^7.0",
"webmozart/assert": "^1.9"
@@ -54,7 +55,7 @@
},
"autoload-dev": {
"psr-4": {
- "Sylius\\Component\\Grid\\spec\\": "spec/"
+ "spec\\Sylius\\Component\\Grid\\": "spec/"
}
}
}
diff --git a/src/Component/spec/ExpressionLanguage/ArrayVariablesCollectionStub.php b/src/Component/spec/ExpressionLanguage/ArrayVariablesCollectionStub.php
new file mode 100644
index 00000000..96bc91b2
--- /dev/null
+++ b/src/Component/spec/ExpressionLanguage/ArrayVariablesCollectionStub.php
@@ -0,0 +1,28 @@
+variables;
+ }
+}
diff --git a/src/Component/spec/ExpressionLanguage/ExpressionEvaluatorSpec.php b/src/Component/spec/ExpressionLanguage/ExpressionEvaluatorSpec.php
new file mode 100644
index 00000000..cda83f47
--- /dev/null
+++ b/src/Component/spec/ExpressionLanguage/ExpressionEvaluatorSpec.php
@@ -0,0 +1,45 @@
+beConstructedWith(
+ new ExpressionLanguage(),
+ new ArrayVariablesCollectionStub([
+ 'foo' => 69,
+ ]),
+ );
+ }
+
+ function it_evaluate_simple_expression(): void
+ {
+ $this->evaluateExpression('1 + 2')->shouldReturn(3);
+ }
+
+ function it_evaluate_simple_expression_with_variable(): void
+ {
+ $this->evaluateExpression('1 + 2 + bar', ['bar' => 10])->shouldReturn(13);
+ }
+
+ function it_evaluate_simple_expression_with_variable_from_variable_collection(): void
+ {
+ $this->evaluateExpression('foo ~ " lyon"')->shouldReturn('69 lyon');
+ }
+}
diff --git a/src/Component/spec/FieldTypes/ExpressionFieldTypeSpec.php b/src/Component/spec/FieldTypes/ExpressionFieldTypeSpec.php
new file mode 100644
index 00000000..808650be
--- /dev/null
+++ b/src/Component/spec/FieldTypes/ExpressionFieldTypeSpec.php
@@ -0,0 +1,66 @@
+beConstructedWith(
+ $dataExtractor,
+ new ExpressionEvaluator(
+ new ExpressionLanguage(),
+ new ArrayVariablesCollectionStub([]),
+ ),
+ );
+ }
+
+ function it_is_a_grid_field_type(): void
+ {
+ $this->shouldImplement(FieldTypeInterface::class);
+ }
+
+ function it_uses_data_extractor_to_obtain_data_and_evaluates_it_with_htmlspecialchars(
+ DataExtractorInterface $dataExtractor,
+ Field $field,
+ ): void {
+ $dataExtractor->get($field, ['foo' => 5])->willReturn(5);
+
+ $this->render($field, ['foo' => 5], [
+ 'expression' => '"" ~ value * 100 ~ "$"',
+ 'htmlspecialchars' => true,
+ ])->shouldReturn('<strong>500$</strong>');
+ }
+
+ function it_uses_data_extractor_to_obtain_data_and_evaluates_it_without_htmlspecialchars(
+ DataExtractorInterface $dataExtractor,
+ Field $field,
+ ): void {
+ $dataExtractor->get($field, ['foo' => 5])->willReturn(5);
+
+ $this->render($field, ['foo' => 5], [
+ 'expression' => '"" ~ value * 100 ~ "$"',
+ 'htmlspecialchars' => false,
+ ])->shouldReturn('500$');
+ }
+}
diff --git a/tests/Application/config/services.yaml b/tests/Application/config/services.yaml
index 8853a837..d8d9e5bc 100644
--- a/tests/Application/config/services.yaml
+++ b/tests/Application/config/services.yaml
@@ -29,3 +29,6 @@ services:
App\BoardGameBlog\:
resource: '../src/BoardGameBlog'
+
+ App\ExpressionLanguage\Variables: ~
+ App\ExpressionLanguage\Provider: ~
diff --git a/tests/Application/config/sylius/grids.yaml b/tests/Application/config/sylius/grids.yaml
index beb008f6..092c5589 100644
--- a/tests/Application/config/sylius/grids.yaml
+++ b/tests/Application/config/sylius/grids.yaml
@@ -46,6 +46,11 @@ sylius_grid:
label: Nationality
path: author.nationality.name
sortable: author.nationality.name
+ price:
+ type: expression
+ label: Price
+ options:
+ expression: 'value.getAmount() / cents_per_unit ~ " " ~ get_currency_symbol(value.getCurrencyCode())'
limits: [10, 5, 15]
app_author:
diff --git a/tests/Application/config/sylius/grids/book.php b/tests/Application/config/sylius/grids/book.php
index 460521a3..acdf78c6 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\ExpressionField;
use Sylius\Bundle\GridBundle\Builder\Field\StringField;
use Sylius\Bundle\GridBundle\Builder\Filter\EntityFilter;
use Sylius\Bundle\GridBundle\Builder\Filter\SelectFilter;
@@ -64,6 +65,10 @@
->setPath('author.nationality.name')
->setSortable(true, 'author.nationality.name'),
)
+ ->addField(
+ ExpressionField::create('price', 'value.getAmount() / cents_per_unit ~ " " ~ get_currency_symbol(value.getCurrencyCode())')
+ ->setLabel('Price'),
+ )
->setLimits([10, 5, 15]),
);
};
diff --git a/tests/Application/src/ExpressionLanguage/Provider.php b/tests/Application/src/ExpressionLanguage/Provider.php
new file mode 100644
index 00000000..13ea8980
--- /dev/null
+++ b/tests/Application/src/ExpressionLanguage/Provider.php
@@ -0,0 +1,32 @@
+ null, fn ($arguments, string $code): string => match ($code) {
+ 'EUR' => '€',
+ 'GBP' => '£',
+ }),
+ ];
+ }
+}
diff --git a/tests/Application/src/ExpressionLanguage/Variables.php b/tests/Application/src/ExpressionLanguage/Variables.php
new file mode 100644
index 00000000..f5a201b7
--- /dev/null
+++ b/tests/Application/src/ExpressionLanguage/Variables.php
@@ -0,0 +1,28 @@
+ 100,
+ ];
+ }
+}
diff --git a/tests/Application/src/Grid/BookGrid.php b/tests/Application/src/Grid/BookGrid.php
index 9c0ee24c..1094b1ef 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\ExpressionField;
use Sylius\Bundle\GridBundle\Builder\Field\StringField;
use Sylius\Bundle\GridBundle\Builder\Filter\Filter;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
@@ -110,6 +111,10 @@ public function buildGrid(GridBuilderInterface $gridBuilder): void
DeleteAction::create(),
),
)
+ ->addField(
+ ExpressionField::create('price', 'value.getAmount() / cents_per_unit ~ " " ~ get_currency_symbol(value.getCurrencyCode())')
+ ->setLabel('Price'),
+ )
->setLimits([10, 5, 15])
;
}