Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
56 changes: 54 additions & 2 deletions src/Aggregate/ArrayElementBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -277,6 +282,53 @@ public function phone(?callable $configurator = null): static
return $this->element(PhoneElement::class, $configurator);
}

/**
* Define as array of any values
*
* <code>
* $builder->array('values')->any();
* </code>
*
* @param callable(ElementBuilderInterface<ElementInterface<mixed>>):void|null $configurator Callback for configure the inner element builder
*
* @return static
* @psalm-this-out ArrayElementBuilder<mixed>
*
* @since 2.0
*/
public function any(?callable $configurator = null): static
{
return $this->element(AnyElement::class, $configurator);
}

/**
* Define as array of enum
*
* <code>
* $builder->array('types')->enum(Types::class, function(EnumElementBuilder $builder) {
* $builder->backed(false);
* })->getset();
* </code>
*
* @param class-string<UnitEnum> $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
*
Expand Down Expand Up @@ -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<BackedEnum>|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 {
Expand Down
18 changes: 18 additions & 0 deletions src/Aggregate/FormBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -39,6 +41,7 @@
use Override;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use UnitEnum;

/**
* Builder for a form
Expand Down Expand Up @@ -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<UnitEnum> $enumClass The enum class
* @return ChildBuilder<EnumElementBuilder>
*/
#[Override]
public function enum(string $name, string $enumClass): ChildBuilderInterface
{
/** @var ChildBuilder<EnumElementBuilder> $builder */
$builder = $this->add($name, UnitEnumElement::class);
return $builder->enumClass($enumClass);
}

/**
* {@inheritdoc}
*
Expand Down
21 changes: 20 additions & 1 deletion src/Aggregate/FormBuilderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -80,7 +82,7 @@ public function any(string $name): ChildBuilderInterface;
* Add a new string element on the form
*
* <code>
* $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);
* </code>
*
* @param non-empty-string $name The child name
Expand Down Expand Up @@ -170,6 +172,23 @@ public function dateTime(string $name): ChildBuilderInterface;
*/
public function phone(string $name): ChildBuilderInterface;

/**
* Add a new enum element on the form
*
* <code>
* $builder->enum('type', Types::class)->getset();
* </code>
*
* @param non-empty-string $name The child name
* @param class-string<UnitEnum> $enumClass The enum class
*
* @return ChildBuilder|EnumElementBuilder
* @psalm-return ChildBuilderInterface<EnumElementBuilder>
*
* @since 2.0
*/
public function enum(string $name, string $enumClass): ChildBuilderInterface;

/**
* Add a new csrf token on form
*
Expand Down
17 changes: 14 additions & 3 deletions src/Choice/ChoiceBuilderTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -38,22 +42,29 @@
* 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
* </code>
*
* @param ChoiceInterface|array|callable $choices The allowed values in PHP form.
* @param ChoiceInterface|array|class-string<BackedEnum>|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
*
* @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) {

Check warning on line 63 in src/Choice/ChoiceBuilderTrait.php

View workflow job for this annotation

GitHub Actions / Analysis

Escaped Mutant for Mutator "MatchArmRemoval": @@ @@ $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), }; }
is_array($choices) => new ArrayChoice($choices),
is_string($choices) && is_subclass_of($choices, BackedEnum::class) => new EnumChoice($choices),

Check warning on line 65 in src/Choice/ChoiceBuilderTrait.php

View workflow job for this annotation

GitHub Actions / Analysis

Escaped Mutant for Mutator "LogicalAnd": @@ @@ if (!$choices instanceof ChoiceInterface) { $choices = match (true) { is_array($choices) => new ArrayChoice($choices), - is_string($choices) && is_subclass_of($choices, BackedEnum::class) => new EnumChoice($choices), + is_string($choices) || is_subclass_of($choices, BackedEnum::class) => new EnumChoice($choices), is_callable($choices) => new LazyChoice($choices), }; }
is_callable($choices) => new LazyChoice($choices),
};
}

$callback = $choices->values(...);
Expand Down
2 changes: 1 addition & 1 deletion src/Choice/ChoiceView.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ChoiceView
public mixed $value;

/**
* The view representation of the choice.
* Does the current option is selected?
*/
public bool $selected;

Expand Down
64 changes: 64 additions & 0 deletions src/Choice/EnumChoice.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

namespace Bdf\Form\Choice;

use BackedEnum;
use Closure;
use Override;

use function assert;
use function is_subclass_of;

/**
* Adapt a backed enum to a choice
*
* @template E as BackedEnum
* @implements ChoiceInterface<int|string>
*/
final readonly class EnumChoice implements ChoiceInterface
{
public function __construct(
/**
* @var class-string<E>
*/
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;
}
}
Loading
Loading