diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml
index 46d82c5..a7ffcee 100644
--- a/.github/workflows/php.yml
+++ b/.github/workflows/php.yml
@@ -97,7 +97,7 @@ jobs:
with:
php-version: 8.4
extensions: json
- ini-values: date.timezone=Europe/Paris
+ ini-values: date.timezone=Europe/Paris, opcache.jit=disable
- name: Install Infection
run: |
diff --git a/README.md b/README.md
index a8fa9be..1e1ebe7 100644
--- a/README.md
+++ b/README.md
@@ -990,12 +990,23 @@ An element for handle any value types. This is useful for create an inline custo
But it's strongly discouraged : prefer use one of the native element, or create a custom one.
```php
-$builder->add('foo', AnyElement::class) // No helper method are present
+$builder->any('foo')
->satisfy(function ($value) { ... }) // Configure the element
->transform(function ($value) { ... })
;
```
+### EnumElement
+
+Handle PHP 8.1 enum values. Both backed enum and simple unit enum are supported.
+If the enum is not backed, it's name will be used as HTTP value. Otherwise, the backed value will be used.
+
+```php
+$builder->enum('foo', FooEnum::class)
+ ->backed(false) // By default, the type of the enum is automatically detected. Use this method to force the use of name instead of backed value, or vice versa
+;
+```
+
## Create a custom element
You can declare custom elements to handle complex types, and reuse into any forms.
diff --git a/composer.json b/composer.json
index 2bcae80..ef34b13 100755
--- a/composer.json
+++ b/composer.json
@@ -27,7 +27,7 @@
"symfony/security-csrf": "~6.4|~7.0|~8.0",
"giggsey/libphonenumber-for-php": "~8.0|~9.0",
"phpunit/phpunit": "~13.0",
- "vimeo/psalm": "~6.15.1",
+ "vimeo/psalm": "~6.16",
"symfony/http-foundation": "~6.4|~7.0|~8.0",
"symfony/form": "~6.4|~7.0|~8.0"
},
diff --git a/src/Aggregate/ArrayElementBuilder.php b/src/Aggregate/ArrayElementBuilder.php
index 1364707..eacd25f 100644
--- a/src/Aggregate/ArrayElementBuilder.php
+++ b/src/Aggregate/ArrayElementBuilder.php
@@ -2,15 +2,19 @@
namespace Bdf\Form\Aggregate;
+use BackedEnum;
use Bdf\Form\Choice\ChoiceBuilderTrait;
use Bdf\Form\Choice\ChoiceInterface;
use Bdf\Form\ElementBuilderInterface;
use Bdf\Form\ElementInterface;
+use Bdf\Form\Leaf\AnyElement;
use Bdf\Form\Leaf\BooleanElement;
use Bdf\Form\Leaf\Date\DateTimeElement;
+use Bdf\Form\Leaf\EnumElementBuilder;
use Bdf\Form\Leaf\FloatElement;
use Bdf\Form\Leaf\IntegerElement;
use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\Leaf\UnitEnumElement;
use Bdf\Form\Phone\PhoneElement;
use Bdf\Form\Registry\Registry;
use Bdf\Form\Registry\RegistryInterface;
@@ -23,6 +27,7 @@
use Symfony\Component\Validator\Constraints\Choice as ChoiceConstraint;
use Symfony\Component\Validator\Constraints\Count;
use Symfony\Component\Validator\Constraints\NotBlank;
+use UnitEnum;
use function assert;
@@ -277,6 +282,53 @@ public function phone(?callable $configurator = null): static
return $this->element(PhoneElement::class, $configurator);
}
+ /**
+ * Define as array of any values
+ *
+ *
+ * $builder->array('values')->any();
+ *
+ *
+ * @param callable(ElementBuilderInterface>):void|null $configurator Callback for configure the inner element builder
+ *
+ * @return static
+ * @psalm-this-out ArrayElementBuilder
+ *
+ * @since 2.0
+ */
+ public function any(?callable $configurator = null): static
+ {
+ return $this->element(AnyElement::class, $configurator);
+ }
+
+ /**
+ * Define as array of enum
+ *
+ *
+ * $builder->array('types')->enum(Types::class, function(EnumElementBuilder $builder) {
+ * $builder->backed(false);
+ * })->getset();
+ *
+ *
+ * @param class-string $enumClass The enum class
+ * @param callable(EnumElementBuilder):void|null $configurator Callback for configure the inner element builder
+ *
+ * @return static
+ * @psalm-this-out ArrayElementBuilder<\UnitEnum>
+ *
+ * @since 2.0
+ */
+ public function enum(string $enumClass, ?callable $configurator = null): static
+ {
+ return $this->element(UnitEnumElement::class, function (EnumElementBuilder $builder) use ($enumClass, $configurator) {
+ $builder->enumClass($enumClass);
+
+ if ($configurator !== null) {
+ $configurator($builder);
+ }
+ });
+ }
+
/**
* Define as array of embedded forms
*
@@ -350,12 +402,12 @@ final public function required(string|Constraint|null $message = null, ?bool $al
/**
* {@inheritdoc}
*
- * @param ChoiceInterface|array|callable $choices The allowed values in PHP form.
+ * @param ChoiceInterface|array|class-string|callable $choices The allowed values in PHP form.
* @param string|null $message The error message.
* @param non-negative-int $min
* @param positive-int $max
*/
- final public function choices(ChoiceInterface|array|callable $choices, ?string $message = null, ?bool $multiple = null, ?bool $strict = null, ?int $min = null, ?int $max = null, ?string $minMessage = null, ?string $maxMessage = null): static
+ final public function choices(ChoiceInterface|array|string|callable $choices, ?string $message = null, ?bool $multiple = null, ?bool $strict = null, ?int $min = null, ?int $max = null, ?string $minMessage = null, ?string $maxMessage = null): static
{
/** @psalm-suppress MissingConstructor */
$builder = new class {
diff --git a/src/Aggregate/FormBuilder.php b/src/Aggregate/FormBuilder.php
index 5c2a43b..cc41590 100644
--- a/src/Aggregate/FormBuilder.php
+++ b/src/Aggregate/FormBuilder.php
@@ -21,6 +21,7 @@
use Bdf\Form\Leaf\Date\DateTimeChildBuilder;
use Bdf\Form\Leaf\Date\DateTimeElement;
use Bdf\Form\Leaf\Date\DateTimeElementBuilder;
+use Bdf\Form\Leaf\EnumElementBuilder;
use Bdf\Form\Leaf\FloatElement;
use Bdf\Form\Leaf\FloatElementBuilder;
use Bdf\Form\Leaf\Helper\EmailElement;
@@ -29,6 +30,7 @@
use Bdf\Form\Leaf\IntegerElementBuilder;
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\Leaf\StringElementBuilder;
+use Bdf\Form\Leaf\UnitEnumElement;
use Bdf\Form\Phone\PhoneChildBuilder;
use Bdf\Form\Phone\PhoneElement;
use Bdf\Form\Phone\PhoneElementBuilder;
@@ -39,6 +41,7 @@
use Override;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
+use UnitEnum;
/**
* Builder for a form
@@ -221,6 +224,21 @@ public function phone(string $name): ChildBuilderInterface
return $this->add($name, PhoneElement::class);
}
+ /**
+ * {@inheritdoc}
+ *
+ * @param non-empty-string $name The child name
+ * @param class-string $enumClass The enum class
+ * @return ChildBuilder
+ */
+ #[Override]
+ public function enum(string $name, string $enumClass): ChildBuilderInterface
+ {
+ /** @var ChildBuilder $builder */
+ $builder = $this->add($name, UnitEnumElement::class);
+ return $builder->enumClass($enumClass);
+ }
+
/**
* {@inheritdoc}
*
diff --git a/src/Aggregate/FormBuilderInterface.php b/src/Aggregate/FormBuilderInterface.php
index f05c52a..6035349 100644
--- a/src/Aggregate/FormBuilderInterface.php
+++ b/src/Aggregate/FormBuilderInterface.php
@@ -13,6 +13,7 @@
use Bdf\Form\Leaf\BooleanElementBuilder;
use Bdf\Form\Leaf\Date\DateTimeChildBuilder;
use Bdf\Form\Leaf\Date\DateTimeElementBuilder;
+use Bdf\Form\Leaf\EnumElementBuilder;
use Bdf\Form\Leaf\FloatElementBuilder;
use Bdf\Form\Leaf\IntegerElementBuilder;
use Bdf\Form\Leaf\StringElementBuilder;
@@ -21,6 +22,7 @@
use Override;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
+use UnitEnum;
/**
* Base builder type for a form
@@ -80,7 +82,7 @@ public function any(string $name): ChildBuilderInterface;
* Add a new string element on the form
*
*
- * $builder->string('id', 'aaa-aaa-aaa')->regex('/[a-z]{3}(-[a-z]{3}){2}/i')->length(['max' => 35]);
+ * $builder->string('id', 'aaa-aaa-aaa')->regex('/[a-z]{3}(-[a-z]{3}){2}/i')->length(max: 35);
*
*
* @param non-empty-string $name The child name
@@ -170,6 +172,23 @@ public function dateTime(string $name): ChildBuilderInterface;
*/
public function phone(string $name): ChildBuilderInterface;
+ /**
+ * Add a new enum element on the form
+ *
+ *
+ * $builder->enum('type', Types::class)->getset();
+ *
+ *
+ * @param non-empty-string $name The child name
+ * @param class-string $enumClass The enum class
+ *
+ * @return ChildBuilder|EnumElementBuilder
+ * @psalm-return ChildBuilderInterface
+ *
+ * @since 2.0
+ */
+ public function enum(string $name, string $enumClass): ChildBuilderInterface;
+
/**
* Add a new csrf token on form
*
diff --git a/src/Choice/ChoiceBuilderTrait.php b/src/Choice/ChoiceBuilderTrait.php
index 3551251..65ec263 100644
--- a/src/Choice/ChoiceBuilderTrait.php
+++ b/src/Choice/ChoiceBuilderTrait.php
@@ -2,11 +2,15 @@
namespace Bdf\Form\Choice;
+use BackedEnum;
use Bdf\Form\ElementBuilderInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Choice as ChoiceConstraint;
use function is_array;
+use function is_callable;
+use function is_string;
+use function is_subclass_of;
/**
* Trait for configure choices on an element
@@ -38,11 +42,14 @@ trait ChoiceBuilderTrait
* return $this->repository->loadChoices();
* });
*
+ * // Using enum
+ * $builder->choices(MyEnum::class);
+ *
* $builder->choices(['foo', 'bar'], 'my error'); // With message
* $builder->choices(['foo', 'bar'], min: 2, max: 6); // With custom options
*
*
- * @param ChoiceInterface|array|callable $choices The allowed values in PHP form.
+ * @param ChoiceInterface|array|class-string|callable $choices The allowed values in PHP form.
* @param string|null $message The error message.
* @param non-negative-int|null $min
* @param positive-int|null $max
@@ -50,10 +57,14 @@ trait ChoiceBuilderTrait
* @return $this
* @see ChoiceConstraint
*/
- final public function choices(ChoiceInterface|array|callable $choices, ?string $message = null, ?bool $multiple = null, ?bool $strict = null, ?int $min = null, ?int $max = null, ?string $minMessage = null, ?string $maxMessage = null): static
+ final public function choices(ChoiceInterface|array|string|callable $choices, ?string $message = null, ?bool $multiple = null, ?bool $strict = null, ?int $min = null, ?int $max = null, ?string $minMessage = null, ?string $maxMessage = null): static
{
if (!$choices instanceof ChoiceInterface) {
- $choices = is_array($choices) ? new ArrayChoice($choices) : new LazyChoice($choices);
+ $choices = match (true) {
+ is_array($choices) => new ArrayChoice($choices),
+ is_string($choices) && is_subclass_of($choices, BackedEnum::class) => new EnumChoice($choices),
+ is_callable($choices) => new LazyChoice($choices),
+ };
}
$callback = $choices->values(...);
diff --git a/src/Choice/ChoiceView.php b/src/Choice/ChoiceView.php
index adce368..fd483be 100644
--- a/src/Choice/ChoiceView.php
+++ b/src/Choice/ChoiceView.php
@@ -27,7 +27,7 @@ class ChoiceView
public mixed $value;
/**
- * The view representation of the choice.
+ * Does the current option is selected?
*/
public bool $selected;
diff --git a/src/Choice/EnumChoice.php b/src/Choice/EnumChoice.php
new file mode 100644
index 0000000..26deed6
--- /dev/null
+++ b/src/Choice/EnumChoice.php
@@ -0,0 +1,64 @@
+
+ */
+final readonly class EnumChoice implements ChoiceInterface
+{
+ public function __construct(
+ /**
+ * @var class-string
+ */
+ private string $enumClass,
+
+ /**
+ * Function use to generate the label from the enum case.
+ * If not set, {@see BackedEnum::$name} is used as label.
+ *
+ * @var (Closure(E):string)|null
+ */
+ private ?Closure $label = null,
+ ) {
+ assert(is_subclass_of($enumClass, BackedEnum::class));
+ }
+
+ #[Override]
+ public function values(): array
+ {
+ $values = [];
+
+ foreach ($this->enumClass::cases() as $value) {
+ $values[$this->label ? ($this->label)($value) : $value->name] = $value->value;
+ }
+
+ return $values;
+ }
+
+ #[Override]
+ public function view(?callable $configuration = null): array
+ {
+ $view = [];
+
+ foreach ($this->enumClass::cases() as $value) {
+ $view[] = $choice = new ChoiceView($value->value, $this->label ? ($this->label)($value) : $value->name);
+
+ if ($configuration !== null) {
+ $configuration($choice);
+ }
+ }
+
+ return $view;
+ }
+}
diff --git a/src/Leaf/BackedEnumElement.php b/src/Leaf/BackedEnumElement.php
new file mode 100644
index 0000000..de3ce64
--- /dev/null
+++ b/src/Leaf/BackedEnumElement.php
@@ -0,0 +1,83 @@
+
+ */
+final class BackedEnumElement extends LeafElement
+{
+ public function __construct(
+ /**
+ * @var class-string
+ */
+ private readonly string $enumClass,
+
+ ?ValueValidatorInterface $validator = null,
+ ?TransformerInterface $transformer = null,
+ ?ChoiceInterface $choices = null
+ ) {
+ parent::__construct($validator, $transformer, $choices);
+ }
+
+ #[Override]
+ protected function toPhp(mixed $httpValue): ?BackedEnum
+ {
+ if (is_int($httpValue) || is_string($httpValue)) {
+ return $this->enumClass::tryFrom($httpValue);
+ }
+
+ if ($httpValue instanceof $this->enumClass) {
+ return $httpValue;
+ }
+
+ return null;
+ }
+
+ #[Override]
+ protected function toHttp(mixed $phpValue): string|int|null
+ {
+ if ($phpValue instanceof $this->enumClass) {
+ return $phpValue->value;
+ }
+
+ return null;
+ }
+
+ #[Override]
+ protected function tryCast(mixed $value): ?BackedEnum
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ if ($value instanceof $this->enumClass) {
+ return $value;
+ }
+
+ throw new TypeError(sprintf("The import()'ed value of a %s must be an instance of %s or null", self::class, $this->enumClass));
+ }
+
+ #[Override]
+ protected function sanitize(mixed $rawValue)
+ {
+ return $rawValue;
+ }
+}
diff --git a/src/Leaf/EnumElementBuilder.php b/src/Leaf/EnumElementBuilder.php
new file mode 100644
index 0000000..a0d08c0
--- /dev/null
+++ b/src/Leaf/EnumElementBuilder.php
@@ -0,0 +1,114 @@
+
+ * $builder->enum('type', EnumType::class)
+ * ->backed(false) // Force the use of case name instead of its value
+ * ;
+ *
+ *
+ * @see UnitEnumElement
+ * @see BackedEnumElement
+ * @see FormBuilderInterface::enum()
+ *
+ * @extends AbstractElementBuilder
+ */
+final class EnumElementBuilder extends AbstractElementBuilder
+{
+ use ChoiceBuilderTrait;
+
+ /**
+ * @var class-string|null
+ */
+ private ?string $enumClass = null;
+
+ /**
+ * Use the backed value of the enum to resolve it:
+ * - If true, the enum will be resolved using the backed value (BackedEnum)
+ * - If false, the case name will be used
+ * - If null, the behavior will depend on the enum type (backed or not)
+ */
+ private ?bool $backedEnum = null;
+
+ public function __construct(?RegistryInterface $registry = null)
+ {
+ parent::__construct($registry);
+ }
+
+ /**
+ * Define the enum class
+ * This method should not be called manually, and will fail if an enum class is already defined.
+ *
+ * @param class-string $enumClass
+ * @return $this
+ *
+ * @internal
+ */
+ public function enumClass(string $enumClass): static
+ {
+ if ($this->enumClass !== null) {
+ throw new LogicException(sprintf('Enum class "%s" is already defined.', $enumClass));
+ }
+
+ $this->enumClass = $enumClass;
+ return $this;
+ }
+
+ /**
+ * Use the backed value of the enum to resolve it:
+ * - If true, the enum will be resolved using the backed value (BackedEnum)
+ * - If false, the case name will be used
+ * - If null, the behavior will depend on the enum type (backed or not)
+ *
+ * @param bool|null $flag
+ * @return $this
+ */
+ public function backed(?bool $flag = true): static
+ {
+ if ($this->enumClass !== null && $flag && !is_subclass_of($this->enumClass, BackedEnum::class)) {
+ throw new InvalidArgumentException(sprintf('The enum class "%s" is not a backed enum', $this->enumClass));
+ }
+
+ $this->backedEnum = $flag;
+
+ return $this;
+ }
+
+ #[Override]
+ protected function createElement(ValueValidatorInterface $validator, TransformerInterface $transformer): UnitEnumElement|BackedEnumElement
+ {
+ $enumClass = $this->enumClass;
+ $isBacked = $this->backedEnum;
+
+ if ($enumClass === null) {
+ throw new LogicException('An enum class must be provided');
+ }
+
+ $isBacked ??= is_subclass_of($enumClass, BackedEnum::class);
+ assert(!$isBacked || is_subclass_of($enumClass, BackedEnum::class));
+
+ return $isBacked
+ ? new BackedEnumElement($enumClass, $validator, $transformer, $this->choices)
+ : new UnitEnumElement($enumClass, $validator, $transformer, $this->choices)
+ ;
+ }
+}
diff --git a/src/Leaf/UnitEnumElement.php b/src/Leaf/UnitEnumElement.php
new file mode 100644
index 0000000..9d8b6b9
--- /dev/null
+++ b/src/Leaf/UnitEnumElement.php
@@ -0,0 +1,80 @@
+
+ */
+final class UnitEnumElement extends LeafElement
+{
+ public function __construct(
+ /**
+ * @var class-string
+ */
+ private readonly string $enumClass,
+
+ ?ValueValidatorInterface $validator = null,
+ ?TransformerInterface $transformer = null,
+ ?ChoiceInterface $choices = null
+ ) {
+ parent::__construct($validator, $transformer, $choices);
+ }
+
+ #[Override]
+ protected function toPhp(mixed $httpValue): ?UnitEnum
+ {
+ if (is_string($httpValue) && defined($this->enumClass . '::' . $httpValue)) {
+ return $this->enumClass::{$httpValue};
+ }
+
+ if ($httpValue instanceof $this->enumClass) {
+ return $httpValue;
+ }
+
+ return null;
+ }
+
+ #[Override]
+ protected function toHttp(mixed $phpValue): string|int|null
+ {
+ if ($phpValue instanceof $this->enumClass) {
+ return $phpValue->name;
+ }
+
+ return null;
+ }
+
+ #[Override]
+ protected function tryCast(mixed $value): ?UnitEnum
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ if ($value instanceof $this->enumClass) {
+ return $value;
+ }
+
+ throw new TypeError(sprintf("The import()'ed value of a %s must be an instance of %s or null", self::class, $this->enumClass));
+ }
+
+ #[Override]
+ protected function sanitize(mixed $rawValue)
+ {
+ return $rawValue;
+ }
+}
diff --git a/src/Registry/Registry.php b/src/Registry/Registry.php
index 0b7aa04..817e1f7 100755
--- a/src/Registry/Registry.php
+++ b/src/Registry/Registry.php
@@ -18,11 +18,13 @@
use Bdf\Form\ElementBuilderInterface;
use Bdf\Form\Leaf\AnyElement;
use Bdf\Form\Leaf\AnyElementBuilder;
+use Bdf\Form\Leaf\BackedEnumElement;
use Bdf\Form\Leaf\BooleanElement;
use Bdf\Form\Leaf\BooleanElementBuilder;
use Bdf\Form\Leaf\Date\DateTimeChildBuilder;
use Bdf\Form\Leaf\Date\DateTimeElement;
use Bdf\Form\Leaf\Date\DateTimeElementBuilder;
+use Bdf\Form\Leaf\EnumElementBuilder;
use Bdf\Form\Leaf\FloatElement;
use Bdf\Form\Leaf\FloatElementBuilder;
use Bdf\Form\Leaf\Helper\EmailElement;
@@ -33,6 +35,7 @@
use Bdf\Form\Leaf\IntegerElementBuilder;
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\Leaf\StringElementBuilder;
+use Bdf\Form\Leaf\UnitEnumElement;
use Bdf\Form\Phone\PhoneChildBuilder;
use Bdf\Form\Phone\PhoneElement;
use Bdf\Form\Phone\PhoneElementBuilder;
@@ -56,6 +59,8 @@ final class Registry implements RegistryInterface
FloatElement::class => FloatElementBuilder::class,
BooleanElement::class => BooleanElementBuilder::class,
AnyElement::class => AnyElementBuilder::class,
+ BackedEnumElement::class => EnumElementBuilder::class,
+ UnitEnumElement::class => EnumElementBuilder::class,
EmailElement::class => EmailElementBuilder::class,
UrlElement::class => UrlElementBuilder::class,
diff --git a/tests/Aggregate/ArrayElementBuilderTest.php b/tests/Aggregate/ArrayElementBuilderTest.php
index 2593d89..3e09f3a 100644
--- a/tests/Aggregate/ArrayElementBuilderTest.php
+++ b/tests/Aggregate/ArrayElementBuilderTest.php
@@ -2,11 +2,15 @@
namespace Bdf\Form\Aggregate;
+use Bdf\Form\Choice\EnumChoice;
use Bdf\Form\Choice\LazyChoice;
+use Bdf\Form\Leaf\AnyElementBuilder;
use Bdf\Form\Leaf\BooleanElementBuilder;
use Bdf\Form\Leaf\Date\DateTimeElementBuilder;
+use Bdf\Form\Leaf\EnumElementBuilder;
use Bdf\Form\Leaf\FloatElementBuilder;
use Bdf\Form\Leaf\IntegerElementBuilder;
+use Bdf\Form\Leaf\MyStringEnum;
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\Leaf\StringElementBuilder;
use Bdf\Form\Phone\PhoneElementBuilder;
@@ -14,6 +18,7 @@
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;
use PHPUnit\Framework\TestCase;
+use stdClass;
use Symfony\Component\Validator\Constraints\Count;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotEqualTo;
@@ -173,6 +178,24 @@ public function test_phone_with_configurator()
$this->assertEquals('+33241578932', PhoneNumberUtil::getInstance()->format($phones[1], PhoneNumberFormat::E164));
}
+ public function test_any()
+ {
+ $element = $this->builder->any(function (AnyElementBuilder $builder) {})->buildElement();
+
+ $values = $element->submit($data = ['foo', new stdClass()])->value();
+
+ $this->assertSame($data, $values);
+ }
+
+ public function test_enum()
+ {
+ $element = $this->builder->enum(MyStringEnum::class, function (EnumElementBuilder $builder) {})->buildElement();
+
+ $values = $element->submit(['bar'])->value();
+
+ $this->assertSame([MyStringEnum::Bar], $values);
+ }
+
/**
*
*/
@@ -339,5 +362,12 @@ public function test_choices()
$this->assertEquals('One or more of the given values is invalid.', $element->error()->global());
$this->assertTrue($element->submit(['foo', 'baz'])->valid());
+
+ $element = $this->builder->choices(new EnumChoice(MyStringEnum::class))->buildElement();
+
+ $this->assertFalse($element->submit(['foo', 'bar', 'aaa'])->valid());
+ $this->assertEquals('One or more of the given values is invalid.', $element->error()->global());
+
+ $this->assertTrue($element->submit(['foo', 'bar'])->valid());
}
}
diff --git a/tests/Aggregate/FormBuilderTest.php b/tests/Aggregate/FormBuilderTest.php
index 51464bc..9e2dced 100644
--- a/tests/Aggregate/FormBuilderTest.php
+++ b/tests/Aggregate/FormBuilderTest.php
@@ -6,6 +6,7 @@
use Bdf\Form\Child\ChildBuilder;
use Bdf\Form\Csrf\CsrfElement;
use Bdf\Form\Leaf\AnyElement;
+use Bdf\Form\Leaf\BackedEnumElement;
use Bdf\Form\Leaf\BooleanElement;
use Bdf\Form\Leaf\Date\DateTimeChildBuilder;
use Bdf\Form\Leaf\Date\DateTimeElement;
@@ -14,6 +15,7 @@
use Bdf\Form\Leaf\Helper\UrlElement;
use Bdf\Form\Leaf\IntegerElement;
use Bdf\Form\Leaf\IntegerElementBuilder;
+use Bdf\Form\Leaf\MyStringEnum;
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\Phone\FormattedPhoneElement;
use Bdf\Form\Phone\PhoneChildBuilder;
@@ -159,6 +161,22 @@ public function test_url()
$this->assertInstanceOf(UrlElement::class, $form['value']->element());
}
+ /**
+ *
+ */
+ public function test_enum()
+ {
+ $this->assertInstanceOf(ChildBuilder::class, $this->builder->enum('value', MyStringEnum::class));
+
+ $form = $this->builder->buildElement();
+
+ $this->assertInstanceOf(Form::class, $form);
+ $this->assertInstanceOf(BackedEnumElement::class, $form['value']->element());
+
+ $form->submit(['value' => 'foo']);
+ $this->assertSame(MyStringEnum::Foo, $form['value']->element()->value());
+ }
+
/**
*
*/
diff --git a/tests/Choice/EnumChoiceTest.php b/tests/Choice/EnumChoiceTest.php
new file mode 100644
index 0000000..9cba450
--- /dev/null
+++ b/tests/Choice/EnumChoiceTest.php
@@ -0,0 +1,84 @@
+assertSame([
+ 'Foo' => 'value',
+ 'Bar' => 'other',
+ ], $choice->values());
+
+ $view = $choice->view();
+
+ $this->assertEquals([
+ new ChoiceView('value', 'Foo'),
+ new ChoiceView('other', 'Bar'),
+ ], $view);
+ }
+
+ /**
+ *
+ */
+ public function test_with_label_closure()
+ {
+ $choice = new EnumChoice(
+ MyStringEnum::class,
+ static fn (MyStringEnum $enum) => $enum->name . ' (' . $enum->value . ')',
+ );
+
+ $this->assertSame([
+ 'Foo (value)' => 'value',
+ 'Bar (other)' => 'other',
+ ], $choice->values());
+
+ $view = $choice->view();
+
+ $this->assertEquals([
+ new ChoiceView('value', 'Foo (value)'),
+ new ChoiceView('other', 'Bar (other)'),
+ ], $view);
+ }
+
+ /**
+ *
+ */
+ public function test_view_with_configurator()
+ {
+ $choice = new EnumChoice(MyStringEnum::class);
+
+ $view = $choice->view(
+ static function (ChoiceView $view) {
+ $view->setSelected($view->value === 'other');
+ },
+ );
+
+ $expected = [
+ new ChoiceView('value', 'Foo'),
+ new ChoiceView('other', 'Bar'),
+ ];
+ $expected[1]->setSelected(true);
+
+ $this->assertEquals($expected, $view);
+ }
+
+}
+
+enum MyStringEnum: string
+{
+ case Foo = 'value';
+ case Bar = 'other';
+}
+
diff --git a/tests/Leaf/BackedEnumElementTest.php b/tests/Leaf/BackedEnumElementTest.php
new file mode 100644
index 0000000..1fecdfb
--- /dev/null
+++ b/tests/Leaf/BackedEnumElementTest.php
@@ -0,0 +1,339 @@
+assertFalse($element->valid());
+ $this->assertTrue($element->failed());
+ $this->assertNull($element->value());
+ $this->assertTrue($element->error()->empty());
+ }
+
+ /**
+ *
+ */
+ public function test_submit_success()
+ {
+ $element = new BackedEnumElement(TestBackedEnum::class);
+
+ $this->assertTrue($element->submit('foo')->valid());
+ $this->assertFalse($element->failed());
+ $this->assertSame(TestBackedEnum::Foo, $element->value());
+ $this->assertTrue($element->error()->empty());
+
+ $this->assertTrue($element->submit(TestBackedEnum::Bar)->valid());
+ $this->assertFalse($element->failed());
+ $this->assertSame(TestBackedEnum::Bar, $element->value());
+ $this->assertTrue($element->error()->empty());
+ }
+
+ #[
+ TestWith([false]),
+ TestWith([new stdClass()]),
+ TestWith([TestUnitEnum::Bar]),
+ TestWith(['invalid']),
+ TestWith([42]),
+ TestWith([42.1]),
+ ]
+ public function test_submit_invalid(mixed $value)
+ {
+ $element = new BackedEnumElement(TestBackedEnum::class);
+
+ $this->assertTrue($element->submit($value)->valid());
+ $this->assertFalse($element->failed());
+ $this->assertNull($element->value());
+ $this->assertTrue($element->error()->empty());
+ }
+
+ /**
+ *
+ */
+ public function test_submit_null()
+ {
+ $element = new BackedEnumElement(TestBackedEnum::class);
+
+ $this->assertTrue($element->submit(null)->valid());
+ $this->assertNull($element->value());
+ $this->assertTrue($element->error()->empty());
+ }
+
+ /**
+ *
+ */
+ public function test_submit_with_constraint()
+ {
+ $element = new BackedEnumElement(TestBackedEnum::class, new ConstraintValueValidator([new NotBlank()]));
+
+ $this->assertFalse($element->submit(null)->valid());
+ $this->assertNull($element->value());
+ $this->assertEquals('This value should not be blank.', $element->error()->global());
+
+ $this->assertTrue($element->submit('foo')->valid());
+ $this->assertSame(TestBackedEnum::Foo, $element->value());
+ $this->assertTrue($element->error()->empty());
+ }
+
+ /**
+ *
+ */
+ public function test_submit_with_transformer_exception()
+ {
+ $transformer = $this->createMock(TransformerInterface::class);
+ $transformer->expects($this->once())->method('transformFromHttp')->willThrowException(new TransformationFailedException('my error'));
+ $element = new BackedEnumElement(TestBackedEnum::class, transformer: $transformer);
+
+ $this->assertFalse($element->submit('aa')->valid());
+ $this->assertSame('aa', $element->value());
+ $this->assertEquals('my error', $element->error()->global());
+ }
+
+ /**
+ *
+ */
+ public function test_submit_with_transformer_exception_ignored()
+ {
+ $transformer = $this->createMock(TransformerInterface::class);
+ $transformer->expects($this->once())->method('transformFromHttp')->willThrowException(new TransformationFailedException('my error'));
+ $element = new BackedEnumElement(
+ TestBackedEnum::class,
+ validator: new ConstraintValueValidator([], new TransformerExceptionConstraint(ignoreException: true)),
+ transformer: $transformer
+ );
+
+ $this->assertTrue($element->submit('aa')->valid());
+ $this->assertSame('aa', $element->value());
+ }
+
+ /**
+ *
+ */
+ public function test_submit_with_transformer_exception_ignored_should_validate_other_constraints()
+ {
+ $transformer = $this->createMock(TransformerInterface::class);
+ $transformer->expects($this->once())->method('transformFromHttp')->willThrowException(new TransformationFailedException('my error'));
+ $element = new BackedEnumElement(
+ TestBackedEnum::class,
+ new ConstraintValueValidator(
+ [new Closure(function () { return 'validation error'; })],
+ new TransformerExceptionConstraint(ignoreException: true)
+ ),
+ $transformer
+ );
+
+ $this->assertFalse($element->submit('aa')->valid());
+ $this->assertSame('aa', $element->value());
+ $this->assertEquals('validation error', $element->error()->global());
+ }
+
+ /**
+ *
+ */
+ public function test_transformer()
+ {
+ $element = new BackedEnumElement(TestBackedEnum::class, transformer: new ClosureTransformer(function ($value, $_, $toPhp) {
+ if ($toPhp) {
+ return strtolower($value);
+ } else {
+ return strtoupper($value);
+ }
+ }));
+
+ $element->submit('foO')->valid();
+ $this->assertSame(TestBackedEnum::Foo, $element->value());
+ $this->assertSame('FOO', $element->httpValue());
+ }
+
+ #[DataProvider('provideValidValues')]
+ public function test_import($value, $expected)
+ {
+ $element = new BackedEnumElement(TestBackedEnum::class);
+
+ $this->assertSame($expected, $element->import($value)->value());
+ }
+
+ public static function provideValidValues()
+ {
+ return [
+ [TestBackedEnum::Foo, TestBackedEnum::Foo],
+ [null, null],
+ ];
+ }
+
+ #[DataProvider('provideInvalidValue')]
+ public function test_import_invalid_type($value)
+ {
+ $this->expectException(\TypeError::class);
+ $this->expectExceptionMessage('The import()\'ed value of a Bdf\Form\Leaf\BackedEnumElement must be an instance of Bdf\Form\Leaf\Fixtures\TestBackedEnum or null');
+ $element = new BackedEnumElement(TestBackedEnum::class);
+
+ $element->import($value);
+ }
+
+ /**
+ *
+ */
+ public static function provideInvalidValue()
+ {
+ return [
+ [[]],
+ [new \stdClass()],
+ [STDIN],
+ ['foo'],
+ [123],
+ [TestUnitEnum::Foo],
+ ];
+ }
+
+ /**
+ *
+ */
+ public function test_httpValue()
+ {
+ $element = new BackedEnumElement(TestBackedEnum::class);
+
+ $this->assertSame('bar', $element->import(TestBackedEnum::Bar)->httpValue());
+ }
+
+ /**
+ *
+ */
+ public function test_container()
+ {
+ $element = new BackedEnumElement(TestBackedEnum::class);
+
+ $this->assertNull($element->container());
+
+ $container = new Child('name', $element);
+ $newElement = $element->setContainer($container);
+
+ $this->assertNotSame($element, $newElement);
+ $this->assertSame($container, $newElement->container());
+ }
+
+ /**
+ *
+ */
+ public function test_root_without_container()
+ {
+ $element = new BackedEnumElement(TestBackedEnum::class);
+
+ $this->assertInstanceOf(LeafRootElement::class, $element->root());
+ }
+
+ /**
+ *
+ */
+ public function test_root_with_container()
+ {
+ $element = new BackedEnumElement(TestBackedEnum::class);
+
+ $this->assertNull($element->container());
+
+ $container = new Child('name', $element);
+ $container->setParent($form = new Form(new ChildrenCollection()));
+
+ $element = $element->setContainer($container);
+
+ $this->assertSame($container->parent()->root(), $element->root());
+ }
+
+ /**
+ *
+ */
+ public function test_view()
+ {
+ $element = new BackedEnumElement(TestBackedEnum::class);
+ $element->import(TestBackedEnum::Bar);
+
+ $view = $element->view(HttpFieldPath::named('name'));
+
+ $this->assertEquals('', (string) $view);
+ $this->assertEquals('', (string) $view->id('foo')->class('form-element'));
+ $this->assertNull($view->onError('my error'));
+
+ $this->assertEquals('bar', $view->value());
+ $this->assertEquals('name', $view->name());
+ $this->assertFalse($view->hasError());
+ $this->assertNull($view->error());
+ $this->assertFalse($view->required());
+ $this->assertEmpty($view->constraints());
+
+ $element->import(null);
+
+ $view = $element->view(HttpFieldPath::named('name'));
+
+ $this->assertEquals('', (string) $view);
+ $this->assertEquals('', (string) $view->id('foo')->class('form-element'));
+
+ $this->assertEquals('', $view->value());
+
+ $this->assertEquals('', (string) $element->view());
+ }
+
+ /**
+ *
+ */
+ public function test_view_not_submitted()
+ {
+ $element = new BackedEnumElement(TestBackedEnum::class);
+
+ $view = $element->view(HttpFieldPath::named('name'));
+
+ $this->assertEquals('', (string) $view);
+ $this->assertEquals('', (string) $view->id('foo')->class('form-element'));
+ $this->assertNull($view->onError('my error'));
+
+ $this->assertNull($view->value());
+ $this->assertEquals('name', $view->name());
+ $this->assertFalse($view->hasError());
+ $this->assertNull($view->error());
+ $this->assertFalse($view->required());
+ $this->assertEmpty($view->constraints());
+ }
+
+ /**
+ *
+ */
+ public function test_error()
+ {
+ $element = (new EnumElementBuilder())->enumClass(BackedEnumElement::class)->satisfy(function() { return false; })->buildElement();
+ $element->submit('ok');
+
+ $error = $element->error(HttpFieldPath::named('foo'));
+
+ $this->assertEquals('foo', $error->field());
+ $this->assertEquals('The value is invalid', $error->global());
+ $this->assertEquals('CUSTOM_ERROR', $error->code());
+ $this->assertEmpty($error->children());
+ }
+}
diff --git a/tests/Leaf/EnumElementBuilderTest.php b/tests/Leaf/EnumElementBuilderTest.php
new file mode 100644
index 0000000..0aa0e5c
--- /dev/null
+++ b/tests/Leaf/EnumElementBuilderTest.php
@@ -0,0 +1,147 @@
+builder = new EnumElementBuilder();
+ }
+
+ /**
+ *
+ */
+ public function test_buildElement_backed()
+ {
+ $element = $this->builder->enumClass(Fixtures\TestBackedEnum::class)->buildElement();
+ $this->assertInstanceOf(BackedEnumElement::class, $element);
+ }
+
+ /**
+ *
+ */
+ public function test_buildElement_unit()
+ {
+ $element = $this->builder->enumClass(Fixtures\TestUnitEnum::class)->buildElement();
+ $this->assertInstanceOf(UnitEnumElement::class, $element);
+ }
+
+ /**
+ *
+ */
+ public function test_buildElement_backed_false_with_backed_enum()
+ {
+ $element = $this->builder->enumClass(Fixtures\TestBackedEnum::class)->backed(false)->buildElement();
+ $this->assertInstanceOf(UnitEnumElement::class, $element);
+ }
+
+ /**
+ /**
+ *
+ */
+ public function test_buildElement_backed_true_with_unit_enum_should_fail()
+ {
+ $this->expectException(\LogicException::class);
+ $this->expectExceptionMessage('The enum class "Bdf\Form\Leaf\Fixtures\TestUnitEnum" is not a backed enum');
+
+ $this->builder->enumClass(Fixtures\TestUnitEnum::class)->backed(true)->buildElement();
+ }
+
+ /**
+ *
+ */
+ public function test_satisfy()
+ {
+ $element = $this->builder->enumClass(Fixtures\TestBackedEnum::class)->satisfy(new NotEqualTo(Fixtures\TestBackedEnum::Foo))->buildElement();
+
+ $this->assertFalse($element->submit('foo')->valid());
+ $this->assertTrue($element->submit('bar')->valid());
+ }
+
+ /**
+ *
+ */
+ public function test_transformer()
+ {
+ $element = $this->builder
+ ->enumClass(Fixtures\TestBackedEnum::class)
+ ->transformer(static fn ($value) => strtolower($value))
+ ->buildElement()
+ ;
+
+ $this->assertEquals(Fixtures\TestBackedEnum::Foo, $element->submit('FOO')->value());
+ }
+
+ /**
+ *
+ */
+ public function test_value()
+ {
+ $element = $this->builder->enumClass(Fixtures\TestBackedEnum::class)->value(Fixtures\TestBackedEnum::Foo)->buildElement();
+
+ $this->assertSame(Fixtures\TestBackedEnum::Foo, $element->value());
+ }
+
+ /**
+ *
+ */
+ public function test_required()
+ {
+ $element = $this->builder->enumClass(Fixtures\TestBackedEnum::class)->required()->buildElement();
+
+ $element->submit(null);
+ $this->assertEquals('This value should not be blank.', $element->error()->global());
+ }
+
+ /**
+ *
+ */
+ public function test_required_with_custom_message()
+ {
+ $element = $this->builder->enumClass(Fixtures\TestBackedEnum::class)->required('my message')->buildElement();
+
+ $element->submit(null);
+ $this->assertEquals('my message', $element->error()->global());
+ }
+
+ /**
+ *
+ */
+ public function test_choices()
+ {
+ $element = $this->builder->enumClass(Fixtures\TestBackedEnum::class)->choices([Fixtures\TestBackedEnum::Bar])->buildElement();
+
+ $this->assertEquals(new ArrayChoice([Fixtures\TestBackedEnum::Bar]), $element->choices());
+
+ $element->submit('foo');
+ $this->assertFalse($element->valid());
+ $this->assertTrue($element->failed());
+ $this->assertEquals('The value you selected is not a valid choice.', $element->error()->global());
+ }
+
+ /**
+ *
+ */
+ public function test_choices_custom_message()
+ {
+ $element = $this->builder->enumClass(Fixtures\TestBackedEnum::class)->choices([Fixtures\TestBackedEnum::Bar], 'my error')->buildElement();
+
+ $element->submit('foo');
+ $this->assertFalse($element->valid());
+ $this->assertTrue($element->failed());
+ $this->assertEquals('my error', $element->error()->global());
+ }
+}
diff --git a/tests/Leaf/Fixtures/TestBackedEnum.php b/tests/Leaf/Fixtures/TestBackedEnum.php
new file mode 100644
index 0000000..7e25b0e
--- /dev/null
+++ b/tests/Leaf/Fixtures/TestBackedEnum.php
@@ -0,0 +1,9 @@
+assertTrue($element->failed());
$this->assertEquals('my error', $element->error()->global());
}
+
+ /**
+ *
+ */
+ public function test_choices_enum()
+ {
+ $element = $this->builder->choices(MyIntEnum::class)->buildElement();
+
+ $this->assertEquals(new EnumChoice(MyIntEnum::class), $element->choices());
+
+ $element->submit(12);
+ $this->assertFalse($element->valid());
+ $this->assertTrue($element->failed());
+ $this->assertEquals('The value you selected is not a valid choice.', $element->error()->global());
+
+ $element->submit(42);
+ $this->assertTrue($element->valid());
+
+ $view = $element->view()->choices();
+ $this->assertTrue($view[0]->selected);
+ $this->assertSame('Foo', $view[0]->label);
+ $this->assertSame('42', $view[0]->value);
+ $this->assertFalse($view[1]->selected);
+ $this->assertSame('Bar', $view[1]->label);
+ $this->assertSame('121', $view[1]->value);
+ }
+}
+
+enum MyIntEnum: int
+{
+ case Foo = 42;
+ case Bar = 121;
}
diff --git a/tests/Leaf/StringElementBuilderTest.php b/tests/Leaf/StringElementBuilderTest.php
index 8463906..94b7b6f 100644
--- a/tests/Leaf/StringElementBuilderTest.php
+++ b/tests/Leaf/StringElementBuilderTest.php
@@ -3,6 +3,7 @@
namespace Bdf\Form\Leaf;
use Bdf\Form\Choice\ArrayChoice;
+use Bdf\Form\Choice\EnumChoice;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraints\NotEqualTo;
use Symfony\Component\Validator\Constraints\Positive;
@@ -182,4 +183,36 @@ public function test_choices_custom_message()
$this->assertTrue($element->failed());
$this->assertEquals('my error', $element->error()->global());
}
+
+ /**
+ *
+ */
+ public function test_choices_enum()
+ {
+ $element = $this->builder->choices(MyStringEnum::class)->buildElement();
+
+ $this->assertEquals(new EnumChoice(MyStringEnum::class), $element->choices());
+
+ $element->submit('aaa');
+ $this->assertFalse($element->valid());
+ $this->assertTrue($element->failed());
+ $this->assertEquals('The value you selected is not a valid choice.', $element->error()->global());
+
+ $element->submit('foo');
+ $this->assertTrue($element->valid());
+
+ $view = $element->view()->choices();
+ $this->assertTrue($view[0]->selected);
+ $this->assertSame('Foo', $view[0]->label);
+ $this->assertSame('foo', $view[0]->value);
+ $this->assertFalse($view[1]->selected);
+ $this->assertSame('Bar', $view[1]->label);
+ $this->assertSame('bar', $view[1]->value);
+ }
+}
+
+enum MyStringEnum: string
+{
+ case Foo = 'foo';
+ case Bar = 'bar';
}
diff --git a/tests/Leaf/UnitEnumElementTest.php b/tests/Leaf/UnitEnumElementTest.php
new file mode 100644
index 0000000..64b94e7
--- /dev/null
+++ b/tests/Leaf/UnitEnumElementTest.php
@@ -0,0 +1,339 @@
+assertFalse($element->valid());
+ $this->assertTrue($element->failed());
+ $this->assertNull($element->value());
+ $this->assertTrue($element->error()->empty());
+ }
+
+ /**
+ *
+ */
+ public function test_submit_success()
+ {
+ $element = new UnitEnumElement(TestUnitEnum::class);
+
+ $this->assertTrue($element->submit('Foo')->valid());
+ $this->assertFalse($element->failed());
+ $this->assertSame(TestUnitEnum::Foo, $element->value());
+ $this->assertTrue($element->error()->empty());
+
+ $this->assertTrue($element->submit(TestUnitEnum::Bar)->valid());
+ $this->assertFalse($element->failed());
+ $this->assertSame(TestUnitEnum::Bar, $element->value());
+ $this->assertTrue($element->error()->empty());
+ }
+
+ #[
+ TestWith([false]),
+ TestWith([new stdClass()]),
+ TestWith([TestBackedEnum::Bar]),
+ TestWith(['invalid']),
+ TestWith([42]),
+ TestWith([42.1]),
+ ]
+ public function test_submit_invalid(mixed $value)
+ {
+ $element = new UnitEnumElement(TestUnitEnum::class);
+
+ $this->assertTrue($element->submit($value)->valid());
+ $this->assertFalse($element->failed());
+ $this->assertNull($element->value());
+ $this->assertTrue($element->error()->empty());
+ }
+
+ /**
+ *
+ */
+ public function test_submit_null()
+ {
+ $element = new UnitEnumElement(TestUnitEnum::class);
+
+ $this->assertTrue($element->submit(null)->valid());
+ $this->assertNull($element->value());
+ $this->assertTrue($element->error()->empty());
+ }
+
+ /**
+ *
+ */
+ public function test_submit_with_constraint()
+ {
+ $element = new UnitEnumElement(TestUnitEnum::class, new ConstraintValueValidator([new NotBlank()]));
+
+ $this->assertFalse($element->submit(null)->valid());
+ $this->assertNull($element->value());
+ $this->assertEquals('This value should not be blank.', $element->error()->global());
+
+ $this->assertTrue($element->submit('Foo')->valid());
+ $this->assertSame(TestUnitEnum::Foo, $element->value());
+ $this->assertTrue($element->error()->empty());
+ }
+
+ /**
+ *
+ */
+ public function test_submit_with_transformer_exception()
+ {
+ $transformer = $this->createMock(TransformerInterface::class);
+ $transformer->expects($this->once())->method('transformFromHttp')->willThrowException(new TransformationFailedException('my error'));
+ $element = new UnitEnumElement(TestUnitEnum::class, transformer: $transformer);
+
+ $this->assertFalse($element->submit('aa')->valid());
+ $this->assertSame('aa', $element->value());
+ $this->assertEquals('my error', $element->error()->global());
+ }
+
+ /**
+ *
+ */
+ public function test_submit_with_transformer_exception_ignored()
+ {
+ $transformer = $this->createMock(TransformerInterface::class);
+ $transformer->expects($this->once())->method('transformFromHttp')->willThrowException(new TransformationFailedException('my error'));
+ $element = new UnitEnumElement(
+ TestUnitEnum::class,
+ validator: new ConstraintValueValidator([], new TransformerExceptionConstraint(ignoreException: true)),
+ transformer: $transformer
+ );
+
+ $this->assertTrue($element->submit('aa')->valid());
+ $this->assertSame('aa', $element->value());
+ }
+
+ /**
+ *
+ */
+ public function test_submit_with_transformer_exception_ignored_should_validate_other_constraints()
+ {
+ $transformer = $this->createMock(TransformerInterface::class);
+ $transformer->expects($this->once())->method('transformFromHttp')->willThrowException(new TransformationFailedException('my error'));
+ $element = new UnitEnumElement(
+ TestUnitEnum::class,
+ new ConstraintValueValidator(
+ [new Closure(function () { return 'validation error'; })],
+ new TransformerExceptionConstraint(ignoreException: true)
+ ),
+ $transformer
+ );
+
+ $this->assertFalse($element->submit('aa')->valid());
+ $this->assertSame('aa', $element->value());
+ $this->assertEquals('validation error', $element->error()->global());
+ }
+
+ /**
+ *
+ */
+ public function test_transformer()
+ {
+ $element = new UnitEnumElement(TestUnitEnum::class, transformer: new ClosureTransformer(function ($value, $_, $toPhp) {
+ if ($toPhp) {
+ return ucfirst(strtolower($value));
+ } else {
+ return strtoupper($value);
+ }
+ }));
+
+ $element->submit('foO')->valid();
+ $this->assertSame(TestUnitEnum::Foo, $element->value());
+ $this->assertSame('FOO', $element->httpValue());
+ }
+
+ #[DataProvider('provideValidValues')]
+ public function test_import($value, $expected)
+ {
+ $element = new UnitEnumElement(TestUnitEnum::class);
+
+ $this->assertSame($expected, $element->import($value)->value());
+ }
+
+ public static function provideValidValues()
+ {
+ return [
+ [TestUnitEnum::Foo, TestUnitEnum::Foo],
+ [null, null],
+ ];
+ }
+
+ #[DataProvider('provideInvalidValue')]
+ public function test_import_invalid_type($value)
+ {
+ $this->expectException(\TypeError::class);
+ $this->expectExceptionMessage('The import()\'ed value of a Bdf\Form\Leaf\UnitEnumElement must be an instance of Bdf\Form\Leaf\Fixtures\TestUnitEnum or null');
+ $element = new UnitEnumElement(TestUnitEnum::class);
+
+ $element->import($value);
+ }
+
+ /**
+ *
+ */
+ public static function provideInvalidValue()
+ {
+ return [
+ [[]],
+ [new \stdClass()],
+ [STDIN],
+ ['foo'],
+ [123],
+ [TestBackedEnum::Foo],
+ ];
+ }
+
+ /**
+ *
+ */
+ public function test_httpValue()
+ {
+ $element = new UnitEnumElement(TestUnitEnum::class);
+
+ $this->assertSame('Bar', $element->import(TestUnitEnum::Bar)->httpValue());
+ }
+
+ /**
+ *
+ */
+ public function test_container()
+ {
+ $element = new UnitEnumElement(TestUnitEnum::class);
+
+ $this->assertNull($element->container());
+
+ $container = new Child('name', $element);
+ $newElement = $element->setContainer($container);
+
+ $this->assertNotSame($element, $newElement);
+ $this->assertSame($container, $newElement->container());
+ }
+
+ /**
+ *
+ */
+ public function test_root_without_container()
+ {
+ $element = new UnitEnumElement(TestUnitEnum::class);
+
+ $this->assertInstanceOf(LeafRootElement::class, $element->root());
+ }
+
+ /**
+ *
+ */
+ public function test_root_with_container()
+ {
+ $element = new UnitEnumElement(TestUnitEnum::class);
+
+ $this->assertNull($element->container());
+
+ $container = new Child('name', $element);
+ $container->setParent($form = new Form(new ChildrenCollection()));
+
+ $element = $element->setContainer($container);
+
+ $this->assertSame($container->parent()->root(), $element->root());
+ }
+
+ /**
+ *
+ */
+ public function test_view()
+ {
+ $element = new UnitEnumElement(TestUnitEnum::class);
+ $element->import(TestUnitEnum::Bar);
+
+ $view = $element->view(HttpFieldPath::named('name'));
+
+ $this->assertEquals('', (string) $view);
+ $this->assertEquals('', (string) $view->id('foo')->class('form-element'));
+ $this->assertNull($view->onError('my error'));
+
+ $this->assertEquals('Bar', $view->value());
+ $this->assertEquals('name', $view->name());
+ $this->assertFalse($view->hasError());
+ $this->assertNull($view->error());
+ $this->assertFalse($view->required());
+ $this->assertEmpty($view->constraints());
+
+ $element->import(null);
+
+ $view = $element->view(HttpFieldPath::named('name'));
+
+ $this->assertEquals('', (string) $view);
+ $this->assertEquals('', (string) $view->id('foo')->class('form-element'));
+
+ $this->assertEquals('', $view->value());
+
+ $this->assertEquals('', (string) $element->view());
+ }
+
+ /**
+ *
+ */
+ public function test_view_not_submitted()
+ {
+ $element = new UnitEnumElement(TestUnitEnum::class);
+
+ $view = $element->view(HttpFieldPath::named('name'));
+
+ $this->assertEquals('', (string) $view);
+ $this->assertEquals('', (string) $view->id('foo')->class('form-element'));
+ $this->assertNull($view->onError('my error'));
+
+ $this->assertNull($view->value());
+ $this->assertEquals('name', $view->name());
+ $this->assertFalse($view->hasError());
+ $this->assertNull($view->error());
+ $this->assertFalse($view->required());
+ $this->assertEmpty($view->constraints());
+ }
+
+ /**
+ *
+ */
+ public function test_error()
+ {
+ $element = (new EnumElementBuilder())->enumClass(UnitEnumElement::class)->satisfy(function() { return false; })->buildElement();
+ $element->submit('ok');
+
+ $error = $element->error(HttpFieldPath::named('foo'));
+
+ $this->assertEquals('foo', $error->field());
+ $this->assertEquals('The value is invalid', $error->global());
+ $this->assertEquals('CUSTOM_ERROR', $error->code());
+ $this->assertEmpty($error->children());
+ }
+}
diff --git a/tests/Registry/RegistryTest.php b/tests/Registry/RegistryTest.php
index 2715219..97e8656 100644
--- a/tests/Registry/RegistryTest.php
+++ b/tests/Registry/RegistryTest.php
@@ -23,11 +23,13 @@
use Bdf\Form\Filter\TrimFilter;
use Bdf\Form\Leaf\AnyElement;
use Bdf\Form\Leaf\AnyElementBuilder;
+use Bdf\Form\Leaf\BackedEnumElement;
use Bdf\Form\Leaf\BooleanElement;
use Bdf\Form\Leaf\BooleanElementBuilder;
use Bdf\Form\Leaf\Date\DateTimeChildBuilder;
use Bdf\Form\Leaf\Date\DateTimeElement;
use Bdf\Form\Leaf\Date\DateTimeElementBuilder;
+use Bdf\Form\Leaf\EnumElementBuilder;
use Bdf\Form\Leaf\FloatElement;
use Bdf\Form\Leaf\FloatElementBuilder;
use Bdf\Form\Leaf\Helper\EmailElement;
@@ -39,6 +41,7 @@
use Bdf\Form\Leaf\LeafElement;
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\Leaf\StringElementBuilder;
+use Bdf\Form\Leaf\UnitEnumElement;
use Bdf\Form\Phone\PhoneChildBuilder;
use Bdf\Form\Phone\PhoneElement;
use Bdf\Form\Phone\PhoneElementBuilder;
@@ -102,6 +105,8 @@ public function test_elementBuilder()
$this->assertInstanceOf(DateTimeElementBuilder::class, $this->registry->elementBuilder(DateTimeElement::class));
$this->assertInstanceOf(EmailElementBuilder::class, $this->registry->elementBuilder(EmailElement::class));
$this->assertInstanceOf(UrlElementBuilder::class, $this->registry->elementBuilder(UrlElement::class));
+ $this->assertInstanceOf(EnumElementBuilder::class, $this->registry->elementBuilder(UnitEnumElement::class));
+ $this->assertInstanceOf(EnumElementBuilder::class, $this->registry->elementBuilder(BackedEnumElement::class));
$this->assertInstanceOf(FormBuilder::class, $this->registry->elementBuilder(Form::class));
$this->assertInstanceOf(ArrayElementBuilder::class, $this->registry->elementBuilder(ArrayElement::class));
$this->assertInstanceOf(CustomFormBuilder::class, $this->registry->elementBuilder(MyCustomForm::class));