Skip to content

Commit 323c155

Browse files
authoredAug 18, 2023
Convert enum objects into scalar values during form submission (#57)
* Convert enum objects into scalar values during form submission * Only trigger deprecation about "enum_choice_value" option when values are objects * Add doc about "enum_choice_value" option * Add doc for PHP native enum support --------- Co-authored-by: Yann Eugoné <[email protected]>
1 parent 565f030 commit 323c155

File tree

6 files changed

+287
-4
lines changed

6 files changed

+287
-4
lines changed
 

‎README.md

+1
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ See example Symfony project in [integration test suite](tests/Integration).
192192

193193
- Creating [enums](docs/creating-enum.md)
194194
- Creating [translated enums](docs/creating-translated-enum.md)
195+
- Integration with [PHP native enum](docs/native-enum-integration.md)
195196
- Integration with [myclabs/php-enum](docs/myclabs-enum-integration.md)
196197
- Migrating [from standard Symfony](docs/migrating-from-symfony-standard.md)
197198
- Integration with [SonataAdminBundle](docs/sonata-admin-integration.md)

‎docs/myclabs-enum-integration.md

+37
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,40 @@ class StatusEnum extends MyCLabsTranslatedEnum
7070
}
7171
}
7272
```
73+
74+
## Submitting values through a form
75+
76+
Because values of enum like `StatusEnum` above are objects, it is not possible to submit it via HTTP.
77+
As described in the [documentation](https://symfony.com/doc/current/reference/forms/types/choice.html#choice-value) Symfony will use an incrementing integer as value.
78+
Example, with `StatusEnum` above:
79+
- `0` will be the value for `MemberStatus::NEW`
80+
- `1` will be the value for `MemberStatus::VALIDATED`
81+
- `2` will be the value for `MemberStatus::DISABLED`
82+
83+
But, if you do not like this behavior, you can configure the form to use values instead:
84+
```php
85+
<?php
86+
87+
namespace App\Form\Type;
88+
89+
use App\Enum\StatusEnum;
90+
use Symfony\Component\Form\AbstractType;
91+
use Symfony\Component\Form\FormBuilderInterface;
92+
use Yokai\EnumBundle\Form\Type\EnumType;
93+
94+
class MemberType extends AbstractType
95+
{
96+
public function buildForm(FormBuilderInterface $builder, array $options): void
97+
{
98+
$builder->add('status', EnumType::class, [
99+
'enum' => StatusEnum::class,
100+
'enum_choice_value' => true,
101+
]);
102+
}
103+
}
104+
```
105+
106+
Now, still with `StatusEnum` above:
107+
- `"new"` will be the value for `MemberStatus::NEW`
108+
- `"validated"` will be the value for `MemberStatus::VALIDATED`
109+
- `"disabled"` will be the value for `MemberStatus::DISABLED`

‎docs/native-enum-integration.md

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# PHP native enum integration
2+
3+
Let say that you already has such enum, from [PHP](https://www.php.net/manual/en/language.enumerations.php).
4+
5+
```php
6+
<?php
7+
8+
declare(strict_types=1);
9+
10+
namespace App\Model;
11+
12+
enum MemberStatus: string
13+
{
14+
case NEW = 'new';
15+
case VALIDATED = 'validated';
16+
case DISABLED = 'disabled';
17+
}
18+
```
19+
20+
> **Note**
21+
> Here, we are using a `StringBackedEnum`, but it is not required.
22+
> The bundle supports any form of `UnitEnum`, backed or not.
23+
> https://www.php.net/manual/en/language.enumerations.backed.php
24+
25+
## Standard enum
26+
27+
If you want to integrate with the bundle, you just have to declare an enum for that class.
28+
29+
```php
30+
<?php
31+
32+
declare(strict_types=1);
33+
34+
namespace App\Enum;
35+
36+
use App\Model\MemberStatus;
37+
use Yokai\EnumBundle\NativeEnum;
38+
39+
class StatusEnum extends NativeEnum
40+
{
41+
public function __construct()
42+
{
43+
parent::__construct(MemberStatus::class);
44+
}
45+
}
46+
```
47+
48+
## Translated enum
49+
50+
Or if you want to translate enum constant labels.
51+
52+
```php
53+
<?php
54+
55+
declare(strict_types=1);
56+
57+
namespace App\Enum;
58+
59+
use App\Model\MemberStatus;
60+
use Symfony\Contracts\Translation\TranslatorInterface;
61+
use Yokai\EnumBundle\NativeTranslatedEnum;
62+
63+
class StatusEnum extends NativeTranslatedEnum
64+
{
65+
public function __construct(TranslatorInterface $translator)
66+
{
67+
parent::__construct(MemberStatus::class, $translator, 'status.%s');
68+
}
69+
}
70+
```
71+
72+
## Submitting values through a form
73+
74+
Because values of enum like `StatusEnum` above are objects, it is not possible to submit it via HTTP.
75+
As described in the [documentation](https://symfony.com/doc/current/reference/forms/types/choice.html#choice-value) Symfony will use an incrementing integer as value.
76+
Example, with `StatusEnum` above:
77+
- `0` will be the value for `MemberStatus::NEW`
78+
- `1` will be the value for `MemberStatus::VALIDATED`
79+
- `2` will be the value for `MemberStatus::DISABLED`
80+
81+
But, if you do not like this behavior, you can configure the form to use values instead:
82+
```php
83+
<?php
84+
85+
namespace App\Form\Type;
86+
87+
use App\Enum\StatusEnum;
88+
use Symfony\Component\Form\AbstractType;
89+
use Symfony\Component\Form\FormBuilderInterface;
90+
use Yokai\EnumBundle\Form\Type\EnumType;
91+
92+
class MemberType extends AbstractType
93+
{
94+
public function buildForm(FormBuilderInterface $builder, array $options): void
95+
{
96+
$builder->add('status', EnumType::class, [
97+
'enum' => StatusEnum::class,
98+
'enum_choice_value' => true,
99+
]);
100+
}
101+
}
102+
```
103+
104+
Now, still with `StatusEnum` above:
105+
- `"new"` will be the value for `MemberStatus::NEW`
106+
- `"validated"` will be the value for `MemberStatus::VALIDATED`
107+
- `"disabled"` will be the value for `MemberStatus::DISABLED`

‎src/Form/Type/EnumType.php

+42-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Yokai\EnumBundle\Form\Type;
66

7+
use MyCLabs\Enum\Enum;
78
use Symfony\Component\Form\AbstractType;
89
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
910
use Symfony\Component\OptionsResolver\Options;
@@ -34,6 +35,7 @@ public function __construct(EnumRegistry $enumRegistry)
3435
public function configureOptions(OptionsResolver $resolver): void
3536
{
3637
$resolver
38+
->setDefined(['enum', 'enum_choice_value'])
3739
->setRequired('enum')
3840
->setAllowedValues(
3941
'enum',
@@ -44,7 +46,46 @@ function (string $name): bool {
4446
->setDefault(
4547
'choices',
4648
function (Options $options): array {
47-
return $this->enumRegistry->get($options['enum'])->getChoices();
49+
$choices = $this->enumRegistry->get($options['enum'])->getChoices();
50+
51+
if ($options['enum_choice_value'] === null) {
52+
foreach ($choices as $value) {
53+
if (!\is_scalar($value)) {
54+
@\trigger_error(
55+
'Not configuring the "enum_choice_value" option is deprecated.' .
56+
' It will default to "true" in 5.0.',
57+
\E_USER_DEPRECATED
58+
);
59+
break;
60+
}
61+
}
62+
}
63+
64+
return $choices;
65+
}
66+
)
67+
->setAllowedTypes('enum_choice_value', ['bool', 'null'])
68+
->setDefault('enum_choice_value', null)
69+
->setDefault(
70+
'choice_value',
71+
static function (Options $options) {
72+
if ($options['enum_choice_value'] !== true) {
73+
return null;
74+
}
75+
76+
return function ($value) {
77+
if ($value instanceof \BackedEnum) {
78+
return $value->value;
79+
}
80+
if ($value instanceof \UnitEnum) {
81+
return $value->name;
82+
}
83+
if ($value instanceof Enum) {
84+
return $value->getValue();
85+
}
86+
87+
return $value;
88+
};
4889
}
4990
)
5091
;

‎tests/Unit/Form/TestExtension.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,6 @@ protected function loadTypeGuesser(): ?EnumTypeGuesser
5454
return null;
5555
}
5656

57-
return new EnumTypeGuesser($this->metadataFactory, $this->enumRegistry);
57+
return new EnumTypeGuesser($this->metadataFactory);
5858
}
5959
}

‎tests/Unit/Form/Type/EnumTypeTest.php

+99-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,15 @@
1010
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
1111
use Yokai\EnumBundle\EnumRegistry;
1212
use Yokai\EnumBundle\Form\Type\EnumType;
13+
use Yokai\EnumBundle\NativeEnum;
14+
use Yokai\EnumBundle\Tests\Unit\Fixtures\Action;
15+
use Yokai\EnumBundle\Tests\Unit\Fixtures\ActionEnum;
16+
use Yokai\EnumBundle\Tests\Unit\Fixtures\HTTPMethod;
17+
use Yokai\EnumBundle\Tests\Unit\Fixtures\HTTPStatus;
18+
use Yokai\EnumBundle\Tests\Unit\Fixtures\Picture;
1319
use Yokai\EnumBundle\Tests\Unit\Fixtures\StateEnum;
1420
use Yokai\EnumBundle\Tests\Unit\Form\TestExtension;
21+
use Yokai\EnumBundle\Tests\Unit\Translator;
1522

1623
/**
1724
* @author Yann Eugoné <eugone.yann@gmail.com>
@@ -40,19 +47,109 @@ public function testEnumOptionValid(): void
4047
);
4148
}
4249

50+
/**
51+
* @dataProvider submit
52+
*/
53+
public function testSubmit($enum, $options, $data, $expected): void
54+
{
55+
$form = $this->createForm($enum, $options);
56+
$form->submit($data);
57+
self::assertEquals($expected, $form->getData());
58+
}
59+
60+
public static function submit(): \Generator
61+
{
62+
yield [
63+
StateEnum::class,
64+
[],
65+
'new',
66+
'new',
67+
];
68+
yield [
69+
StateEnum::class,
70+
['enum_choice_value' => true],
71+
'new',
72+
'new',
73+
];
74+
75+
yield [
76+
ActionEnum::class,
77+
[],
78+
1,
79+
Action::EDIT(),
80+
];
81+
yield [
82+
ActionEnum::class,
83+
['enum_choice_value' => true],
84+
'edit',
85+
Action::EDIT(),
86+
];
87+
88+
if (\PHP_VERSION_ID < 80100) {
89+
return;
90+
}
91+
92+
yield [
93+
Picture::class,
94+
[],
95+
0,
96+
Picture::Landscape,
97+
];
98+
yield [
99+
Picture::class,
100+
['enum_choice_value' => true],
101+
'Landscape',
102+
Picture::Landscape,
103+
];
104+
105+
yield [
106+
HTTPMethod::class,
107+
[],
108+
0,
109+
HTTPMethod::GET,
110+
];
111+
yield [
112+
HTTPMethod::class,
113+
['enum_choice_value' => true],
114+
'get',
115+
HTTPMethod::GET,
116+
];
117+
118+
yield [
119+
HTTPStatus::class,
120+
['enum_choice_value' => true],
121+
200,
122+
HTTPStatus::OK,
123+
];
124+
yield [
125+
HTTPStatus::class,
126+
[],
127+
0,
128+
HTTPStatus::OK,
129+
];
130+
}
131+
43132
protected function getExtensions(): array
44133
{
45134
$enumRegistry = new EnumRegistry();
46135
$enumRegistry->add(new StateEnum());
136+
$enumRegistry->add(new ActionEnum(new Translator([
137+
'action.VIEW' => 'Voir',
138+
'action.EDIT' => 'Modifier',
139+
])));
140+
if (\PHP_VERSION_ID >= 80100) {
141+
$enumRegistry->add(new NativeEnum(Picture::class));
142+
$enumRegistry->add(new NativeEnum(HTTPMethod::class));
143+
$enumRegistry->add(new NativeEnum(HTTPStatus::class));
144+
}
47145

48146
return [
49147
new TestExtension($enumRegistry)
50148
];
51149
}
52150

53-
private function createForm($enum = null): FormInterface
151+
private function createForm($enum = null, $options = []): FormInterface
54152
{
55-
$options = [];
56153
if ($enum) {
57154
$options['enum'] = $enum;
58155
}

0 commit comments

Comments
 (0)
Please sign in to comment.