Skip to content

Commit

Permalink
Allow to customize the string representation of Collection field entries
Browse files Browse the repository at this point in the history
  • Loading branch information
javiereguiluz committed Oct 20, 2024
1 parent 5e4e7e3 commit 36163d8
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 6 deletions.
14 changes: 11 additions & 3 deletions doc/fields/CollectionField.rst
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,22 @@ used to render the form of each collection entry::
yield CollectionField::new('...')->setEntryType(SomeType::class);

setEntryToStringMethod
~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~

By default, items in the collection are represented by a single line showing
their ``__toString()`` value. The ``setEntryToStringMethod()`` method defines the method
used to get string value of each item::
their ``__toString()`` value. Use this option to define how to get the string
representation of each collection entry::

// this calls the 'getFullName()' method in the entity
yield CollectionField::new('...')->setEntryToStringMethod('getFullName');

// you can also pass a callable to generate the string
yield CollectionField::new('...')->setEntryToStringMethod(fn (): string => '...');
// your callable receives the entity and the translator service as arguments
yield CollectionField::new('...')->setEntryToStringMethod(
fn (Category $value, TranslatorInterface $translator): string => $translator->trans($value->getDescription())
);

showEntryLabel
~~~~~~~~~~~~~~

Expand Down
5 changes: 4 additions & 1 deletion src/Field/CollectionField.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ public function setEntryType(string $formTypeFqcn): self
return $this;
}

public function setEntryToStringMethod(string $toStringMethod): self
/**
* @param string|callable $toStringMethod Either a string with the name of the method to call in the entry object or a callable to generate the string representation of the entry. The callable is passed the value as the first argument and the translator service as the second argument.
*/
public function setEntryToStringMethod(string|callable $toStringMethod): self
{
$this->setCustomOption(self::OPTION_ENTRY_TO_STRING_METHOD, $toStringMethod);

Expand Down
16 changes: 14 additions & 2 deletions src/Twig/EasyAdminTwigExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
use Twig\Error\RuntimeError;
use Twig\Extension\AbstractExtension;
use Twig\Extension\GlobalsInterface;
use Twig\TwigFilter;
Expand Down Expand Up @@ -134,8 +133,21 @@ public function applyFilterIfExists(Environment $environment, $value, string $fi
throw new RuntimeError(sprintf('Invalid callback for filter: "%s"', $filterName));
}

public function representAsString($value, ?string $toStringMethod = null): string
public function representAsString($value, string|callable|null $toStringMethod = null): string
{
if (null !== $toStringMethod) {
if (\is_callable($toStringMethod)) {
return $toStringMethod($value, $this->translator);
}

$callable = [$value, $toStringMethod];
if (!\is_callable($callable) || !\method_exists($value, $toStringMethod)) {
throw new \RuntimeException(sprintf('The method "%s()" does not exist or is not callable in the value of type "%s"', $toStringMethod, is_object($value) ? \get_class($value) : \gettype($value)));
}

return \call_user_func($callable);
}

if (null === $value) {
return '';
}
Expand Down
75 changes: 75 additions & 0 deletions tests/Twig/EasyAdminTwigExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Tests\Twig;

use EasyCorp\Bundle\EasyAdminBundle\Provider\AdminContextProvider;
use EasyCorp\Bundle\EasyAdminBundle\Twig\EasyAdminTwigExtension;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

class EasyAdminTwigExtensionTest extends KernelTestCase
{
/**
* @dataProvider provideValuesForRepresentAsString
*/
public function testRepresentAsString($value, $expectedValue, bool $assertRegex = false, string|callable|null $toStringMethod = null): void
{
$translator = $this->getMockBuilder(TranslatorInterface::class)->disableOriginalConstructor()->getMock();
$translator->method('trans')->willReturnCallback(fn($value) => '*'.$value);

$extension = new EasyAdminTwigExtension(
$this->getMockBuilder(ServiceLocator::class)->disableOriginalConstructor()->getMock(),
$this->getMockBuilder(AdminContextProvider::class)->disableOriginalConstructor()->getMock(),
$this->getMockBuilder(CsrfTokenManagerInterface::class)->disableOriginalConstructor()->getMock(),
$this->getMockBuilder(ImportMapRenderer::class)->disableOriginalConstructor()->getMock(),
$translator
);

$result = $extension->representAsString($value, $toStringMethod);

if ($assertRegex) {
$this->assertMatchesRegularExpression($expectedValue, $result);
} else {
$this->assertSame($expectedValue, $result);
}
}

public function testRepresentAsStringExcepion()
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches('/The method "someMethod\(\)" does not exist or is not callable in the value of type "class@anonymous.*"/');

$extension = new EasyAdminTwigExtension(
$this->getMockBuilder(ServiceLocator::class)->disableOriginalConstructor()->getMock(),
$this->getMockBuilder(AdminContextProvider::class)->disableOriginalConstructor()->getMock(),
$this->getMockBuilder(CsrfTokenManagerInterface::class)->disableOriginalConstructor()->getMock(),
$this->getMockBuilder(ImportMapRenderer::class)->disableOriginalConstructor()->getMock(),
$this->getMockBuilder(TranslatorInterface::class)->disableOriginalConstructor()->getMock()
);

$extension->representAsString(new class {}, 'someMethod');
}

public function provideValuesForRepresentAsString()
{
yield [null, ''];
yield ['foo bar', 'foo bar'];
yield [5, '5'];
yield [3.14, '3.14'];
yield [true, 'true'];
yield [false, 'false'];
yield [[1, 2, 3], 'Array (3 items)'];
yield [new class implements TranslatableInterface { public function trans(TranslatorInterface $translator, string $locale = null): string { return $translator->trans('some value'); } }, '*some value'];
yield [new class {}, '/class@anonymous.*/', true];
yield [new class { public function __toString() { return 'foo bar'; }}, 'foo bar'];
yield [new class { public function getId() { return 1234; }}, '/class@anonymous.* #1234/', true];

yield ['foo', 'foo bar', false, fn($value) => $value.' bar'];
yield ['foo', '*foo bar', false, fn($value, $translator) => $translator->trans($value.' bar')];
yield [new class () { public function someMethod() { return 'foo'; }}, 'foo', false, 'someMethod'];
}
}

0 comments on commit 36163d8

Please sign in to comment.