Skip to content

Commit f8f212f

Browse files
committed
make callback field compatible with yaml config
1 parent 7f9689b commit f8f212f

File tree

15 files changed

+283
-35
lines changed

15 files changed

+283
-35
lines changed

docs/field_types.md

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,34 @@ Callback
326326
The Callback column aims to offer almost as much flexibility as the Twig column, but without requiring the creation of a template.
327327
You simply need to specify a callback, which allows you to transform the 'data' variable on the fly.
328328

329-
By default it uses the name of the field, but you can specify the path
330-
alternatively. For example:
329+
When defining callbacks in YAML, only string representations of callables are supported.
330+
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).
331+
By contrast, when configuring grids with service definitions, you can use both callables and closures.
332+
333+
Here are some examples of what you can do:
334+
335+
<details open><summary>Yaml</summary>
336+
337+
```yaml
338+
# config/packages/sylius_grid.yaml
339+
340+
sylius_grid:
341+
grids:
342+
app_user:
343+
fields:
344+
id:
345+
type: callback
346+
options:
347+
callback: "callback:App\\Helper\\GridHelper::addHashPrefix"
348+
label: app.ui.id
349+
name:
350+
type: callback
351+
options:
352+
callback: "callback:strtoupper"
353+
label: app.ui.name
354+
```
355+
356+
</details>
331357
332358
<details open><summary>PHP</summary>
333359
@@ -342,14 +368,18 @@ use Sylius\Bundle\GridBundle\Config\GridConfig;
342368
return static function (GridConfig $grid): void {
343369
$grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
344370
->addField(
345-
CallbackField::create('roles' fn (array $roles): string => implode(', ', $roles))
346-
->setLabel('app.ui.roles') // # each filed type can have a label, we suggest using translation keys instead of messages
347-
->setPath('roles')
371+
CallbackField::create('id', 'App\\Helper\\GridHelper::addHashPrefix')
372+
->setLabel('app.ui.id')
348373
)
374+
// or
349375
->addField(
350-
CallbackField::create('status' fn (array $status): string => "<strong>$status</strong>", false) // the third argument allows to disable htmlspecialchars if set to false
351-
->setLabel('app.ui.status') // # each filed type can have a label, we suggest using translation keys instead of messages
352-
->setPath('status')
376+
CallbackField::create('id', ['App\\Helper\\GridHelper', 'addHashPrefix'])
377+
->setLabel('app.ui.id')
378+
)
379+
380+
->addField(
381+
CallbackField::create('name', 'strtoupper')
382+
->setLabel('app.ui.name')
353383
)
354384
)
355385
};
@@ -382,14 +412,16 @@ final class UserGrid extends AbstractGrid implements ResourceAwareGridInterface
382412
{
383413
$gridBuilder
384414
->addField(
385-
CallbackField::create('roles' fn (array $roles): string => implode(', ', $roles))
386-
->setLabel('app.ui.roles') // # each filed type can have a label, we suggest using translation keys instead of messages
387-
->setPath('roles')
415+
CallbackField::create('id', GridHelper::addHashPrefix(...))
416+
->setLabel('app.ui.id')
388417
)
389418
->addField(
390-
CallbackField::create('status' fn (array $status): string => "<strong>$status</strong>", false) // the third argument allows to disable htmlspecialchars if set to false
391-
->setLabel('app.ui.status') // # each filed type can have a label, we suggest using translation keys instead of messages
392-
->setPath('status')
419+
CallbackField::create('name', 'strtoupper')
420+
->setLabel('app.ui.name')
421+
)
422+
->addField(
423+
CallbackField::create('roles' fn (array $roles): string => implode(', ', $roles))
424+
->setLabel('app.ui.roles')
393425
)
394426
;
395427
}
@@ -402,6 +434,3 @@ final class UserGrid extends AbstractGrid implements ResourceAwareGridInterface
402434
```
403435

404436
</details>
405-
406-
This configuration will display each role of a customer separated with a comma.
407-

src/Bundle/Parser/OptionsParser.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Sylius\Bundle\GridBundle\Parser;
15+
16+
final class OptionsParser implements OptionsParserInterface
17+
{
18+
public function parseOptions(array $parameters): array
19+
{
20+
return array_map(
21+
/**
22+
* @param mixed $parameter
23+
*
24+
* @return mixed
25+
*/
26+
function ($parameter) {
27+
if (is_array($parameter)) {
28+
return $this->parseOptions($parameter);
29+
}
30+
31+
return $this->parseOption($parameter);
32+
},
33+
$parameters,
34+
);
35+
}
36+
37+
/**
38+
* @param mixed $parameter
39+
*
40+
* @return mixed
41+
*/
42+
private function parseOption($parameter)
43+
{
44+
if (!is_string($parameter)) {
45+
return $parameter;
46+
}
47+
48+
if (0 === strpos($parameter, 'callback:')) {
49+
return $this->parseOptionCallback(substr($parameter, 9));
50+
}
51+
52+
return $parameter;
53+
}
54+
55+
private function parseOptionCallback(string $callback): \Closure
56+
{
57+
if (!is_callable($callback)) {
58+
throw new \RuntimeException(\sprintf('%s is not a callable.', $callback));
59+
}
60+
61+
return $callback(...);
62+
}
63+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Sylius\Bundle\GridBundle\Parser;
15+
16+
interface OptionsParserInterface
17+
{
18+
public function parseOptions(array $parameters): array;
19+
}

src/Bundle/Renderer/TwigGridRenderer.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace Sylius\Bundle\GridBundle\Renderer;
1515

1616
use Sylius\Bundle\GridBundle\Form\Registry\FormTypeRegistryInterface;
17+
use Sylius\Bundle\GridBundle\Parser\OptionsParserInterface;
1718
use Sylius\Component\Grid\Definition\Action;
1819
use Sylius\Component\Grid\Definition\Field;
1920
use Sylius\Component\Grid\Definition\Filter;
@@ -36,6 +37,8 @@ final class TwigGridRenderer implements GridRendererInterface
3637

3738
private FormTypeRegistryInterface $formTypeRegistry;
3839

40+
private OptionsParserInterface $optionsParser;
41+
3942
private string $defaultTemplate;
4043

4144
private array $actionTemplates;
@@ -47,6 +50,7 @@ public function __construct(
4750
ServiceRegistryInterface $fieldsRegistry,
4851
FormFactoryInterface $formFactory,
4952
FormTypeRegistryInterface $formTypeRegistry,
53+
OptionsParserInterface $optionsParser,
5054
string $defaultTemplate,
5155
array $actionTemplates = [],
5256
array $filterTemplates = [],
@@ -55,6 +59,7 @@ public function __construct(
5559
$this->fieldsRegistry = $fieldsRegistry;
5660
$this->formFactory = $formFactory;
5761
$this->formTypeRegistry = $formTypeRegistry;
62+
$this->optionsParser = $optionsParser;
5863
$this->defaultTemplate = $defaultTemplate;
5964
$this->actionTemplates = $actionTemplates;
6065
$this->filterTemplates = $filterTemplates;
@@ -71,7 +76,8 @@ public function renderField(GridViewInterface $gridView, Field $field, $data)
7176
$fieldType = $this->fieldsRegistry->get($field->getType());
7277
$resolver = new OptionsResolver();
7378
$fieldType->configureOptions($resolver);
74-
$options = $resolver->resolve($field->getOptions());
79+
80+
$options = $resolver->resolve($this->optionsParser->parseOptions($field->getOptions()));
7581

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

src/Bundle/Resources/config/services.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,5 +128,8 @@
128128
<tag name="maker.command" />
129129
</service>
130130
<service id="Sylius\Bundle\GridBundle\Maker\MakeGrid" alias="sylius.grid.maker" />
131+
132+
<service id="sylius.grid.options_parser" class="Sylius\Bundle\GridBundle\Parser\OptionsParser" />
133+
<service id="Sylius\Bundle\GridBundle\Parser\OptionsParserInterface" alias="sylius.grid.options_parser" />
131134
</services>
132135
</container>

src/Bundle/Resources/config/services/twig.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<argument type="service" id="sylius.registry.grid_field" />
2121
<argument type="service" id="form.factory" />
2222
<argument type="service" id="sylius.form_registry.grid_filter" />
23+
<argument type="service" id="sylius.grid.options_parser" />
2324
<argument>@SyliusGrid/_grid.html.twig</argument>
2425
<argument>%sylius.grid.templates.action%</argument>
2526
<argument>%sylius.grid.templates.filter%</argument>

src/Bundle/Tests/Functional/GridUiTest.php

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ public function it_shows_authors_grid(): void
4141
$this->assertCount(10, $this->getAuthorNamesFromResponse());
4242
}
4343

44+
/** @test */
45+
public function it_shows_authors_ids(): void
46+
{
47+
$this->client->request('GET', '/authors/?limit=100');
48+
49+
$ids = $this->getAuthorIdsFromResponse();
50+
51+
$this->assertNotEmpty($ids);
52+
$this->assertSame(
53+
array_filter($ids, fn (string $id) => str_starts_with($id, '#')),
54+
$ids,
55+
);
56+
}
57+
4458
/** @test */
4559
public function it_sorts_authors_by_name_ascending_by_default(): void
4660
{
@@ -98,7 +112,7 @@ public function it_filters_books_by_title(): void
98112
$titles = $this->getBookTitlesFromResponse();
99113

100114
$this->assertCount(1, $titles);
101-
$this->assertSame('Book 5', $titles[0]);
115+
$this->assertSame('BOOK 5', $titles[0]);
102116
}
103117

104118
/** @test */
@@ -112,7 +126,7 @@ public function it_filters_books_by_title_with_contains(): void
112126
$titles = $this->getBookTitlesFromResponse();
113127

114128
$this->assertCount(1, $titles);
115-
$this->assertSame('Jurassic Park', $titles[0]);
129+
$this->assertSame('JURASSIC PARK', $titles[0]);
116130
}
117131

118132
/** @test */
@@ -125,7 +139,7 @@ public function it_filters_books_by_author(): void
125139
$titles = $this->getBookTitlesFromResponse();
126140

127141
$this->assertCount(2, $titles);
128-
$this->assertSame('Jurassic Park', $titles[0]);
142+
$this->assertSame('JURASSIC PARK', $titles[0]);
129143
}
130144

131145
/** @test */
@@ -139,7 +153,7 @@ public function it_filters_books_by_authors(): void
139153
$titles = $this->getBookTitlesFromResponse();
140154

141155
$this->assertCount(3, $titles);
142-
$this->assertSame('A Study in Scarlet', $titles[0]);
156+
$this->assertSame('A STUDY IN SCARLET', $titles[0]);
143157
}
144158

145159
/** @test */
@@ -152,7 +166,7 @@ public function it_filters_books_by_authors_nationality(): void
152166
$titles = $this->getBookTitlesFromResponse();
153167

154168
$this->assertCount(2, $titles);
155-
$this->assertSame('Jurassic Park', $titles[0]);
169+
$this->assertSame('JURASSIC PARK', $titles[0]);
156170
}
157171

158172
/** @test */
@@ -165,7 +179,7 @@ public function it_filters_books_by_author_and_currency(): void
165179
$titles = $this->getBookTitlesFromResponse();
166180

167181
$this->assertCount(1, $titles);
168-
$this->assertSame('Jurassic Park', $titles[0]);
182+
$this->assertSame('JURASSIC PARK', $titles[0]);
169183
}
170184

171185
/** @test */
@@ -274,6 +288,16 @@ private function getBookAuthorNationalitiesFromResponse(): array
274288
);
275289
}
276290

291+
/** @return string[] */
292+
private function getAuthorIdsFromResponse(): array
293+
{
294+
return $this->getCrawler()
295+
->filter('[data-test-id]')
296+
->each(
297+
fn (Crawler $node): string => $node->text(),
298+
);
299+
}
300+
277301
/** @return string[] */
278302
private function getAuthorNamesFromResponse(): array
279303
{
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace spec\Sylius\Bundle\GridBundle\Parser;
15+
16+
use PhpSpec\ObjectBehavior;
17+
use Sylius\Bundle\GridBundle\Parser\OptionsParserInterface;
18+
19+
final class OptionsParserSpec extends ObjectBehavior
20+
{
21+
function it_is_an_options_parser(): void
22+
{
23+
$this->shouldImplement(OptionsParserInterface::class);
24+
}
25+
26+
function it_parses_options_with_callback(): void
27+
{
28+
$this
29+
->parseOptions([
30+
'type' => 'callback',
31+
'option' => [
32+
'callback' => 'callback:App\\Helper\\GridHelper::addHashPrefix',
33+
],
34+
'label' => 'app.ui.id',
35+
])
36+
->shouldBeAValidConfig([
37+
'type' => 'callback',
38+
'option' => [],
39+
'label' => 'app.ui.id',
40+
])
41+
;
42+
}
43+
44+
public function getMatchers(): array
45+
{
46+
return [
47+
'beAValidConfig' => function ($subject, $subset) {
48+
if ([] !== array_diff($subject, $subset)) {
49+
return false;
50+
}
51+
52+
return is_callable($subject['option']['callback'] ?? null);
53+
},
54+
];
55+
}
56+
57+
function it_fails_while_parsing_options_with_invalid_callback(): void
58+
{
59+
$this
60+
->shouldThrow(\RuntimeException::class)
61+
->during('parseOptions', [[
62+
'type' => 'callback',
63+
'option' => [
64+
'callback' => 'callback:foobar',
65+
],
66+
'label' => 'app.ui.id',
67+
]])
68+
;
69+
}
70+
}

0 commit comments

Comments
 (0)