Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add callback field #321

Open
wants to merge 2 commits into
base: 1.13
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions docs/field_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

<details open><summary>PHP</summary>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To match existing documentation, please add YAML example as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not include documentation for the YAML format because I believe it's impossible to define a callback field in this format.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feature is not complete then and cannot be merged until YAML support will be dropped.

Here is example on how callback can be implemented with non-php config format.

https://symfony.com/doc/current/reference/constraints/Callback.html#external-callbacks-and-closures

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can do it. The repository can be a callback in YAML.

app_book_by_english_authors:
    driver:
        name: doctrine/orm
        options:
            class: App\Entity\Book
            repository:
                method: [expr:service('app.english_books_query_builder'), create]


```php
<?php
// config/packages/sylius_grid.php

use Sylius\Bundle\GridBundle\Builder\Field\CallbackField;
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(
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 => "<strong>$status</strong>", 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
<?php
# src/Grid/UserGrid.php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\User;
use Sylius\Bundle\GridBundle\Builder\Field\CallbackField;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Bundle\GridBundle\Grid\ResourceAwareGridInterface;

final class UserGrid extends AbstractGrid implements ResourceAwareGridInterface
{
public static function getName(): string
{
return 'app_user';
}

public function buildGrid(GridBuilderInterface $gridBuilder): void
{
$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')
)
->addField(
CallbackField::create('status' fn (array $status): string => "<strong>$status</strong>", 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;
}
}
```

</details>

This configuration will display each role of a customer separated with a comma.

25 changes: 25 additions & 0 deletions src/Bundle/Builder/Field/CallbackField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?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 CallbackField
{
public static function create(string $name, callable $callback, bool $htmlspecialchars = true): FieldInterface
{
return Field::create($name, 'callback')
->setOption('callback', $callback)
->setOption('htmlspecialchars', $htmlspecialchars)
;
}
}
59 changes: 58 additions & 1 deletion src/Bundle/Renderer/TwigGridRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
namespace Sylius\Bundle\GridBundle\Renderer;

use Sylius\Bundle\GridBundle\Form\Registry\FormTypeRegistryInterface;
use Sylius\Bundle\ResourceBundle\ExpressionLanguage\ExpressionLanguage;
use Sylius\Component\Grid\Definition\Action;
use Sylius\Component\Grid\Definition\Field;
use Sylius\Component\Grid\Definition\Filter;
use Sylius\Component\Grid\FieldTypes\FieldTypeInterface;
use Sylius\Component\Grid\Renderer\GridRendererInterface;
use Sylius\Component\Grid\View\GridViewInterface;
use Sylius\Component\Registry\ServiceRegistryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
Expand All @@ -36,6 +38,10 @@ final class TwigGridRenderer implements GridRendererInterface

private FormTypeRegistryInterface $formTypeRegistry;

private ContainerInterface $container;

private ExpressionLanguage $expression;

private string $defaultTemplate;

private array $actionTemplates;
Expand All @@ -47,6 +53,8 @@ public function __construct(
ServiceRegistryInterface $fieldsRegistry,
FormFactoryInterface $formFactory,
FormTypeRegistryInterface $formTypeRegistry,
ContainerInterface $container,
ExpressionLanguage $expression,
string $defaultTemplate,
array $actionTemplates = [],
array $filterTemplates = [],
Expand All @@ -55,6 +63,8 @@ public function __construct(
$this->fieldsRegistry = $fieldsRegistry;
$this->formFactory = $formFactory;
$this->formTypeRegistry = $formTypeRegistry;
$this->container = $container;
$this->expression = $expression;
$this->defaultTemplate = $defaultTemplate;
$this->actionTemplates = $actionTemplates;
$this->filterTemplates = $filterTemplates;
Expand All @@ -71,11 +81,58 @@ 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->parseOptions($field->getOptions()));

return $fieldType->render($field, $data, $options);
}

private function parseOptions(array $parameters): array
{
return array_map(
/**
* @param mixed $parameter
*
* @return mixed
*/
function ($parameter) {
if (is_array($parameter)) {
return $this->parseOptions($parameter);
}

return $this->parseOption($parameter);
},
$parameters,
);
}

/**
* @param mixed $parameter
* @param array|object|null $data
*
* @return mixed
*/
private function parseOption($parameter)
{
if (!is_string($parameter)) {
return $parameter;
}

if (0 === strpos($parameter, 'expr:')) {
return $this->parseOptionExpression(substr($parameter, 5));
}

return $parameter;
}

/**
* @return mixed
*/
private function parseOptionExpression(string $expression)
{
return $this->expression->evaluate($expression, ['container' => $this->container]);
}

public function renderAction(GridViewInterface $gridView, Action $action, $data = null)
{
$type = $action->getType();
Expand Down
6 changes: 6 additions & 0 deletions src/Bundle/Resources/config/services/field_types.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
<services>
<defaults public="true" />

<service id="sylius.grid_field.callback" class="Sylius\Component\Grid\FieldTypes\CallbackFieldType">
<argument type="service" id="sylius.grid.data_extractor" />
<tag name="sylius.grid_field" type="callback" />
</service>
<service id="Sylius\Component\Grid\FieldTypes\CallbackFieldType" alias="sylius.grid_field.callback" />

<service id="sylius.grid_field.datetime" class="Sylius\Component\Grid\FieldTypes\DatetimeFieldType">
<argument type="service" id="sylius.grid.data_extractor" />
<argument>%sylius_grid.timezone%</argument>
Expand Down
2 changes: 2 additions & 0 deletions src/Bundle/Resources/config/services/twig.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
<argument type="service" id="sylius.registry.grid_field" />
<argument type="service" id="form.factory" />
<argument type="service" id="sylius.form_registry.grid_filter" />
<argument type="service" id="service_container" />
<argument type="service" id="sylius.expression_language" />
<argument>@SyliusGrid/_grid.html.twig</argument>
<argument>%sylius.grid.templates.action%</argument>
<argument>%sylius.grid.templates.filter%</argument>
Expand Down
12 changes: 6 additions & 6 deletions src/Bundle/Tests/Functional/GridUiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,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 */
Expand All @@ -112,7 +112,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 */
Expand All @@ -125,7 +125,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 */
Expand All @@ -139,7 +139,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 */
Expand All @@ -152,7 +152,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 */
Expand All @@ -165,7 +165,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 */
Expand Down
46 changes: 46 additions & 0 deletions src/Component/FieldTypes/CallbackFieldType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?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\FieldTypes;

use Sylius\Component\Grid\DataExtractor\DataExtractorInterface;
use Sylius\Component\Grid\Definition\Field;
use Symfony\Component\OptionsResolver\OptionsResolver;

final class CallbackFieldType implements FieldTypeInterface
{
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);

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');
}
}
Loading
Loading