Skip to content

Commit

Permalink
Support editable mode on FieldDescriptionInterface::TYPE_ENUM (#8219)
Browse files Browse the repository at this point in the history
  • Loading branch information
onEXHovia authored Dec 4, 2024
1 parent 13f1e43 commit 23d9004
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 1 deletion.
2 changes: 1 addition & 1 deletion src/Action/SetObjectFieldValueAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public function __invoke(Request $request): JsonResponse
$value = $dataTransformer->reverseTransform($value);
}

if (null === $value && FieldDescriptionInterface::TYPE_CHOICE === $fieldDescription->getType()) {
if (null === $value && \in_array($fieldDescription->getType(), [FieldDescriptionInterface::TYPE_CHOICE, FieldDescriptionInterface::TYPE_ENUM], true)) {
return new JsonResponse(\sprintf(
'Edit failed, object with id "%s" not found in association "%s".',
$objectId,
Expand Down
73 changes: 73 additions & 0 deletions src/Form/DataTransformer/BackedEnumTransformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Sonata\AdminBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;

/**
* @phpstan-template T of \BackedEnum
* @phpstan-implements DataTransformerInterface<T, int|string>
*/
final class BackedEnumTransformer implements DataTransformerInterface
{
/**
* @phpstan-param class-string<T> $className
*/
public function __construct(
private string $className,
) {
}

/**
* @param int|string|null $value
*
* @phpstan-return T|null
*/
public function reverseTransform($value): ?\BackedEnum
{
if (null === $value || '' === $value) {
return null;
}

if (!\is_int($value) && !\is_string($value)) {
throw new TransformationFailedException(\sprintf('Could not transform value: expecting an int or string, got "%s".', get_debug_type($value)));
}

try {
return $this->className::from($value);
} catch (\ValueError|\TypeError) {
throw new TransformationFailedException(\sprintf('Could not transform value "%s".', $value));
}
}

/**
* @param \BackedEnum|null $value
*
* @phpstan-param T|null $value
*/
public function transform($value): string|int|null
{
if (null === $value) {
return null;
}

if (!$value instanceof \BackedEnum) {
throw new UnexpectedTypeException($value, \BackedEnum::class);
}

return $value->value;
}
}
13 changes: 13 additions & 0 deletions src/Form/DataTransformerResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace Sonata\AdminBundle\Form;

use Sonata\AdminBundle\FieldDescription\FieldDescriptionInterface;
use Sonata\AdminBundle\Form\DataTransformer\BackedEnumTransformer;
use Sonata\AdminBundle\Form\DataTransformer\ModelToIdTransformer;
use Sonata\AdminBundle\Model\ModelManagerInterface;
use Symfony\Component\Form\DataTransformerInterface;
Expand Down Expand Up @@ -75,6 +76,18 @@ public function resolve(
return $this->globalCustomTransformers[$fieldType];
}

if (FieldDescriptionInterface::TYPE_ENUM === $fieldType) {
$className = $fieldDescription->getOption('class');

if (
null !== $className
&& \is_string($className)
&& is_a($className, \BackedEnum::class, true)
) {
return new BackedEnumTransformer($className);
}
}

// Handle entity choice association type, transforming the value into entity
if (FieldDescriptionInterface::TYPE_CHOICE === $fieldType) {
$targetModel = $fieldDescription->getTargetModel();
Expand Down
2 changes: 2 additions & 0 deletions src/Resources/views/CRUD/base_list_field.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ file that was distributed with this source code.
{% set data_value = value|date('Y-m-d', options.timezone|default(null)) %}
{% elseif field_description.type == constant('Sonata\\AdminBundle\\FieldDescription\\FieldDescriptionInterface::TYPE_BOOLEAN') and value is empty %}
{% set data_value = 0 %}
{% elseif field_description.type == constant('Sonata\\AdminBundle\\FieldDescription\\FieldDescriptionInterface::TYPE_ENUM') and value is not empty %}
{% set data_value = value.value %}
{% elseif value is iterable %}
{% set data_value = value|json_encode %}
{% else %}
Expand Down
15 changes: 15 additions & 0 deletions src/Resources/views/CRUD/list_enum.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ file that was distributed with this source code.

{% extends get_admin_template('base_list_field', admin.code) %}

{% set is_editable =
field_description.option('editable', false) and
admin.hasAccess('edit', object)
%}
{% set x_editable_type = field_description.type|sonata_xeditable_type %}

{% block field_span_attributes %}
{% if is_editable and x_editable_type %}
{% apply spaceless %}
{{ parent() }}
data-source="{{ field_description|sonata_xeditable_choices|json_encode }}"
{% endapply %}
{% endif %}
{% endblock %}

{% block field %}
{%- include '@SonataAdmin/CRUD/display_enum.html.twig' with {
value: value,
Expand Down
6 changes: 6 additions & 0 deletions src/Twig/XEditableRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
final class XEditableRuntime implements RuntimeExtensionInterface
{
public const FIELD_DESCRIPTION_MAPPING = [
FieldDescriptionInterface::TYPE_ENUM => 'select',
FieldDescriptionInterface::TYPE_CHOICE => 'select',
FieldDescriptionInterface::TYPE_BOOLEAN => 'select',
FieldDescriptionInterface::TYPE_TEXTAREA => 'textarea',
Expand Down Expand Up @@ -83,6 +84,11 @@ public function getXEditableChoices(FieldDescriptionInterface $fieldDescription)
break;
}

if ($text instanceof \BackedEnum) {
$value = $text->value;
$text = $text->name;
}

if (\is_string($catalogue)) {
$text = $this->translator->trans($text, [], $catalogue);
}
Expand Down
78 changes: 78 additions & 0 deletions tests/Form/DataTransformer/BackedEnumTransformerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Sonata\AdminBundle\Tests\Form\DataTransformer;

use PHPUnit\Framework\TestCase;
use Sonata\AdminBundle\Form\DataTransformer\BackedEnumTransformer;
use Sonata\AdminBundle\Tests\Fixtures\Enum\Suit;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;

/**
* @requires PHP 8.1
*/
final class BackedEnumTransformerTest extends TestCase
{
public function testReverseTransform(): void
{
$transformer = new BackedEnumTransformer(Suit::class);

static::assertNull($transformer->reverseTransform(null));
static::assertNull($transformer->reverseTransform(''));
static::assertSame(Suit::Hearts, $transformer->reverseTransform(Suit::Hearts->value));
}

public function testReverseTransformNotValidValue(): void
{
$this->expectException(TransformationFailedException::class);
$this->expectExceptionMessage('Could not transform value "not_valid_value".');

$transformer = new BackedEnumTransformer(Suit::class);
$transformer->reverseTransform('not_valid_value');
}

/**
* @psalm-suppress InvalidArgument
*/
public function testReverseTransformNotScalar(): void
{
$this->expectException(TransformationFailedException::class);
$this->expectExceptionMessage('Could not transform value: expecting an int or string, got "stdClass".');

$transformer = new BackedEnumTransformer(Suit::class);
// @phpstan-ignore-next-line
$transformer->reverseTransform(new \stdClass());
}

public function testTransform(): void
{
$transformer = new BackedEnumTransformer(Suit::class);

static::assertNull($transformer->transform(null));
static::assertSame(Suit::Clubs->value, $transformer->transform(Suit::Clubs));
}

/**
* @psalm-suppress InvalidArgument
*/
public function testTransformUnexpectedType(): void
{
$this->expectException(UnexpectedTypeException::class);
$this->expectExceptionMessage('Expected argument of type "BackedEnum", "stdClass" given');

$transformer = new BackedEnumTransformer(Suit::class);
// @phpstan-ignore-next-line
$transformer->transform(new \stdClass());
}
}
16 changes: 16 additions & 0 deletions tests/Twig/XEditableRuntimeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use PHPUnit\Framework\TestCase;
use Sonata\AdminBundle\FieldDescription\FieldDescriptionInterface;
use Sonata\AdminBundle\Tests\Fixtures\Enum\Suit;
use Sonata\AdminBundle\Twig\XEditableRuntime;
use Symfony\Component\Translation\Translator;

Expand Down Expand Up @@ -91,5 +92,20 @@ public function provideGetXEditableChoicesIsIdempotentCases(): iterable
['value' => 'Status2', 'text' => 'Alias2'],
],
];

// TODO: Remove the "if" check when dropping support of PHP < 8.1 and add the case to the list
if (\PHP_VERSION_ID >= 80100) {
yield 'enum cases' => [
[
'required' => false,
'multiple' => false,
'choices' => [Suit::Hearts, Suit::Clubs],
],
[
['value' => 'H', 'text' => 'Hearts'],
['value' => 'C', 'text' => 'Clubs'],
],
];
}
}
}

0 comments on commit 23d9004

Please sign in to comment.