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 callable field #321

Open
wants to merge 4 commits into
base: 1.14
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
115 changes: 115 additions & 0 deletions docs/field_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,118 @@ $field->setOptions([
// Your options here
]);
```

Callable
--------

The Callable 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 callable, which allows you to transform the 'data' variable on the fly.

When defining callables in YAML, only string representations of callables are supported.
When configuring grids using PHP (as opposed to service grid configuration), both string and array callables are supported. However, closures cannot be used due to restrictions in Symfony's configuration (values of type "Closure" are not permitted in service configuration files).
By contrast, when configuring grids with service definitions, you can use both callables and closures.

Here are some examples of what you can do:

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

```yaml
# config/packages/sylius_grid.yaml

sylius_grid:
grids:
app_user:
fields:
id:
type: callable
options:
callable: "callable:App\\Helper\\GridHelper::addHashPrefix"
label: app.ui.id
name:
type: callable
options:
callable: "callable:strtoupper"
label: app.ui.name
```

</details>

<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]

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 reworked the feature and introduced a new way to define callbacks, inspired by Loic's comment:

sylius_grid:
    grids:
        app_user:
            fields:
                name:
                    type: callback
                    options:
                        callback: "callback:strtoupper"
                    label: app.ui.name

This approach allows string callables to be defined directly in YAML.
When configuring grids using PHP, both string and array callables are supported.
When configuring grids using service definitions, everything is supported: string callables, array callables, and closures.

I’ve documented this change here: SyliusGridBundle PR #321.

To implement this, I copied the OptionParser class from the ResourceBundle, slimming it down to only what’s necessary for this feature (handling callbacks). See the original class: OptionsParser in ResourceBundle.

Since the resource bundle is not a required dependencie, I think this is fine to copy the class. However having two OptionParser might not be ideal. Please let me know what you think about it.


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

use Sylius\Bundle\GridBundle\Builder\Field\CallableField;
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(
CallableField::create('id', 'App\\Helper\\GridHelper::addHashPrefix')
->setLabel('app.ui.id')
)
// or
->addField(
CallableField::create('id', ['App\\Helper\\GridHelper', 'addHashPrefix'])
->setLabel('app.ui.id')
)

->addField(
CallableField::create('name', 'strtoupper')
->setLabel('app.ui.name')
)
)
};
```

OR

```php
<?php
# src/Grid/UserGrid.php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\User;
use Sylius\Bundle\GridBundle\Builder\Field\CallableField;
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(
CallableField::create('id', GridHelper::addHashPrefix(...))
->setLabel('app.ui.id')
)
->addField(
CallableField::create('name', 'strtoupper')
->setLabel('app.ui.name')
)
->addField(
CallableField::create('roles' fn (array $roles): string => implode(', ', $roles))
->setLabel('app.ui.roles')
)
;
}

public function getResourceClass(): string
{
return User::class;
}
}
```

</details>
6 changes: 6 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
parameters:
ignoreErrors:
-
message: "#^Dead catch \\- Throwable is never thrown in the try block\\.$#"
count: 1
path: src/Component/FieldTypes/CallableFieldType.php
2 changes: 2 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
includes:
- phpstan-baseline.neon

- vendor/phpstan/phpstan-webmozart-assert/extension.neon
- vendor/phpstan/phpstan-phpunit/extension.neon

Expand Down
25 changes: 25 additions & 0 deletions src/Bundle/Builder/Field/CallableField.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 CallableField
{
public static function create(string $name, callable $callable, bool $htmlspecialchars = true): FieldInterface
{
return Field::create($name, 'callable')
->setOption('callable', $callable)
->setOption('htmlspecialchars', $htmlspecialchars)
;
}
}
63 changes: 63 additions & 0 deletions src/Bundle/Parser/OptionsParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?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\Parser;

final class OptionsParser implements OptionsParserInterface
{
public 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
*
* @return mixed
*/
private function parseOption($parameter)
{
if (!is_string($parameter)) {
return $parameter;
}

if (0 === strpos($parameter, 'callable:')) {
return $this->parseOptionCallable(substr($parameter, 9));
}

return $parameter;
}

private function parseOptionCallable(string $callable): \Closure
{
if (!is_callable($callable)) {
throw new \RuntimeException(\sprintf('%s is not a callable.', $callable));
}

return $callable(...);
}
}
19 changes: 19 additions & 0 deletions src/Bundle/Parser/OptionsParserInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?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\Parser;

interface OptionsParserInterface
{
public function parseOptions(array $parameters): array;
}
22 changes: 21 additions & 1 deletion src/Bundle/Renderer/TwigGridRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace Sylius\Bundle\GridBundle\Renderer;

use Sylius\Bundle\GridBundle\Form\Registry\FormTypeRegistryInterface;
use Sylius\Bundle\GridBundle\Parser\OptionsParserInterface;
use Sylius\Component\Grid\Definition\Action;
use Sylius\Component\Grid\Definition\Field;
use Sylius\Component\Grid\Definition\Filter;
Expand Down Expand Up @@ -42,6 +43,8 @@ final class TwigGridRenderer implements GridRendererInterface

private array $filterTemplates;

private ?OptionsParserInterface $optionsParser;

public function __construct(
Environment $twig,
ServiceRegistryInterface $fieldsRegistry,
Expand All @@ -50,6 +53,7 @@ public function __construct(
string $defaultTemplate,
array $actionTemplates = [],
array $filterTemplates = [],
?OptionsParserInterface $optionsParser = null,
) {
$this->twig = $twig;
$this->fieldsRegistry = $fieldsRegistry;
Expand All @@ -58,6 +62,17 @@ public function __construct(
$this->defaultTemplate = $defaultTemplate;
$this->actionTemplates = $actionTemplates;
$this->filterTemplates = $filterTemplates;
$this->optionsParser = $optionsParser;

if (null === $optionsParser) {
trigger_deprecation(
'sylius/grid-bundle',
'1.14',
'Not passing an instance of "%s" as the eighth constructor argument of "%s" is deprecated.',
OptionsParserInterface::class,
self::class,
);
}
}

public function render(GridViewInterface $gridView, ?string $template = null)
Expand All @@ -71,7 +86,12 @@ 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 = $field->getOptions();
if (null !== $this->optionsParser) {
$options = $this->optionsParser->parseOptions($options);
}
$options = $resolver->resolve($options);

return $fieldType->render($field, $data, $options);
}
Expand Down
3 changes: 3 additions & 0 deletions src/Bundle/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -128,5 +128,8 @@
<tag name="maker.command" />
</service>
<service id="Sylius\Bundle\GridBundle\Maker\MakeGrid" alias="sylius.grid.maker" />

<service id="sylius.grid.options_parser" class="Sylius\Bundle\GridBundle\Parser\OptionsParser" />
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@GSadee Since the SyliusResourceBundle has already a service with a similar id (sylius.grid_options_parser), i'm not sure about what this service should be named. WDYT ?

<service id="Sylius\Bundle\GridBundle\Parser\OptionsParserInterface" alias="sylius.grid.options_parser" />
</services>
</container>
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.callable" class="Sylius\Component\Grid\FieldTypes\CallableFieldType">
<argument type="service" id="sylius.grid.data_extractor" />
<tag name="sylius.grid_field" type="callable" />
</service>
<service id="Sylius\Component\Grid\FieldTypes\CallableFieldType" alias="sylius.grid_field.callable" />

<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
1 change: 1 addition & 0 deletions src/Bundle/Resources/config/services/twig.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<argument>@SyliusGrid/_grid.html.twig</argument>
<argument>%sylius.grid.templates.action%</argument>
<argument>%sylius.grid.templates.filter%</argument>
<argument type="service" id="sylius.grid.options_parser" />
</service>
<service id="Sylius\Bundle\GridBundle\Renderer\TwigGridRenderer" alias="sylius.grid.renderer.twig" />

Expand Down
Loading
Loading