Skip to content

Commit

Permalink
add expression field
Browse files Browse the repository at this point in the history
  • Loading branch information
Florian-Merle committed Nov 22, 2024
1 parent d654fe3 commit 7589179
Show file tree
Hide file tree
Showing 29 changed files with 714 additions and 3 deletions.
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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/"
Expand Down
37 changes: 37 additions & 0 deletions docs/expression_language.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
Expression Language
===================

The Sylius Grid Bundle utilizes the Symfony `ExpressionLanguage` component to provide a flexible and powerful way to evaluate expressions.

Evaluating Expressions
----------------------

Expressions can be evaluated using the `sylius_grid.expression_language.expression_evaluator` service, which is an instance of `ExpressionEvaluator`.

The `ExpressionEvaluator` uses both an instance of `ExpressionLanguage` and a `VariablesCollectionInterface` (`sylius_grid.expression_language.variables_collection_aggregate`). This ensures that the evaluator has access to a collection of variables that developers can add.

To evaluate an expression:
- The `evaluateExpression` method is called with the expression string and an optional array of variables.
- The evaluator merges the provided variables with the collection of variables provided by the `VariablesCollectionAggregate` mentioned earlier.
- The expression is then evaluated with all the variables.

**Note that:**
- The `ExpressionLanguage` can be extended using expression providers.
- `sylius_grid.expression_language.variables_collection_aggregate` can be extended using custom variables collections.

Extending the Expression Language
---------------------------------

### Adding Expression Providers

The Symfony `ExpressionLanguage` component can be extended with functions using expression providers. To create a custom expression provider:

1. Your class should implement `Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface`.
2. Tag your service with `sylius.grid.provider`. You can automate this by using the `Sylius\Component\Grid\Attribute\AsExpressionProvider` attribute.

### Adding Custom Variables

Adding custom variables is done by creating a new variables collection:

1. Create a class that implements `Sylius\Component\Grid\ExpressionLanguage\VariablesCollectionInterface`.
2. Tag your service with `sylius.grid.variables`. You can automate this by using the `Sylius\Component\Grid\Attribute\AsExpressionVariables` attribute.
79 changes: 79 additions & 0 deletions docs/field_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,82 @@ $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.

If you need to access specific variables or extend the functions available in the expression, see [expression language](expression_language.md).

<details open><summary>Yaml</summary>

```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: '"<strong>" ~ value ~ "</strong>"'
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()'
```
</details>
<details open><summary>PHP</summary>
```php
<?php
// config/packages/sylius_grid.php

use Sylius\Bundle\GridBundle\Builder\Field\ExpressionField;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
$grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
->addField(
ExpressionField::create('price', 'value ~ "$"')
->setLabel('app.ui.price')
)
->addField(
ExpressionField::create('role', '"<strong>" ~ value ~ "</strong>"', 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')
)
);
};
```

</details>
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Menu
* [Creating your First Grid](your_first_grid.md)
* [Configuring Fields](field_configuration.md)
* [Field types](field_types.md)
* [Expression language](expression_language.md)
* [Creating custom Field type](custom_field_type.md)
* [Creating custom Action](custom_action.md)
* [Creating custom Bulk Action](custom_bulk_action.md)
Expand Down
28 changes: 28 additions & 0 deletions src/Bundle/Builder/Field/ExpressionField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Sylius\Bundle\GridBundle\Builder\Field;

final class ExpressionField
{
public static function create(
string $name,
string $expression,
bool $htmlspecialchars = true,
): FieldInterface {
return Field::create($name, 'expression')
->setOption('expression', $expression)
->setOption('htmlspecialchars', $htmlspecialchars)
;
}
}
17 changes: 17 additions & 0 deletions src/Bundle/DependencyInjection/SyliusGridExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/Bundle/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<imports>
<import resource="services/expression_language.xml" />
<import resource="services/field_types.xml" />
<import resource="services/filters.xml" />
<import resource="services/templating.xml" />
Expand Down
33 changes: 33 additions & 0 deletions src/Bundle/Resources/config/services/expression_language.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>

<!--
This file is part of the Sylius package.
(c) Sylius Sp. z o.o.
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
-->

<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="sylius_grid.expression_language.factory" class="Sylius\Component\Grid\ExpressionLanguage\ExpressionLanguageFactory">
<argument type="tagged_iterator" tag="sylius.grid.provider" />
</service>

<service id="sylius_grid.expression_language" class="Symfony\Component\ExpressionLanguage\ExpressionLanguage">
<factory service="sylius_grid.expression_language.factory" />
</service>

<service id="sylius_grid.expression_language.expression_evaluator" class="Sylius\Component\Grid\ExpressionLanguage\ExpressionEvaluator">
<argument type="service" id="sylius_grid.expression_language" />
<argument type="service" id="sylius_grid.expression_language.variables_collection_aggregate" />
</service>

<service id="sylius_grid.expression_language.variables_collection_aggregate" class="Sylius\Component\Grid\ExpressionLanguage\VariablesCollectionAggregate">
<argument type="tagged_iterator" tag="sylius.grid.variables" />
</service>
</services>
</container>
7 changes: 7 additions & 0 deletions src/Bundle/Resources/config/services/field_types.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
</service>
<service id="sylius.grid_field.datetime" alias="Sylius\Component\Grid\FieldTypes\DatetimeFieldType" />

<service id="Sylius\Component\Grid\FieldTypes\ExpressionFieldType">
<argument type="service" id="sylius.grid.data_extractor" />
<argument type="service" id="sylius_grid.expression_language.expression_evaluator" />
<tag name="sylius.grid_field" type="expression" />
</service>
<service id="sylius.grid_field.expression" alias="Sylius\Component\Grid\FieldTypes\ExpressionFieldType" />

<service id="Sylius\Component\Grid\FieldTypes\StringFieldType">
<argument type="service" id="sylius.grid.data_extractor" />
<tag name="sylius.grid_field" type="string" />
Expand Down
33 changes: 33 additions & 0 deletions src/Bundle/Tests/Functional/GridUiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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
{
Expand All @@ -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.",
);
}
}
}
1 change: 1 addition & 0 deletions src/Bundle/Tests/Provider/ServiceGridProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public function test_grids_inheritance(): void
$this->assertEquals([
'title',
'author',
'price',
'id',
], array_keys($gridDefinition->getFields()));

Expand Down
20 changes: 20 additions & 0 deletions src/Component/Attribute/AsExpressionProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Sylius\Component\Grid\Attribute;

#[\Attribute(\Attribute::TARGET_CLASS)]
final class AsExpressionProvider
{
public const SERVICE_TAG = 'sylius.grid.provider';
}
20 changes: 20 additions & 0 deletions src/Component/Attribute/AsExpressionVariables.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Sylius\Component\Grid\Attribute;

#[\Attribute(\Attribute::TARGET_CLASS)]
final class AsExpressionVariables
{
public const SERVICE_TAG = 'sylius.grid.variables';
}
Loading

0 comments on commit 7589179

Please sign in to comment.