diff --git a/README.md b/README.md index 404e7c4..5078a09 100644 --- a/README.md +++ b/README.md @@ -991,12 +991,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/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));