Skip to content

Commit

Permalink
Add callable field (#321)
Browse files Browse the repository at this point in the history
> 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.

The documentation associated for this PR can be found here
Sylius/Stack#228
  • Loading branch information
GSadee authored Feb 10, 2025
2 parents ed0d38d + 58f0dbc commit 9bec850
Show file tree
Hide file tree
Showing 23 changed files with 528 additions and 19 deletions.
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: 1 addition & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
includes:
- phpstan-baseline.neon
- vendor/phpstan/phpstan-webmozart-assert/extension.neon
- vendor/phpstan/phpstan-phpunit/extension.neon

- vendor/phpstan/phpstan-phpunit/rules.neon

parameters:
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)
;
}
}
55 changes: 55 additions & 0 deletions src/Bundle/Parser/OptionsParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?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;

use Sylius\Component\Grid\Exception\InvalidArgumentException;

final class OptionsParser implements OptionsParserInterface
{
public function parseOptions(array $parameters): array
{
return array_map(
function (mixed $parameter): mixed {
if (is_array($parameter)) {
return $this->parseOptions($parameter);
}

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

private function parseOption(mixed $parameter): mixed
{
if (!is_string($parameter)) {
return $parameter;
}

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

return $parameter;
}

private function parseOptionCallable(string $callable): \Closure
{
if (!is_callable($callable)) {
throw new InvalidArgumentException(\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" public="false" />
<service id="Sylius\Bundle\GridBundle\Parser\OptionsParserInterface" alias="sylius.grid.options_parser" public="false" />
</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
36 changes: 30 additions & 6 deletions src/Bundle/Tests/Functional/GridUiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ public function it_shows_authors_grid(): void
$this->assertCount(10, $this->getAuthorNamesFromResponse());
}

/** @test */
public function it_shows_authors_ids(): void
{
$this->client->request('GET', '/authors/?limit=100');

$ids = $this->getAuthorIdsFromResponse();

$this->assertNotEmpty($ids);
$this->assertSame(
array_filter($ids, fn (string $id) => str_starts_with($id, '#')),
$ids,
);
}

/** @test */
public function it_sorts_authors_by_name_ascending_by_default(): void
{
Expand Down Expand Up @@ -98,7 +112,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 +126,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 +139,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 +153,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 +166,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 +179,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 Expand Up @@ -274,6 +288,16 @@ private function getBookAuthorNationalitiesFromResponse(): array
);
}

/** @return string[] */
private function getAuthorIdsFromResponse(): array
{
return $this->getCrawler()
->filter('[data-test-id]')
->each(
fn (Crawler $node): string => $node->text(),
);
}

/** @return string[] */
private function getAuthorNamesFromResponse(): array
{
Expand Down
57 changes: 57 additions & 0 deletions src/Bundle/Tests/Unit/Parser/OptionsParserTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?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\Tests\Unit\Parser;

use PHPUnit\Framework\TestCase;
use Sylius\Bundle\GridBundle\Parser\OptionsParser;
use Sylius\Bundle\GridBundle\Parser\OptionsParserInterface;
use Sylius\Component\Grid\Exception\InvalidArgumentException;

final class OptionsParserTest extends TestCase
{
public function testItImplementsOptionsParserInterface(): void
{
$this->assertInstanceOf(OptionsParserInterface::class, new OptionsParser());
}

public function testItParsesOptionsWithCallable(): void
{
$options = (new OptionsParser())->parseOptions([
'type' => 'callable',
'option' => [
'callable' => 'callable:strtoupper',
],
'label' => 'app.ui.id',
]);

$this->assertArrayHasKey('type', $options);
$this->assertArrayHasKey('option', $options);
$this->assertArrayHasKey('label', $options);

$this->assertIsCallable($options['option']['callable'] ?? null);
}

public function testItFailsWhileParsingOptionsWithInvalidCallable(): void
{
$this->expectException(InvalidArgumentException::class);

$options = (new OptionsParser())->parseOptions([
'type' => 'callable',
'option' => [
'callable' => 'callable:foobar',
],
'label' => 'app.ui.id',
]);
}
}
Loading

0 comments on commit 9bec850

Please sign in to comment.