Skip to content
Merged
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
14 changes: 14 additions & 0 deletions src/Bundle/Builder/Field/CallableField.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,18 @@ public static function create(string $name, callable $callable, bool $htmlspecia
->setOption('htmlspecialchars', $htmlspecialchars)
;
}

public static function createForService(string $name, string $service, ?string $method = null, bool $htmlspecialchars = true): FieldInterface
{
$field = Field::create($name, 'callable')
->setOption('service', $service)
->setOption('htmlspecialchars', $htmlspecialchars)
;

if ($method !== null) {
$field->setOption('method', $method);
}

return $field;
}
}
8 changes: 8 additions & 0 deletions src/Bundle/DependencyInjection/SyliusGridExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Sylius\Bundle\CurrencyBundle\SyliusCurrencyBundle;
use Sylius\Bundle\GridBundle\Grid\GridInterface;
use Sylius\Bundle\GridBundle\SyliusGridBundle;
use Sylius\Component\Grid\Annotation\AsGridFieldCallableService;
use Sylius\Component\Grid\Attribute\AsFilter;
use Sylius\Component\Grid\Data\DataProviderInterface;
use Sylius\Component\Grid\Filtering\ConfigurableFilterInterface;
Expand Down Expand Up @@ -85,6 +86,13 @@ static function (ChildDefinition $definition, AsFilter $attribute, \Reflector $r
$container->registerForAutoconfiguration(DataProviderInterface::class)
->addTag('sylius.grid_data_provider')
;

$container->registerAttributeForAutoconfiguration(
AsGridFieldCallableService::class,
static function (ChildDefinition $definition, AsGridFieldCallableService $attribute, \Reflector $reflector): void {
$definition->addTag('sylius.grid_field_callable_service');
},
);
}

public function getConfiguration(array $config, ContainerBuilder $container): Configuration
Expand Down
1 change: 1 addition & 0 deletions src/Bundle/Resources/config/services/field_types.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

<service id="sylius.grid_field.callable" class="Sylius\Component\Grid\FieldTypes\CallableFieldType">
<argument type="service" id="sylius.grid.data_extractor" />
<argument type="tagged_locator" tag="sylius.grid_field_callable_service" />
<tag name="sylius.grid_field" type="callable" />
</service>
<service id="Sylius\Component\Grid\FieldTypes\CallableFieldType" alias="sylius.grid_field.callable" />
Expand Down
23 changes: 23 additions & 0 deletions src/Bundle/Tests/Functional/GridUiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ public function it_shows_authors_ids(): void
);
}

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

$nationalities = $this->getAuthorNationalitiesFromResponse();

$this->assertCount(3, array_unique($nationalities));
$this->assertContains('US', $nationalities);
$this->assertContains('EN', $nationalities);
$this->assertContains('', $nationalities);
}

/** @test */
public function it_sorts_authors_by_name_ascending_by_default(): void
{
Expand Down Expand Up @@ -313,6 +326,16 @@ private function getAuthorIdsFromResponse(): array
);
}

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

/** @return string[] */
private function getAuthorNamesFromResponse(): array
{
Expand Down
19 changes: 19 additions & 0 deletions src/Component/Annotation/AsGridFieldCallableService.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\Component\Grid\Annotation;

#[\Attribute(\Attribute::TARGET_CLASS)]
final class AsGridFieldCallableService
{
}
49 changes: 45 additions & 4 deletions src/Component/FieldTypes/CallableFieldType.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,28 @@

namespace Sylius\Component\Grid\FieldTypes;

use Psr\Container\ContainerInterface;
use Sylius\Component\Grid\DataExtractor\DataExtractorInterface;
use Sylius\Component\Grid\Definition\Field;
use Sylius\Component\Grid\Exception\UnexpectedValueException;
use Symfony\Component\OptionsResolver\OptionsResolver;

final class CallableFieldType implements FieldTypeInterface
{
public function __construct(private DataExtractorInterface $dataExtractor)
{
public function __construct(
private DataExtractorInterface $dataExtractor,
private ContainerInterface $locator,
) {
}

public function render(Field $field, $data, array $options): string
{
if (isset($options['callable']) === isset($options['service'])) {
throw new \RuntimeException('Exactly one of the "callable" or "service" options must be defined.');
}

$value = $this->dataExtractor->get($field, $data);
$value = call_user_func($options['callable'], $value);
$value = call_user_func($this->getCallable($options), $value);

try {
$value = (string) $value;
Expand All @@ -46,11 +53,45 @@ public function render(Field $field, $data, array $options): string
return $value;
}

private function getCallable(array $options): callable
{
if (isset($options['callable'])) {
return $options['callable'];
}

if (!$this->locator->has($options['service'])) {
throw new \RuntimeException(sprintf('Service "%s" not found, make sure it is tagged with "sylius.grid_field_callable_service".', $options['service']));
}

$service = $this->locator->get($options['service']);
if (isset($options['method'])) {
$callable = [$service, $options['method']];

if (!is_callable($callable)) {
throw new \RuntimeException(sprintf('The method "%s" is not callable on service "%s".', $options['method'], $options['service']));
}

return $callable;
}

if (!is_callable($service)) {
throw new \RuntimeException(sprintf('The service "%s" is not callable.', $options['service']));
}

return $service;
}

public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired('callable');
$resolver->setDefined('callable');
$resolver->setAllowedTypes('callable', 'callable');

$resolver->setDefined('service');
$resolver->setAllowedTypes('service', 'string');

$resolver->setDefined('method');
$resolver->setAllowedTypes('method', 'string');

$resolver->setDefault('htmlspecialchars', true);
$resolver->setAllowedTypes('htmlspecialchars', 'bool');
}
Expand Down
74 changes: 73 additions & 1 deletion src/Component/spec/FieldTypes/CallableFieldTypeSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,30 @@
use Sylius\Component\Grid\Definition\Field;
use Sylius\Component\Grid\Exception\UnexpectedValueException;
use Sylius\Component\Grid\FieldTypes\FieldTypeInterface;
use Symfony\Contracts\Service\ServiceLocatorTrait;
use Symfony\Contracts\Service\ServiceProviderInterface;

final class CallableFieldTypeSpec extends ObjectBehavior
{
function let(DataExtractorInterface $dataExtractor): void
{
$this->beConstructedWith($dataExtractor);
$this->beConstructedWith(
$dataExtractor,
new class(['my_service' => fn () => new class() {
public function __invoke(string $value): string
{
return strtoupper($value);
}

public function concatenate(array $value): string
{
return implode(', ', $value);
}
},
]) implements ServiceProviderInterface {
use ServiceLocatorTrait;
},
);
}

function it_is_a_grid_field_type(): void
Expand Down Expand Up @@ -79,6 +97,31 @@ function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_static_callabl
])->shouldReturn('bar');
}

function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_service(
DataExtractorInterface $dataExtractor,
Field $field,
): void {
$dataExtractor->get($field, ['foo' => 'bar'])->willReturn('bar');

$this->render($field, ['foo' => 'bar'], [
'service' => 'my_service',
'htmlspecialchars' => true,
])->shouldReturn('BAR');
}

function it_uses_data_extractor_to_obtain_data_and_passes_it_to_a_service_and_method(
DataExtractorInterface $dataExtractor,
Field $field,
): void {
$dataExtractor->get($field, ['foo' => ['foo', 'bar', 'foobar']])->willReturn(['foo', 'bar', 'foobar']);

$this->render($field, ['foo' => ['foo', 'bar', 'foobar']], [
'service' => 'my_service',
'method' => 'concatenate',
'htmlspecialchars' => true,
])->shouldReturn('foo, bar, foobar');
}

function it_throws_an_exception_when_a_callable_return_value_cannot_be_casted_to_string(
DataExtractorInterface $dataExtractor,
Field $field,
Expand All @@ -98,6 +141,35 @@ function it_throws_an_exception_when_a_callable_return_value_cannot_be_casted_to
]);
}

function it_throws_an_exception_when_neither_callable_nor_service_options_are_defined(
DataExtractorInterface $dataExtractor,
Field $field,
): void {
$this
->shouldThrow(\RuntimeException::class)
->during('render', [
$field,
['foo' => 'bar'],
[],
]);
}

function it_throws_an_exception_when_both_callable_and_service_options_are_defined(
DataExtractorInterface $dataExtractor,
Field $field,
): void {
$this
->shouldThrow(\RuntimeException::class)
->during('render', [
$field,
['foo' => 'bar'],
[
'callable' => fn () => new \stdclass(),
'service' => 'my_service',
],
]);
}

static function callable(mixed $value): string
{
return strtolower($value);
Expand Down
2 changes: 2 additions & 0 deletions tests/Application/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ services:

App\BoardGameBlog\:
resource: '../src/BoardGameBlog'

App\Helper\GridHelper: ~
7 changes: 5 additions & 2 deletions tests/Application/config/sylius/grids.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,14 @@ sylius_grid:
label: Name
sortable: ~
nationality:
type: string
type: callable
options:
service: 'App\Helper\GridHelper'
method: 'formatNationality'
label: Name
sortable: nationality.name
path: nationality
limits: [10, 5, 15]
limits: [10, 5, 15, 100]

app_book_by_american_authors:
driver:
Expand Down
4 changes: 2 additions & 2 deletions tests/Application/config/sylius/grids/author.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@
->setSortable(true),
)
->addField(
StringField::create('nationality')
CallableField::createForService('nationality', \App\Helper\GridHelper::class)
->setLabel('Nationality')
->setSortable(true, 'nationality.name'),
)
->setLimits([10, 5, 15]),
->setLimits([10, 5, 15, 100]),
);
};
4 changes: 2 additions & 2 deletions tests/Application/src/Grid/AuthorGrid.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ public function buildGrid(GridBuilderInterface $gridBuilder): void
->setSortable(true),
)
->addField(
StringField::create('nationality')
CallableField::createForService('nationality', \App\Helper\GridHelper::class, 'formatNationality')
->setLabel('Name')
->setPath('nationality')
->setSortable(true, 'nationality.name'),
)
->setLimits([10, 5, 15])
->setLimits([10, 5, 15, 100])
;
}
}
17 changes: 17 additions & 0 deletions tests/Application/src/Helper/GridHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,25 @@

namespace App\Helper;

use Sylius\Component\Grid\Annotation\AsGridFieldCallableService;

#[AsGridFieldCallableService]
final class GridHelper
{
public function __invoke(?string $value): string
{
return $this->formatNationality($value);
}

public function formatNationality(?string $value): string
{
return match ($value) {
'English' => 'EN',
'American' => 'US',
null => '',
};
}

public static function addHashPrefix(string $value): string
{
return '#' . $value;
Expand Down