diff --git a/README.md b/README.md
index 77b47ff..d5e310b 100644
--- a/README.md
+++ b/README.md
@@ -135,6 +135,16 @@ $processor->generate(new MyOtherForm());
| [`CallbackGenerator`](src/Form/CallbackGenerator.php) | `CallbackGenerator('generate')` | `$builder->generates([$this, 'generate'])` | Define the method to use for generate the form value. The method must be declared as public on the form class. |
| [`Csrf`](src/Form/Csrf.php) | `Csrf(tokenId: 'MyToken')` | `$builder->csrf()->tokenId('MyToken')` | Add a CSRF element on the form. |
+### On method
+
+| Attribute | Example | Translated to | Purpose |
+|------------------------------------------------------------|--------------------------------------|-------------------------------------------------------|-------------------------------------------------------------|
+| [`AsConstraint`](src/Constraint/AsConstraint.php) | `AsConstraint('validateFoo')` | `$builder->satisfy([$this, 'validateFoo'])` | Use the method as constraint for the target element. |
+| [`AsArrayConstraint`](src/Aggregate/AsArrayConstraint.php) | `AsArrayConstraint('validateFoo')` | `$builder->arrayConstraint([$this, 'validateFoo'])` | Use the method as constraint for the target array element. |
+| [`AsFilter`](src/Child/AsFilter.php) | `AsFilter('filterFoo')` | `$builder->filter([$this, 'filterFoo'])` | Use the method as filter for the target element. |
+| [`AsTransformer`](src/Element/AsTransformer.php) | `AsTransformer('transformFoo')` | `$builder->transformer([$this, 'transformFoo'])` | Use the method as HTTP transformer for the target element. |
+| [`AsModelTransformer`](src/Child/AsModelTransformer.php) | `AsModelTransformer('transformFoo')` | `$builder->modelTransformer([$this, 'transformFoo'])` | Use the method as model transformer for the target element. |
+
### On button property
| Attribute | Example | Translated to | Purpose |
@@ -153,6 +163,8 @@ $processor->generate(new MyOtherForm());
| [`DefaultValue`](src/Child/DefaultValue.php) | `DefaultValue(42)` | `...->configureField($elementBuilder)` | Define the default value of the input. |
| [`Dependencies`](src/Child/Dependencies.php) | `Dependencies('foo', 'bar')` | `...->depends('foo', 'bar')` | Declare dependencies on the current input. Dependencies will be submitted before the current field. |
| [`GetSet`](src/Child/GetSet.php) | `GetSet('realField')` | `...->getter('realField')->setter('realField')` | Enable hydration and extraction of the entity. |
+| [`CallbackFilter`](src/Child/CallbackFilter.php) | `CallbackFilter('filterMethod')` | `...->filter([$this, 'filterMethod'])` | Add a filter on the current child using a method. |
+| [`HttpField`](src/Child/HttpField.php) | `HttpField('_field')` | `...->httpField(new ArrayOffsetHttpField('_field'))` | Define the http field name to use on the current child, instead of use the property name. |
| **Element** | | | |
| [`CallbackConstraint`](src/Constraint/CallbackConstraint.php) | `CallbackConstraint('validateInput')` | `...->satisfy([$this, 'validateInput'])` | Validate an input using a method. |
| [`Satisfy`](src/Constraint/Satisfy.php) | `Satisfy(MyConstraint::class, ['opt' => 'val'])` | `...->satisfy(new MyConstraint(['opt' => 'val']))` | Add a constraint on the input. Prefer directly use the constraint class as attribute if possible. |
@@ -162,13 +174,17 @@ $processor->generate(new MyOtherForm());
| [`Raw`](src/Element/Raw.php) | `Raw` | `...->raw()` | For number elements. Use native PHP cast instead of locale parsing for convert number. |
| [`TransformerError`](src/Element/TransformerError.php) | `TransformerError(message: 'Invalid value provided')` | `...->transformerErrorMessage('Invalid value provided')` | Configure error handling of transformer exceptions. |
| [`IgnoreTransformerException`](src/Element/IgnoreTransformerException.php) | `IgnoreTransformerException` | `...->ignoreTransformerException()` | Ignore transformer exception. If enable and an exception occurs, the raw value will be used. |
+| [`Required`](src/Element/Required.php) | `Required` | `...->required()` | Mark the element as required. The error message can be defined as parameter of the attribute. |
| **DateTimeElement** | | | |
| [`DateFormat`](src/Element/Date/DateFormat.php) | `DateFormat('d/m/Y H:i')` | `...->format('d/m/Y H:i')` | Define the input date format. |
| [`DateTimeClass`](src/Element/Date/DateTimeClass.php) | `DateTimeClass(Carbon::class)` | `...->className(Carbon::class)` | Define date time class to use on for parse the date. |
| [`ImmutableDateTime`](src/Element/Date/ImmutableDateTime.php) | `ImmutableDateTime` | `...->immutable()` | Use `DateTimeImmutable` as date time class. |
| [`Timezone`](src/Element/Date/Timezone.php) | `Timezone('Europe/Paris')` | `...->timezone('Europe/Paris')` | Define the parsing and normalized timezone to use. |
+| [`AfterField`](src/Element/Date/AfterField.php) | `AfterField('otherField')` | `...->afterField('otherField')` | Add a greater than an other field constraint to the current element. |
+| [`BeforeField`](src/Element/Date/BeforeField.php) | `BeforeField('otherField')` | `...->beforeField('otherField')` | Add a less than an other field constraint to the current element. |
| **ArrayElement** | | | |
| [`ArrayConstraint`](src/Aggregate/ArrayConstraint.php) | `ArrayConstraint(MyConstraint::class, ['opt' => 'val'])` | `...->arrayConstraint(new MyConstraint(['opt' => 'val']))` | Add a constraint on the whole array element. |
| [`CallbackArrayConstraint`](src/Aggregate/CallbackArrayConstraint.php) | `CallbackArrayConstraint('validateInput')` | `...->arrayConstraint([$this, 'validateInput'])` | Add a constraint on the whole array element, using a form method. |
| [`Count`](src/Aggregate/Count.php) | `Count(min: 3, max: 6)` | `...->arrayConstraint(new Count(min: 3, max: 6))` | Add a Count constraint on the array element. |
| [`ElementType`](src/Aggregate/ElementType.php) | `ElementType(IntegerElement::class, 'configureElement')` | `...->element(IntegerElement::class, [$this, 'configureElement'])` | Define the array element type. A configuration callback method can be define for configure the inner element. |
+| [`ArrayTransformer`](src/Aggregate/ArrayTransformer.php) | `ArrayTransformer(MyTransformer::class, ['ctroarg'])` | `...->arrayTransformer(new MyTransformer('ctorarg))` | Add a transformer for the whole array input. |
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index a9e870d..3b8f2ad 100755
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -21,6 +21,10 @@
./tests
+ ./tests/Php81
+
+
+ ./tests/Php81
diff --git a/src/Aggregate/ArrayConstraint.php b/src/Aggregate/ArrayConstraint.php
index d73332f..f6117fd 100644
--- a/src/Aggregate/ArrayConstraint.php
+++ b/src/Aggregate/ArrayConstraint.php
@@ -8,11 +8,15 @@
use Bdf\Form\Attribute\ChildBuilderAttributeInterface;
use Bdf\Form\Attribute\Constraint\Satisfy;
use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator;
-use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy;
+use Bdf\Form\Attribute\Processor\CodeGenerator\ObjectInstantiation;
use Bdf\Form\Child\ChildBuilderInterface;
+use InvalidArgumentException;
use Nette\PhpGenerator\Literal;
use Symfony\Component\Validator\Constraint;
+use function is_object;
+use function is_string;
+
/**
* Add a constraint on the whole array element
* Use Satisfy, or directly the constraint as attribute for add a constraint on one array item
@@ -28,6 +32,10 @@
* {
* #[ArrayConstraint(Unique::class, ['message' => 'My error'])]
* private ArrayElement $values;
+ *
+ * // or on PHP 8.1
+ * #[ArrayConstraint(new Unique(['message' => 'My error']))]
+ * private ArrayElement $values;
* }
*
*
@@ -43,12 +51,19 @@ final class ArrayConstraint implements ChildBuilderAttributeInterface
{
public function __construct(
/**
- * The constraint class name
+ * The constraint
*
- * @var class-string
+ * You can use a class name, and provider arguments on the next parameter,
+ * or directly use the constraint instance.
+ *
+ * When a constraint instance is used, in case of code generation,
+ * the constructor parameters will be deduced from public properties of the constraint.
+ * This may not work if the constraint has a complex constructor.
+ *
+ * @var class-string|Constraint
* @readonly
*/
- private string $constraint,
+ private string|Constraint $constraint,
/**
* Constraint's constructor options
*
@@ -57,6 +72,9 @@ public function __construct(
*/
private mixed $options = null
) {
+ if (is_object($constraint) && $options !== null) {
+ throw new InvalidArgumentException('Cannot use options with constraint instance');
+ }
}
/**
@@ -72,8 +90,12 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $
*/
public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
{
- $constraint = $generator->useAndSimplifyType($this->constraint);
-
- $generator->line('$?->arrayConstraint(?::class, ?);', [$name, new Literal($constraint), $this->options]);
+ if (is_string($this->constraint)) {
+ $constraint = $generator->useAndSimplifyType($this->constraint);
+ $generator->line('$?->arrayConstraint(?::class, ?);', [$name, new Literal($constraint), $this->options]);
+ } else {
+ $constraint = ObjectInstantiation::singleArrayParameter($this->constraint)->render($generator);
+ $generator->line('$?->arrayConstraint(?);', [$name, $constraint]);
+ }
}
}
diff --git a/src/Aggregate/ArrayTransformer.php b/src/Aggregate/ArrayTransformer.php
new file mode 100644
index 0000000..7dfef5f
--- /dev/null
+++ b/src/Aggregate/ArrayTransformer.php
@@ -0,0 +1,45 @@
+
+ * $builder->string('foo')->arrayTransformer(new MyTransformer(...$arguments));
+ *
+ *
+ * Usage:
+ *
+ * class MyForm extends AttributeForm
+ * {
+ * #[ArrayTransformer(MyTransformer::class, ['foo', 'bar']), ElementType(IntegerElement::class)]
+ * private ArrayElement $foo;
+ * }
+ *
+ *
+ * @see ArrayElementBuilder::arrayTransformer() The called method
+ * @see CallbackTransformer For use custom methods as transformer instead of class
+ * @see Transformer To add a transformer the item of the array
+ *
+ * @api
+ */
+#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
+class ArrayTransformer extends Transformer
+{
+ /**
+ * @param class-string $transformerClass The transformer class name
+ * @param array $constructorArguments Arguments to provide on the transformer constructor
+ */
+ public function __construct(string $transformerClass, array $constructorArguments = [])
+ {
+ parent::__construct($transformerClass, $constructorArguments, true);
+ }
+}
diff --git a/src/Aggregate/AsArrayConstraint.php b/src/Aggregate/AsArrayConstraint.php
new file mode 100644
index 0000000..fc9e90c
--- /dev/null
+++ b/src/Aggregate/AsArrayConstraint.php
@@ -0,0 +1,91 @@
+
+ * $builder->array('foo')->arrayConstraint([$this, 'validateFoo'], 'Foo is invalid');
+ *
+ *
+ * Usage:
+ *
+ * class MyForm extends AttributeForm
+ * {
+ * private ArrayElement $foo;
+ *
+ * #[AsArrayConstraint('foo', message: 'Foo is invalid')]
+ * public function validateFoo(array $value, ElementInterface $input): bool
+ * {
+ * return count($value) % 5 > 2;
+ * }
+ * }
+ *
+ *
+ * @see ArrayElementBuilder::arrayConstraint() The called method
+ * @see Constraint
+ * @see Closure The used constraint
+ * @see ArrayConstraint Use for a class constraint
+ * @see CallbackArrayConstraint To annotate the property instead of the method
+ *
+ * @implements MethodChildBuilderAttributeInterface
+ *
+ * @api
+ */
+#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class AsArrayConstraint implements MethodChildBuilderAttributeInterface
+{
+ public function __construct(
+ /**
+ * The element property name to which the constraint is applied
+ *
+ * @var non-empty-string
+ * @readonly
+ */
+ private string $target,
+ /**
+ * The error message to use
+ * This option is used only if the validator return false, in other cases,
+ * the message returned by the validator will be used
+ *
+ * @var string|null
+ * @readonly
+ */
+ private ?string $message = null,
+ ) {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function targetElements(): array
+ {
+ return [$this->target];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface
+ {
+ return new CallbackArrayConstraint($method->getName(), $this->message);
+ }
+}
diff --git a/src/Aggregate/CallbackArrayConstraint.php b/src/Aggregate/CallbackArrayConstraint.php
index 88986cb..bb83539 100644
--- a/src/Aggregate/CallbackArrayConstraint.php
+++ b/src/Aggregate/CallbackArrayConstraint.php
@@ -7,10 +7,8 @@
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\ChildBuilderAttributeInterface;
use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator;
-use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy;
use Bdf\Form\Child\ChildBuilderInterface;
use Bdf\Form\Constraint\Closure;
-use Bdf\Form\ElementBuilderInterface;
use Nette\PhpGenerator\Literal;
use Symfony\Component\Validator\Constraint;
@@ -61,7 +59,7 @@ public function __construct(
* - Return string for a custom error message
* - Return array with error message and code
*
- * @var literal-string
+ * @var non-empty-string
* @readonly
*/
private string $methodName,
diff --git a/src/Child/AsFilter.php b/src/Child/AsFilter.php
new file mode 100644
index 0000000..cdc704b
--- /dev/null
+++ b/src/Child/AsFilter.php
@@ -0,0 +1,74 @@
+
+ * $builder->string('foo')->filter([$this, 'myTransformer']);
+ *
+ *
+ * Usage:
+ *
+ * class MyForm extends AttributeForm
+ * {
+ * private IntegerElement $foo;
+ *
+ * #[AsFilter('filterFoo')]
+ * public function filterFoo($value, ChildInterface $child, $default): string
+ * {
+ * return hexdec($value);
+ * }
+ * }
+ *
+ *
+ * @implements MethodChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface>
+ *
+ * @see ChildBuilderInterface::filter() The called method
+ * @see ClosureFilter The used filter class
+ * @see CallbackTransformer For use transformer in same way, but for http transformer intead of filter one
+ * @see CallbackFilter To annotate the property instead
+ *
+ * @api
+ */
+#[Attribute(Attribute::TARGET_METHOD)]
+final class AsFilter implements MethodChildBuilderAttributeInterface
+{
+ /**
+ * @var list
+ * @readonly
+ */
+ private array $targets;
+
+ /**
+ * @param non-empty-string ...$targets List of elements names that the attribute can be applied on. Must be a property name.
+ * @no-named-arguments
+ */
+ public function __construct(string ...$targets)
+ {
+ $this->targets = $targets;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function targetElements(): array
+ {
+ return $this->targets;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface
+ {
+ return new CallbackFilter($method->getName());
+ }
+}
diff --git a/src/Child/AsModelTransformer.php b/src/Child/AsModelTransformer.php
new file mode 100644
index 0000000..6a85f06
--- /dev/null
+++ b/src/Child/AsModelTransformer.php
@@ -0,0 +1,82 @@
+
+ * $builder->string('foo')->modelTransformer([$this, 'myTransformer']);
+ *
+ *
+ * Usage:
+ *
+ * class MyForm extends AttributeForm
+ * {
+ * private IntegerElement $bar;
+ *
+ * #[AsModelTransformer('bar')]
+ * public function barTransformer($value, IntegerElement $input, bool $toEntity)
+ * {
+ * return $toEntity ? dechex($value) : hexdec($value);
+ * }
+ * }
+ *
+ *
+ * @implements MethodChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface>
+ *
+ * @see ChildBuilderInterface::modelTransformer() The called method
+ * @see ModelTransformer For use a transformer class as model transformer
+ * @see CallbackTransformer For use transformer in same way, but for http transformer intead of model one
+ * @see CallbackModelTransformer To annotate the property instead
+ *
+ * @api
+ */
+#[Attribute(Attribute::TARGET_METHOD)]
+final class AsModelTransformer implements MethodChildBuilderAttributeInterface
+{
+ /**
+ * @var list
+ * @readonly
+ */
+ private array $targets;
+
+ /**
+ * @param non-empty-string ...$targets Target elements properties names
+ * @no-named-arguments
+ */
+ public function __construct(string ...$targets)
+ {
+ $this->targets = $targets;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function targetElements(): array
+ {
+ return $this->targets;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface
+ {
+ return new CallbackModelTransformer($method->getName());
+ }
+}
diff --git a/src/Child/CallbackFilter.php b/src/Child/CallbackFilter.php
new file mode 100644
index 0000000..3c0aace
--- /dev/null
+++ b/src/Child/CallbackFilter.php
@@ -0,0 +1,74 @@
+
+ * $builder->string('foo')->filter([$this, 'myTransformer']);
+ *
+ *
+ * Usage:
+ *
+ * class MyForm extends AttributeForm
+ * {
+ * #[CallbackFilter('filterFoo')]
+ * private IntegerElement $foo;
+ *
+ * public function filterFoo($value, ChildInterface $child, $default): string
+ * {
+ * return hexdec($value);
+ * }
+ * }
+ *
+ *
+ * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface>
+ *
+ * @see ChildBuilderInterface::filter() The called method
+ * @see ClosureFilter The used filter class
+ * @see CallbackTransformer For use transformer in same way, but for http transformer intead of filter one
+ * @see AsFilter To annotate a method as filter
+ *
+ * @api
+ */
+#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
+final class CallbackFilter implements ChildBuilderAttributeInterface
+{
+ public function __construct(
+ /**
+ * The method name to use as filter
+ *
+ * The method must be public and follow the signature `function (mixed $value, ElementInterface $input, mixed|null $default): mixed`
+ *
+ * @var non-empty-string
+ * @readonly
+ */
+ private string $method,
+ ) {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ {
+ $builder->filter([$form, $this->method]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ {
+ $generator->line('$?->filter([$form, ?]);', [$name, $this->method]);
+ }
+}
diff --git a/src/Child/CallbackModelTransformer.php b/src/Child/CallbackModelTransformer.php
index 65f8e21..701d080 100644
--- a/src/Child/CallbackModelTransformer.php
+++ b/src/Child/CallbackModelTransformer.php
@@ -70,6 +70,7 @@
* @see ChildBuilderInterface::modelTransformer() The called method
* @see ModelTransformer For use a transformer class as model transformer
* @see CallbackTransformer For use transformer in same way, but for http transformer intead of model one
+ * @see AsModelTransformer To annotate the method instead
*
* @api
*/
@@ -81,21 +82,21 @@ public function __construct(
* Method name use to define the unified transformer method
* If defined, the other parameters will be ignored
*
- * @var literal-string|null
+ * @var non-empty-string|null
* @readonly
*/
private ?string $callback = null,
/**
* Method name use to define the transformation process from input value to the entity
*
- * @var literal-string|null
+ * @var non-empty-string|null
* @readonly
*/
private ?string $toEntity = null,
/**
* Method name use to define the transformation process from entity value to input
*
- * @var literal-string|null
+ * @var non-empty-string|null
* @readonly
*/
private ?string $toInput = null,
diff --git a/src/Child/Configure.php b/src/Child/Configure.php
index caab40a..c5f6a82 100644
--- a/src/Child/Configure.php
+++ b/src/Child/Configure.php
@@ -6,14 +6,18 @@
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\ChildBuilderAttributeInterface;
use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator;
-use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy;
+use Bdf\Form\Attribute\Processor\MethodChildBuilderAttributeInterface;
use Bdf\Form\Child\ChildBuilderInterface;
+use ReflectionMethod;
/**
* Define a custom configuration method for an element
*
* Use this attribute when other attributes cannot be used to configure the current element
- * Takes as argument the method name to use. This method must be declared into the form class with public visibility
+ * Takes as argument the method name to use. This method must be declared into the form class with public visibility.
+ *
+ * It can also be used to directly annotate the configuration method.
+ * In this case, it will take as argument the property name to configure.
*
* Usage:
*
@@ -26,25 +30,37 @@
* {
* $builder->min(5);
* }
+ *
+ * // Or directly on the method (same as above)
+ * #[Configure('foo')]
+ * public function otherConfiguration(ChildBuilderInterface $builder): void
+ * {
+ * $builder->min(5);
+ * }
* }
*
*
* @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface>
+ * @implements MethodChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface>
*
* @api
*/
-#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
-class Configure implements ChildBuilderAttributeInterface
+#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+class Configure implements ChildBuilderAttributeInterface, MethodChildBuilderAttributeInterface
{
public function __construct(
/**
- * The method name to use as configurator
+ * Thet target of the configuration.
+ *
+ * In case of a property attribute, define the method name to use as configurator.
* The method should follow the prototype `public function (ChildBuilderInterface $builder): void`
*
- * @var literal-string
+ * In case of a method attribute, define the property name to configure.
+ *
+ * @var non-empty-string
* @readonly
*/
- private string $callback
+ private string $target
) {
}
@@ -53,7 +69,7 @@ public function __construct(
*/
public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
{
- $form->{$this->callback}($builder);
+ $form->{$this->target}($builder);
}
/**
@@ -61,6 +77,22 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $
*/
public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
{
- $generator->line('$form->?($?);', [$this->callback, $name]);
+ $generator->line('$form->?($?);', [$this->target, $name]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function targetElements(): array
+ {
+ return [$this->target];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface
+ {
+ return new self($method->getName());
}
}
diff --git a/src/Child/HttpField.php b/src/Child/HttpField.php
new file mode 100644
index 0000000..0d62df6
--- /dev/null
+++ b/src/Child/HttpField.php
@@ -0,0 +1,78 @@
+
+ * $builder->float('foo')->httpFields(new ArrayOffsetHttpFields('_foo'));
+ *
+ *
+ * Usage:
+ *
+ * class MyForm extends AttributeForm
+ * {
+ * #[HttpField('package_length')]
+ * private FloatElement $packageLength;
+ * }
+ *
+ *
+ * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface>
+ *
+ * @see ChildBuilder::httpFields() The called method
+ * @see ArrayOffsetHttpFields The used HTTP fields implementation
+ *
+ * @api
+ */
+#[Attribute(Attribute::TARGET_PROPERTY)]
+final class HttpField implements ChildBuilderAttributeInterface
+{
+ public function __construct(
+ /**
+ * The HTTP field name
+ *
+ * @var non-empty-string
+ * @readonly
+ */
+ private string $name,
+ ) {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ {
+ if (!$builder instanceof ChildBuilder) {
+ throw new LogicException('The HttpField attribute can only be used on a ChildBuilder instance');
+ }
+
+ $builder->httpFields(new ArrayOffsetHttpFields($this->name));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ {
+ $generator->use(ArrayOffsetHttpFields::class);
+ $generator->line('$?->httpFields(new ArrayOffsetHttpFields(?));', [$name, $this->name]);
+ }
+}
diff --git a/src/Constraint/AsConstraint.php b/src/Constraint/AsConstraint.php
new file mode 100644
index 0000000..ef05d98
--- /dev/null
+++ b/src/Constraint/AsConstraint.php
@@ -0,0 +1,91 @@
+
+ * $builder->integer('foo')->satisfy([$this, 'validateFoo'], 'Foo is invalid');
+ *
+ *
+ * Usage:
+ *
+ * class MyForm extends AttributeForm
+ * {
+ * #[AsConstraint('foo', message: 'Foo is invalid')]
+ * public function validateFoo($value, ElementInterface $input): bool
+ * {
+ * return $value % 5 > 2;
+ * }
+ * }
+ *
+ *
+ * @implements MethodChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface>
+ *
+ * @see ElementBuilderInterface::satisfy() The called method
+ * @see Constraint
+ * @see Closure The used constraint
+ * @see CallbackConstraint For annotate the property instead of the method
+ * @see AsArrayConstraint For use a constraint on an array element
+ *
+ * @api
+ */
+#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
+final class AsConstraint implements MethodChildBuilderAttributeInterface
+{
+ public function __construct(
+ /**
+ * The element property name to which the constraint is applied
+ *
+ * @var literal-string
+ * @readonly
+ */
+ private string $target,
+ /**
+ * The error message to use
+ * This option is used only if the validator return false, in other cases,
+ * the message returned by the validator will be used
+ *
+ * @var string|null
+ * @readonly
+ */
+ private ?string $message = null,
+ ) {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function targetElements(): array
+ {
+ return [$this->target];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface
+ {
+ return new CallbackConstraint($method->getName(), $this->message);
+ }
+}
diff --git a/src/Constraint/CallbackConstraint.php b/src/Constraint/CallbackConstraint.php
index e28ee96..249bea6 100644
--- a/src/Constraint/CallbackConstraint.php
+++ b/src/Constraint/CallbackConstraint.php
@@ -42,6 +42,7 @@
* @see ElementBuilderInterface::satisfy() The called method
* @see Constraint
* @see Closure The used constraint
+ * @see AsConstraint To annotate the method instead of the property
*
* @api
*/
@@ -61,7 +62,7 @@ public function __construct(
* - Return string for a custom error message
* - Return array with error message and code
*
- * @var literal-string
+ * @var non-empty-string
* @readonly
*/
private string $methodName,
diff --git a/src/Constraint/Satisfy.php b/src/Constraint/Satisfy.php
index fa046d4..83c7ab6 100644
--- a/src/Constraint/Satisfy.php
+++ b/src/Constraint/Satisfy.php
@@ -6,11 +6,15 @@
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\ChildBuilderAttributeInterface;
use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator;
-use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy;
+use Bdf\Form\Attribute\Processor\CodeGenerator\ObjectInstantiation;
use Bdf\Form\Child\ChildBuilderInterface;
+use InvalidArgumentException;
use Nette\PhpGenerator\Literal;
use Symfony\Component\Validator\Constraint;
+use function is_object;
+use function is_string;
+
/**
* Define a custom constraint for an element, using a validation method
*
@@ -27,6 +31,10 @@
* {
* #[Satisfy(MyConstraint::class, ['foo' => 'bar'])]
* private IntegerElement $foo;
+ *
+ * // or on PHP 8.1
+ * #[Satisfy(new MyConstraint(['foo' => 'bar']))]
+ * private IntegerElement $foo;
* }
*
*
@@ -42,12 +50,18 @@ class Satisfy implements ChildBuilderAttributeInterface
{
public function __construct(
/**
- * The constraint class name
+ * The constraint
+ *
+ * You can use a class name, and provider arguments on the next parameter,
+ * or directly use the constraint instance.
*
- * @var class-string
+ * When a constraint instance is used, in case of code generation,
+ * the constructor parameters will be deduced from public properties of the constraint.
+ * This may not work if the constraint has a complex constructor.
+ * @var class-string|Constraint
* @readonly
*/
- private string $constraint,
+ private string|Constraint $constraint,
/**
* Constraint's constructor options
*
@@ -56,6 +70,9 @@ public function __construct(
*/
private mixed $options = null
) {
+ if (is_object($constraint) && $options !== null) {
+ throw new InvalidArgumentException('Cannot use options with constraint instance');
+ }
}
/**
@@ -71,7 +88,12 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $
*/
public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
{
- $type = $generator->useAndSimplifyType($this->constraint);
- $generator->line('$?->satisfy(?::class, ?);', [$name, new Literal($type), $this->options]);
+ if (is_string($this->constraint)) {
+ $type = $generator->useAndSimplifyType($this->constraint);
+ $generator->line('$?->satisfy(?::class, ?);', [$name, new Literal($type), $this->options]);
+ } else {
+ $constraint = ObjectInstantiation::singleArrayParameter($this->constraint)->render($generator);
+ $generator->line('$?->satisfy(?);', [$name, $constraint]);
+ }
}
}
diff --git a/src/Element/AsTransformer.php b/src/Element/AsTransformer.php
new file mode 100644
index 0000000..aea8c06
--- /dev/null
+++ b/src/Element/AsTransformer.php
@@ -0,0 +1,82 @@
+
+ * $builder->string('foo')->transformer([$this, 'myTransformer']);
+ *
+ *
+ * Usage:
+ *
+ * class MyForm extends AttributeForm
+ * {
+ * private IntegerElement $bar;
+ *
+ * #[AsTransformer('bar')]
+ * public function barTransformer($value, IntegerElement $input, bool $toPhp)
+ * {
+ * return $toPhp ? hexdec($value) : dechex($value);
+ * }
+ * }
+ *
+ *
+ * @implements MethodChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface>
+ *
+ * @see ElementBuilderInterface::transformer() The called method
+ * @see Transformer For use a transformer class as transformer
+ * @see AsModelTransformer For use transformer in same way, but for model transformer intead of http one
+ * @see CallbackTransformer To annotate the property instead
+ *
+ * @api
+ */
+#[Attribute(Attribute::TARGET_METHOD)]
+final class AsTransformer implements MethodChildBuilderAttributeInterface
+{
+ /**
+ * @var list
+ * @readonly
+ */
+ private array $targets;
+
+ /**
+ * @param non-empty-string ...$targets Target elements properties names
+ * @no-named-arguments
+ */
+ public function __construct(string ...$targets)
+ {
+ $this->targets = $targets;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function targetElements(): array
+ {
+ return $this->targets;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface
+ {
+ return new CallbackTransformer($method->getName());
+ }
+}
diff --git a/src/Element/CallbackTransformer.php b/src/Element/CallbackTransformer.php
index 330dfe2..310b166 100644
--- a/src/Element/CallbackTransformer.php
+++ b/src/Element/CallbackTransformer.php
@@ -84,21 +84,21 @@ public function __construct(
* Method name use to define the unified transformer method
* If defined, the other parameters will be ignored
*
- * @var literal-string|null
+ * @var non-empty-string|null
* @readonly
*/
private ?string $callback = null,
/**
* Method name use to define the transformation process from http value to the input
*
- * @var literal-string|null
+ * @var non-empty-string|null
* @readonly
*/
private ?string $fromHttp = null,
/**
* Method name use to define the transformation process from input value to http format
*
- * @var literal-string|null
+ * @var non-empty-string|null
* @readonly
*/
private ?string $toHttp = null,
diff --git a/src/Element/Date/AfterField.php b/src/Element/Date/AfterField.php
new file mode 100644
index 0000000..b231d6a
--- /dev/null
+++ b/src/Element/Date/AfterField.php
@@ -0,0 +1,81 @@
+
+ * $builder->dateTime('dateEnd')->afterField('dateStart');
+ *
+ *
+ * Usage:
+ *
+ * class MyForm extends AttributeForm
+ * {
+ * private DateTimeElement $dateStart;
+ *
+ * #[Dependencies('dateStart'), AfterField('dateStart')]
+ * private DateTimeElement $dateEnd;
+ * }
+ *
+ *
+ * @see \Bdf\Form\Leaf\Date\DateTimeElementBuilder::afterField() The called method
+ * @see BeforeField For the opposite constraint
+ *
+ * @implements ChildBuilderAttributeInterface<\Bdf\Form\Leaf\Date\DateTimeElementBuilder>
+ *
+ * @api
+ */
+#[Attribute(Attribute::TARGET_PROPERTY)]
+final class AfterField implements ChildBuilderAttributeInterface
+{
+ public function __construct(
+ /**
+ * The field name to compare
+ *
+ * @var non-empty-string
+ * @readonly
+ */
+ private string $field,
+ /**
+ * The error message.
+ * If not set, the default message will be used
+ *
+ * @var string|null
+ */
+ private ?string $message = null,
+ /**
+ * If true, will allow the date to be equal to the other field
+ *
+ * @var bool
+ */
+ private bool $orEqual = false,
+ ) {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ {
+ $builder->afterField($this->field, $this->message, $this->orEqual);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ {
+ $generator->line('$?->afterField(?, ?, ?);', [$name, $this->field, $this->message, $this->orEqual]);
+ }
+}
diff --git a/src/Element/Date/BeforeField.php b/src/Element/Date/BeforeField.php
new file mode 100644
index 0000000..504e059
--- /dev/null
+++ b/src/Element/Date/BeforeField.php
@@ -0,0 +1,81 @@
+
+ * $builder->dateTime('dateEnd')->beforeField('dateStart');
+ *
+ *
+ * Usage:
+ *
+ * class MyForm extends AttributeForm
+ * {
+ * private DateTimeElement $dateStart;
+ *
+ * #[Dependencies('dateEnd'), BeforeField('dateEnd')]
+ * private DateTimeElement $dateStart;
+ * }
+ *
+ *
+ * @see \Bdf\Form\Leaf\Date\DateTimeElementBuilder::beforeField() The called method
+ * @see AfterField For the opposite constraint
+ *
+ * @implements ChildBuilderAttributeInterface<\Bdf\Form\Leaf\Date\DateTimeElementBuilder>
+ *
+ * @api
+ */
+#[Attribute(Attribute::TARGET_PROPERTY)]
+final class BeforeField implements ChildBuilderAttributeInterface
+{
+ public function __construct(
+ /**
+ * The field name to compare
+ *
+ * @var non-empty-string
+ * @readonly
+ */
+ private string $field,
+ /**
+ * The error message.
+ * If not set, the default message will be used
+ *
+ * @var string|null
+ */
+ private ?string $message = null,
+ /**
+ * If true, will allow the date to be equal to the other field
+ *
+ * @var bool
+ */
+ private bool $orEqual = false,
+ ) {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ {
+ $builder->beforeField($this->field, $this->message, $this->orEqual);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ {
+ $generator->line('$?->beforeField(?, ?, ?);', [$name, $this->field, $this->message, $this->orEqual]);
+ }
+}
diff --git a/src/Element/Required.php b/src/Element/Required.php
new file mode 100644
index 0000000..4f2629a
--- /dev/null
+++ b/src/Element/Required.php
@@ -0,0 +1,66 @@
+
+ * $builder->float('foo')->required();
+ *
+ *
+ * Usage:
+ *
+ * class MyForm extends AttributeForm
+ * {
+ * #[Required]
+ * private IntegerElement $foo;
+ * }
+ *
+ *
+ * @see AbstractElementBuilder::required() The called method
+ *
+ * @implements ChildBuilderAttributeInterface
+ *
+ * @api
+ */
+#[Attribute(Attribute::TARGET_PROPERTY)]
+final class Required implements ChildBuilderAttributeInterface
+{
+ public function __construct(
+ /**
+ * Define the message to display if the element is not filled
+ * If not set, the default message will be used
+ *
+ * @readonly
+ */
+ private ?string $message = null,
+ ) {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ {
+ $builder->required($this->message);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ {
+ $generator->line('$?->required(?);', [$name, $this->message]);
+ }
+}
diff --git a/src/Element/Transformer.php b/src/Element/Transformer.php
index 85a7aed..bf90a96 100644
--- a/src/Element/Transformer.php
+++ b/src/Element/Transformer.php
@@ -3,6 +3,8 @@
namespace Bdf\Form\Attribute\Element;
use Attribute;
+use Bdf\Form\Aggregate\ArrayElementBuilder;
+use Bdf\Form\Attribute\Aggregate\ArrayTransformer;
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\ChildBuilderAttributeInterface;
use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator;
@@ -34,6 +36,7 @@
* @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface>
*
* @see ElementBuilderInterface::transformer() The called method
+ * @see ArrayElementBuilder::arrayTransformer() The called method if array flag is set
* @see CallbackTransformer For use custom methods as transformer instead of class
*
* @api
@@ -56,6 +59,20 @@ public function __construct(
* @readonly
*/
private array $constructorArguments = [],
+ /**
+ * Apply the transformer on the whole array element
+ * instead of each element
+ *
+ * If set to true, {@see ArrayElementBuilder::arrayTransformer()} will be used
+ *
+ * Note: this flag can be used only on array element
+ *
+ * @var bool
+ * @readonly
+ *
+ * @see ArrayTransformer Prefer use this attribute for array element, instead of manually set this flag
+ */
+ private bool $array = false,
) {
}
@@ -64,7 +81,14 @@ public function __construct(
*/
public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
{
- $builder->transformer(new $this->transformerClass(...$this->constructorArguments));
+ $transformer = new $this->transformerClass(...$this->constructorArguments);
+
+ if ($this->array) {
+ /** @var ChildBuilderInterface $builder */
+ $builder->arrayTransformer($transformer);
+ } else {
+ $builder->transformer($transformer);
+ }
}
/**
@@ -73,6 +97,8 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $
public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
{
$transformer = $generator->useAndSimplifyType($this->transformerClass);
- $generator->line('$?->transformer(new ?(...?));', [$name, new Literal($transformer), $this->constructorArguments]);
+ $code = $this->array ? '$?->arrayTransformer(new ?(...?));' : '$?->transformer(new ?(...?));';
+
+ $generator->line($code, [$name, new Literal($transformer), $this->constructorArguments]);
}
}
diff --git a/src/Processor/CodeGenerator/ObjectInstantiation.php b/src/Processor/CodeGenerator/ObjectInstantiation.php
new file mode 100644
index 0000000..9d1630c
--- /dev/null
+++ b/src/Processor/CodeGenerator/ObjectInstantiation.php
@@ -0,0 +1,92 @@
+useAndSimplifyType($this->className) : $this->className;
+
+ return Literal::new($className, $this->constructorParameters);
+ }
+
+ /**
+ * Configure the ObjectInstantiation utility to generate
+ * the constructor call with a single array parameter,
+ * use to inject all public properties of an object.
+ *
+ * This method should be used for symfony constraints.
+ * Properties with default value will be ignored.
+ *
+ * @param object $object
+ * @return self
+ */
+ public static function singleArrayParameter(object $object): self
+ {
+ return new self(
+ get_class($object),
+ [self::extractPublicProperties($object)]
+ );
+ }
+
+ /**
+ * Extract all public properties of the object, ignoring default values
+ *
+ * @param object $object
+ *
+ * @return array
+ * @psalm-suppress MixedAssignment
+ */
+ private static function extractPublicProperties(object $object): array
+ {
+ $reflectionObject = new ReflectionObject($object);
+ $properties = [];
+
+ foreach ($reflectionObject->getProperties() as $property) {
+ if ($property->isPublic() && !$property->isStatic()) {
+ $property->setAccessible(true);
+ $value = $property->getValue($object);
+
+ if ($property->getDefaultValue() !== $value) {
+ $properties[$property->getName()] = $value;
+ }
+ }
+ }
+
+ return $properties;
+ }
+}
diff --git a/src/Processor/ConfigureFormBuilderStrategy.php b/src/Processor/ConfigureFormBuilderStrategy.php
index cd081fa..62aece7 100644
--- a/src/Processor/ConfigureFormBuilderStrategy.php
+++ b/src/Processor/ConfigureFormBuilderStrategy.php
@@ -39,7 +39,7 @@ public function __construct()
/**
* {@inheritdoc}
*/
- public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder): void
+ public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void
{
foreach ($formClass->getAttributes(FormBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
$attribute->newInstance()->applyOnFormBuilder($form, $builder);
@@ -49,7 +49,7 @@ public function onFormClass(ReflectionClass $formClass, AttributeForm $form, For
/**
* {@inheritdoc}
*/
- public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder): void
+ public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void
{
$submitBuilder = $builder->submit($name);
@@ -61,7 +61,7 @@ public function onButtonProperty(ReflectionProperty $property, string $name, Att
/**
* {@inheritdoc}
*/
- public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder): void
+ public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void
{
$elementBuilder = $builder->add($name, $elementType);
@@ -80,14 +80,18 @@ public function onElementProperty(ReflectionProperty $property, string $name, st
}
}
}
+
+ foreach ($metadata->registeredChildAttributes($name) as $attribute) {
+ $attribute->applyOnChildBuilder($form, $elementBuilder);
+ }
}
/**
* {@inheritdoc}
*/
- public function onPostConfigure(array $elementProperties, array $buttonProperties, AttributeForm $form): ?PostConfigureInterface
+ public function onPostConfigure(ProcessorMetadata $metadata, AttributeForm $form): ?PostConfigureInterface
{
- return new PostConfigureReflectionSetProperties($elementProperties, $buttonProperties);
+ return new PostConfigureReflectionSetProperties($metadata->elementProperties(), $metadata->buttonProperties());
}
/**
diff --git a/src/Processor/GenerateConfiguratorStrategy.php b/src/Processor/GenerateConfiguratorStrategy.php
index 5e2ae5f..503a884 100644
--- a/src/Processor/GenerateConfiguratorStrategy.php
+++ b/src/Processor/GenerateConfiguratorStrategy.php
@@ -50,19 +50,24 @@ public function __construct(string $className)
/**
* {@inheritdoc}
*/
- public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder): void
+ public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void
{
+ $empty = true;
+
foreach ($formClass->getAttributes(FormBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
$attribute->newInstance()->generateCodeForFormBuilder($this->generator, $form);
+ $empty = false;
}
- $this->generator->line();
+ if (!$empty) {
+ $this->generator->line();
+ }
}
/**
* {@inheritdoc}
*/
- public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder): void
+ public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void
{
$this->generator->line('$builder->submit(?)', [$name]);
@@ -76,7 +81,7 @@ public function onButtonProperty(ReflectionProperty $property, string $name, Att
/**
* {@inheritdoc}
*/
- public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder): void
+ public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void
{
$elementType = $this->generator->useAndSimplifyType($elementType);
$this->generator->line('$? = $builder->add(?, ?::class);', [$name, $name, new Literal($elementType)]);
@@ -96,13 +101,17 @@ public function onElementProperty(ReflectionProperty $property, string $name, st
}
}
+ foreach ($metadata->registeredChildAttributes($name) as $attribute) {
+ $attribute->generateCodeForChildBuilder($name, $this->generator, $form);
+ }
+
$this->generator->line(); // Add empty line
}
/**
* {@inheritdoc}
*/
- public function onPostConfigure(array $elementProperties, array $buttonProperties, AttributeForm $form): ?PostConfigureInterface
+ public function onPostConfigure(ProcessorMetadata $metadata, AttributeForm $form): ?PostConfigureInterface
{
$this->generator->line('return $this;');
@@ -111,6 +120,9 @@ public function onPostConfigure(array $elementProperties, array $buttonPropertie
->implementsMethod(PostConfigureInterface::class, 'postConfigure')
;
+ $elementProperties = $metadata->elementProperties();
+ $buttonProperties = $metadata->buttonProperties();
+
if (!empty($buttonProperties)) {
$method->addBody('$root = $form->root();');
}
diff --git a/src/Processor/MethodChildBuilderAttributeInterface.php b/src/Processor/MethodChildBuilderAttributeInterface.php
new file mode 100644
index 0000000..1e15a40
--- /dev/null
+++ b/src/Processor/MethodChildBuilderAttributeInterface.php
@@ -0,0 +1,33 @@
+
+ */
+ public function targetElements(): array;
+
+ /**
+ * The actual object which will be applied on the child builder
+ *
+ * @param ReflectionMethod $method The method where the attribute is located
+ * @return ChildBuilderAttributeInterface
+ */
+ public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface;
+}
diff --git a/src/Processor/ProcessorMetadata.php b/src/Processor/ProcessorMetadata.php
new file mode 100644
index 0000000..4ccf720
--- /dev/null
+++ b/src/Processor/ProcessorMetadata.php
@@ -0,0 +1,93 @@
+
+ */
+ private array $buttonProperties = [];
+
+ /**
+ * @var array
+ */
+ private array $elementProperties = [];
+
+ /**
+ * @var array>
+ */
+ private array $childAttributes = [];
+
+ /**
+ * @param non-empty-string $name
+ * @param ReflectionProperty $property
+ * @return void
+ */
+ public function addButtonProperty(string $name, ReflectionProperty $property): void
+ {
+ $this->buttonProperties[$name] = $property;
+ }
+
+ /**
+ * @param non-empty-string $name
+ * @param ReflectionProperty $property
+ * @return void
+ */
+ public function addElementProperty(string $name, ReflectionProperty $property): void
+ {
+ $this->elementProperties[$name] = $property;
+ }
+
+ public function addChildAttribute(string $elementName, ChildBuilderAttributeInterface $attribute): void
+ {
+ $this->childAttributes[$elementName][] = $attribute;
+ }
+
+ /**
+ * @return array
+ */
+ public function buttonProperties(): array
+ {
+ return $this->buttonProperties;
+ }
+
+ /**
+ * @return array
+ */
+ public function elementProperties(): array
+ {
+ return $this->elementProperties;
+ }
+
+ /**
+ * Check if the given property has already been registered
+ *
+ * @param string $name The property name
+ * @return bool
+ */
+ public function hasProperty(string $name): bool
+ {
+ return isset($this->buttonProperties[$name]) || isset($this->elementProperties[$name]);
+ }
+
+ /**
+ * Get child attributes manually registered for the given element name
+ * Those attributes are generally registered by the {@see MethodChildBuilderAttributeInterface} attributes on methods
+ *
+ * @param string $name
+ * @return list
+ */
+ public function registeredChildAttributes(string $name): array
+ {
+ return $this->childAttributes[$name] ?? [];
+ }
+}
diff --git a/src/Processor/ReflectionProcessor.php b/src/Processor/ReflectionProcessor.php
index e620d80..2dd97a2 100644
--- a/src/Processor/ReflectionProcessor.php
+++ b/src/Processor/ReflectionProcessor.php
@@ -6,7 +6,9 @@
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Button\ButtonInterface;
use Bdf\Form\ElementInterface;
+use ReflectionAttribute;
use ReflectionClass;
+use ReflectionMethod;
use ReflectionNamedType;
/**
@@ -39,11 +41,13 @@ public function __construct(ReflectionStrategyInterface $strategy)
*/
public function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
{
- $elementProperties = [];
- $buttonProperties = [];
+ $metadata = new ProcessorMetadata();
+
+ // First iterate over methods to build the metadata
+ $this->registerMethodsMetadata($form, $metadata);
foreach ($this->iterateClassHierarchy($form) as $formClass) {
- $this->strategy->onFormClass($formClass, $form, $builder);
+ $this->strategy->onFormClass($formClass, $form, $builder, $metadata);
foreach ($formClass->getProperties() as $property) {
$name = $property->getName();
@@ -51,8 +55,7 @@ public function configureBuilder(AttributeForm $form, FormBuilderInterface $buil
if (
!$property->hasType()
|| !$property->getType() instanceof ReflectionNamedType
- || isset($elementProperties[$name])
- || isset($buttonProperties[$name])
+ || $metadata->hasProperty($name)
) {
continue;
}
@@ -61,16 +64,16 @@ public function configureBuilder(AttributeForm $form, FormBuilderInterface $buil
$property->setAccessible(true);
if ($elementType === ButtonInterface::class) {
- $buttonProperties[$name] = $property;
- $this->strategy->onButtonProperty($property, $name, $form, $builder);
+ $metadata->addButtonProperty($name, $property);
+ $this->strategy->onButtonProperty($property, $name, $form, $builder, $metadata);
} elseif (is_subclass_of($elementType, ElementInterface::class)) {
- $elementProperties[$name] = $property;
- $this->strategy->onElementProperty($property, $name, $elementType, $form, $builder);
+ $metadata->addElementProperty($name, $property);
+ $this->strategy->onElementProperty($property, $name, $elementType, $form, $builder, $metadata);
}
}
}
- return $this->strategy->onPostConfigure($elementProperties, $buttonProperties, $form);
+ return $this->strategy->onPostConfigure($metadata, $form);
}
/**
@@ -89,4 +92,26 @@ private function iterateClassHierarchy(AttributeForm $form): iterable
yield $reflection;
}
}
+
+ /**
+ * Fill the metadata from methods attributes
+ *
+ * @param AttributeForm $form
+ * @param ProcessorMetadata $metadata
+ *
+ * @return void
+ */
+ private function registerMethodsMetadata(AttributeForm $form, ProcessorMetadata $metadata): void
+ {
+ foreach ((new ReflectionClass($form))->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
+ foreach ($method->getAttributes(MethodChildBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
+ /** @var MethodChildBuilderAttributeInterface $attributeInstance */
+ $attributeInstance = $attribute->newInstance();
+
+ foreach ($attributeInstance->targetElements() as $target) {
+ $metadata->addChildAttribute($target, $attributeInstance->attribute($method));
+ }
+ }
+ }
+ }
}
diff --git a/src/Processor/ReflectionStrategyInterface.php b/src/Processor/ReflectionStrategyInterface.php
index 719ae0b..dd72a91 100644
--- a/src/Processor/ReflectionStrategyInterface.php
+++ b/src/Processor/ReflectionStrategyInterface.php
@@ -25,10 +25,11 @@ interface ReflectionStrategyInterface
* @param ReflectionClass $formClass Form class to use
* @param AttributeForm $form The current form instance
* @param FormBuilderInterface $builder Builder to configure
+ * @param ProcessorMetadata $metadata Metadata for the current form
*
* @return void
*/
- public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder): void;
+ public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void;
/**
* Configure a button following the declared property
@@ -39,10 +40,11 @@ public function onFormClass(ReflectionClass $formClass, AttributeForm $form, For
* @param non-empty-string $name The button name
* @param AttributeForm $form The current form instance
* @param FormBuilderInterface $builder Builder to configure
+ * @param ProcessorMetadata $metadata Metadata for the current form
*
* @return void
*/
- public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder): void;
+ public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void;
/**
* Configure an element following the declared property
@@ -54,16 +56,17 @@ public function onButtonProperty(ReflectionProperty $property, string $name, Att
* @param class-string $elementType The element type (i.e. the property type)
* @param AttributeForm $form The current form instance
* @param FormBuilderInterface $builder Builder to configure
+ * @param ProcessorMetadata $metadata Metadata for the current form
*
* @return void
*/
- public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder): void;
+ public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void;
/**
- * @param array $elementProperties
- * @param array $buttonProperties
- * @param AttributeForm $form
+ * @param ProcessorMetadata $metadata Metadata for the current form
+ * @param AttributeForm $form The current form instance
+ *
* @return PostConfigureInterface|null
*/
- public function onPostConfigure(array $elementProperties, array $buttonProperties, AttributeForm $form): ?PostConfigureInterface;
+ public function onPostConfigure(ProcessorMetadata $metadata, AttributeForm $form): ?PostConfigureInterface;
}
diff --git a/tests/Aggregate/ArrayTransformerTest.php b/tests/Aggregate/ArrayTransformerTest.php
new file mode 100644
index 0000000..0f7c0d6
--- /dev/null
+++ b/tests/Aggregate/ArrayTransformerTest.php
@@ -0,0 +1,94 @@
+submit(['foo' => ['_', '-']]);
+ $this->assertEquals(['A_', 'A-'], $form->foo->value());
+
+ $view = $form->view();
+ $this->assertEquals(['A_A', 'A-A'], $view['foo']->value());
+ }
+
+ public function test_code_generator()
+ {
+ $form = new class extends AttributeForm {
+ #[ArrayTransformer(AArrayTransformer::class, ['A'])]
+ public ArrayElement $foo;
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Tests\Form\Attribute\Aggregate\AArrayTransformer;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', ArrayElement::class);
+ $foo->arrayTransformer(new AArrayTransformer('A'));
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ }
+}
+
+PHP
+ , $form
+);
+ }
+}
+
+class AArrayTransformer implements TransformerInterface
+{
+ public function __construct(
+ public string $c
+ ) {
+ }
+
+ public function transformToHttp($value, ElementInterface $input)
+ {
+ return array_map(fn($v) => $v . $this->c, $value);
+ }
+
+ public function transformFromHttp($value, ElementInterface $input)
+ {
+ return array_map(fn($v) => $this->c . $v, $value);
+ }
+}
diff --git a/tests/Aggregate/AsArrayConstraintTest.php b/tests/Aggregate/AsArrayConstraintTest.php
new file mode 100644
index 0000000..baaae23
--- /dev/null
+++ b/tests/Aggregate/AsArrayConstraintTest.php
@@ -0,0 +1,108 @@
+submit(['foo' => ['a']]);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals('Foo size must be a multiple of 2', $form->foo->error()->global());
+
+ $form->submit(['foo' => ['a', 'b']]);
+
+ $this->assertTrue($form->valid());
+ $this->assertNull($form->foo->error()->global());
+
+ $form->submit(['bar' => ['a']]);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals('The value is invalid', $form->bar->error()->global());
+
+ $form->submit(['bar' => ['a', 'b']]);
+
+ $this->assertTrue($form->valid());
+ $this->assertNull($form->bar->error()->global());
+ }
+
+ public function test_code_generator()
+ {
+ $form = new class extends AttributeForm {
+ public ArrayElement $foo;
+
+ public ArrayElement $bar;
+
+ #[AsArrayConstraint('foo', message: 'Foo size must be a multiple of 2')]
+ #[AsArrayConstraint('bar')]
+ public function validateFoo(array $value): bool
+ {
+ return count($value) % 2 === 0;
+ }
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Constraint\Closure as ClosureConstraint;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', ArrayElement::class);
+ $foo->arrayConstraint(new ClosureConstraint(['callback' => [$form, 'validateFoo'], 'message' => 'Foo size must be a multiple of 2']));
+
+ $bar = $builder->add('bar', ArrayElement::class);
+ $bar->arrayConstraint(new ClosureConstraint([$form, 'validateFoo']));
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ $form->bar = $inner['bar']->element();
+ }
+}
+
+PHP
+ , $form
+ );
+ }
+}
diff --git a/tests/Child/AsFilterTest.php b/tests/Child/AsFilterTest.php
new file mode 100644
index 0000000..4471fad
--- /dev/null
+++ b/tests/Child/AsFilterTest.php
@@ -0,0 +1,98 @@
+submit(['a' => 'Zm9v']);
+ $this->assertEquals('foo', $form->a->value());
+ }
+
+ /**
+ *
+ */
+ public function test_code_generator()
+ {
+ $form = new class() extends AttributeForm {
+ #[Getter, Setter]
+ public StringElement $foo;
+ public StringElement $bar;
+
+ #[AsFilter('foo', 'bar')]
+ public function aFilter($value, Child $input, $default)
+ {
+ return base64_decode($value);
+ }
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->extractor(new Getter());
+ $foo->hydrator(new Setter());
+ $foo->filter([$form, 'aFilter']);
+
+ $bar = $builder->add('bar', StringElement::class);
+ $bar->filter([$form, 'aFilter']);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ $form->bar = $inner['bar']->element();
+ }
+}
+
+PHP
+ , $form
+);
+ }
+}
diff --git a/tests/Child/AsModelTransformerTest.php b/tests/Child/AsModelTransformerTest.php
new file mode 100644
index 0000000..53a292d
--- /dev/null
+++ b/tests/Child/AsModelTransformerTest.php
@@ -0,0 +1,96 @@
+submit(['a' => 'foo']);
+ $this->assertEquals(new Struct(a: 'Zm9v'), $form->value());
+
+ $form->import(new Struct(a: 'SGVsbG8gV29ybGQgIQ=='));
+ $this->assertEquals('Hello World !', $form->a->value());
+ }
+
+ /**
+ *
+ */
+ public function test_code_generator()
+ {
+ $form = new class extends AttributeForm {
+ #[Getter, Setter]
+ public IntegerElement $foo;
+
+ #[AsModelTransformer('foo')]
+ public function t($value, $input)
+ {
+ return $value + 1;
+ }
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\IntegerElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', IntegerElement::class);
+ $foo->extractor(new Getter());
+ $foo->hydrator(new Setter());
+ $foo->modelTransformer([$form, 't']);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ }
+}
+
+PHP
+ , $form
+);
+ }
+}
diff --git a/tests/Child/CallbackFilterTest.php b/tests/Child/CallbackFilterTest.php
new file mode 100644
index 0000000..fe82523
--- /dev/null
+++ b/tests/Child/CallbackFilterTest.php
@@ -0,0 +1,90 @@
+submit(['a' => 'Zm9v']);
+ $this->assertEquals('foo', $form->a->value());
+ }
+
+ /**
+ *
+ */
+ public function test_code_generator()
+ {
+ $form = new class() extends AttributeForm {
+ #[CallbackFilter('aFilter'), Getter, Setter]
+ public StringElement $foo;
+
+ public function aFilter($value, Child $input, $default)
+ {
+ return base64_decode($value);
+ }
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->filter([$form, 'aFilter']);
+ $foo->extractor(new Getter());
+ $foo->hydrator(new Setter());
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ }
+}
+
+PHP
+ , $form
+);
+ }
+}
diff --git a/tests/Child/ConfigureTest.php b/tests/Child/ConfigureTest.php
index a557b54..aea2064 100644
--- a/tests/Child/ConfigureTest.php
+++ b/tests/Child/ConfigureTest.php
@@ -38,6 +38,32 @@ public function configureFoo(ChildBuilderInterface $builder): void
$this->assertTrue($form->valid());
}
+ /**
+ * @dataProvider provideAttributesProcessor
+ */
+ public function test_on_method(AttributesProcessorInterface $processor)
+ {
+ $form = new class(null, $processor) extends AttributeForm {
+ public StringElement $foo;
+
+ /**
+ * @param ChildBuilderInterface|StringElementBuilder $builder
+ */
+ #[Configure('foo')]
+ public function configureFoo(ChildBuilderInterface $builder): void
+ {
+ $builder->length(['min' => 3]);
+ }
+ };
+
+ $form->submit(['foo' => 'a']);
+ $this->assertFalse($form->valid());
+ $this->assertEquals(['foo' => 'This value is too short. It should have 3 characters or more.'], $form->error()->toArray());
+
+ $form->submit(['foo' => 'abc']);
+ $this->assertTrue($form->valid());
+ }
+
public function test_code_generator()
{
$form = new class extends AttributeForm {
@@ -85,6 +111,58 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
}
}
+PHP
+ , $form
+);
+ }
+
+ public function test_code_generator_on_method()
+ {
+ $form = new class extends AttributeForm {
+ public StringElement $foo;
+
+ /**
+ * @param ChildBuilderInterface|StringElementBuilder $builder
+ */
+ #[Configure('foo')]
+ public function configureFoo(ChildBuilderInterface $builder): void
+ {
+ $builder->length(['min' => 3]);
+ }
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\StringElement;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', StringElement::class);
+ $form->configureFoo($foo);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ }
+}
+
PHP
, $form
);
diff --git a/tests/Child/HttpFieldTest.php b/tests/Child/HttpFieldTest.php
new file mode 100644
index 0000000..41ffe08
--- /dev/null
+++ b/tests/Child/HttpFieldTest.php
@@ -0,0 +1,82 @@
+submit(['_v_' => 42]);
+ $this->assertSame(42, $form->v->value());
+ $this->assertSame(['_v_' => '42'], $form->httpValue());
+ }
+
+ /**
+ * @return void
+ */
+ public function test_code_generator()
+ {
+ $form = new class extends AttributeForm {
+ #[HttpField('_v_')]
+ public IntegerElement $v;
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Child\Http\ArrayOffsetHttpFields;
+use Bdf\Form\Leaf\IntegerElement;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $v = $builder->add('v', IntegerElement::class);
+ $v->httpFields(new ArrayOffsetHttpFields('_v_'));
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->v = $inner['v']->element();
+ }
+}
+
+PHP
+ , $form);
+ }
+}
diff --git a/tests/Constraint/AsConstraintTest.php b/tests/Constraint/AsConstraintTest.php
new file mode 100644
index 0000000..6677a39
--- /dev/null
+++ b/tests/Constraint/AsConstraintTest.php
@@ -0,0 +1,100 @@
+submit(['foo' => 'a']);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals('Foo length must be a multiple of 2', $form->foo->error()->global());
+
+ $form->submit(['foo' => 'abcd']);
+
+ $this->assertTrue($form->valid());
+ $this->assertNull($form->foo->error()->global());
+
+ $form->submit(['bar' => 'a']);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals('The value is invalid', $form->bar->error()->global());
+
+ $form->submit(['bar' => 'abcd']);
+
+ $this->assertTrue($form->valid());
+ $this->assertNull($form->bar->error()->global());
+ }
+
+ public function test_code_generator()
+ {
+ $form = new class extends AttributeForm {
+ public StringElement $foo;
+
+ #[AsConstraint('foo', message: 'Foo length must be a multiple of 2')]
+ public function validateFoo($value): bool
+ {
+ return strlen($value) % 2 === 0;
+ }
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Constraint\Closure as ClosureConstraint;
+use Bdf\Form\Leaf\StringElement;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->satisfy(new ClosureConstraint(['callback' => [$form, 'validateFoo'], 'message' => 'Foo length must be a multiple of 2']));
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ }
+}
+
+PHP
+ , $form
+);
+ }
+}
diff --git a/tests/Element/AsTransformerTest.php b/tests/Element/AsTransformerTest.php
new file mode 100644
index 0000000..c6161bf
--- /dev/null
+++ b/tests/Element/AsTransformerTest.php
@@ -0,0 +1,85 @@
+submit(['foo' => 'a']);
+
+ $this->assertEquals('["a",true]', $form->foo->value());
+
+ $view = $form->view();
+
+ $this->assertEquals('["[\"a\",true]",false]', $view['foo']->value());
+ }
+
+
+ public function test_code_generator()
+ {
+ $form = new class() extends AttributeForm {
+ public StringElement $foo;
+
+ #[AsTransformer('foo')]
+ public function fooTransformer($value, StringElement $input, bool $toPhp)
+ {
+ return json_encode([$value, $toPhp]);
+ }
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\StringElement;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->transformer([$form, 'fooTransformer']);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ }
+}
+
+PHP
+ , $form);
+ }
+}
diff --git a/tests/Element/Date/AfterFieldTest.php b/tests/Element/Date/AfterFieldTest.php
new file mode 100644
index 0000000..34e6be7
--- /dev/null
+++ b/tests/Element/Date/AfterFieldTest.php
@@ -0,0 +1,158 @@
+submit([
+ 'foo' => '2020-11-02T15:23:00Z',
+ 'bar' => '2020-11-02T15:21:00Z',
+ ]);
+
+ $this->assertTrue($form->valid());
+
+ $form->submit([
+ 'foo' => '2020-11-02T15:20:50Z',
+ 'bar' => '2020-11-02T15:21:50Z',
+ ]);
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'foo' => 'This value should be greater than Nov 2, 2020, 3:21 PM.',
+ ], self::normalizeSpace($form->error()->toArray()));
+ }
+
+ /**
+ * @dataProvider provideAttributesProcessor
+ */
+ public function test_with_message(AttributesProcessorInterface $processor)
+ {
+ $form = new class(null, $processor) extends AttributeForm {
+ #[Dependencies('bar'), AfterField('bar', message: 'my error')]
+ public DateTimeElement $foo;
+
+ public DateTimeElement $bar;
+ };
+
+ $form->submit([
+ 'foo' => '2020-11-02T15:23:00Z',
+ 'bar' => '2020-11-02T15:21:00Z',
+ ]);
+
+ $this->assertTrue($form->valid());
+
+ $form->submit([
+ 'foo' => '2020-11-02T15:20:50Z',
+ 'bar' => '2020-11-02T15:21:00Z',
+ ]);
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'foo' => 'my error',
+ ], $form->error()->toArray());
+ }
+
+ /**
+ * @dataProvider provideAttributesProcessor
+ */
+ public function test_with_or_equal(AttributesProcessorInterface $processor)
+ {
+ $form = new class(null, $processor) extends AttributeForm {
+ #[Dependencies('bar'), AfterField('bar', orEqual: true)]
+ public DateTimeElement $foo;
+
+ public DateTimeElement $bar;
+ };
+
+ $form->submit([
+ 'foo' => '2020-11-02T15:21:00Z',
+ 'bar' => '2020-11-02T15:21:00Z',
+ ]);
+
+ $this->assertTrue($form->valid());
+
+ $form->submit([
+ 'foo' => '2020-11-02T15:20:50Z',
+ 'bar' => '2020-11-02T15:21:00Z',
+ ]);
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'foo' => 'This value should be greater than or equal to Nov 2, 2020, 3:21 PM.',
+ ], self::normalizeSpace($form->error()->toArray()));
+ }
+
+ public function test_code_generator()
+ {
+ $form = new class extends AttributeForm {
+ #[Dependencies('bar'), AfterField('bar', 'my error', true)]
+ public DateTimeElement $foo;
+ public DateTimeElement $bar;
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\Date\DateTimeElement;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', DateTimeElement::class);
+ $foo->depends('bar');
+ $foo->afterField('bar', 'my error', true);
+
+ $bar = $builder->add('bar', DateTimeElement::class);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ $form->bar = $inner['bar']->element();
+ }
+}
+
+PHP
+ , $form
+ );
+ }
+
+ public static function normalizeSpace(array|string $value): array|string
+ {
+ if (\is_array($value)) {
+ return \array_map([self::class, 'normalizeSpace'], $value);
+ }
+
+ return \preg_replace('/\p{Zs}+/u', ' ', $value);
+ }
+}
diff --git a/tests/Element/Date/BeforeFieldTest.php b/tests/Element/Date/BeforeFieldTest.php
new file mode 100644
index 0000000..180ba86
--- /dev/null
+++ b/tests/Element/Date/BeforeFieldTest.php
@@ -0,0 +1,159 @@
+submit([
+ 'foo' => '2020-11-02T15:21:00Z',
+ 'bar' => '2020-11-02T15:23:00Z',
+ ]);
+
+ $this->assertTrue($form->valid());
+
+ $form->submit([
+ 'foo' => '2020-11-02T15:21:00Z',
+ 'bar' => '2020-11-02T15:20:50Z',
+ ]);
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'foo' => 'This value should be less than Nov 2, 2020, 3:20 PM.',
+ ], self::normalizeSpace($form->error()->toArray()));
+ }
+
+ /**
+ * @dataProvider provideAttributesProcessor
+ */
+ public function test_with_message(AttributesProcessorInterface $processor)
+ {
+ $form = new class(null, $processor) extends AttributeForm {
+ #[Dependencies('bar'), BeforeField('bar', message: 'my error')]
+ public DateTimeElement $foo;
+
+ public DateTimeElement $bar;
+ };
+
+ $form->submit([
+ 'foo' => '2020-11-02T15:21:00Z',
+ 'bar' => '2020-11-02T15:23:00Z',
+ ]);
+
+ $this->assertTrue($form->valid());
+
+ $form->submit([
+ 'foo' => '2020-11-02T15:21:00Z',
+ 'bar' => '2020-11-02T15:20:50Z',
+ ]);
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'foo' => 'my error',
+ ], $form->error()->toArray());
+ }
+
+ /**
+ * @dataProvider provideAttributesProcessor
+ */
+ public function test_with_or_equal(AttributesProcessorInterface $processor)
+ {
+ $form = new class(null, $processor) extends AttributeForm {
+ #[Dependencies('bar'), BeforeField('bar', orEqual: true)]
+ public DateTimeElement $foo;
+
+ public DateTimeElement $bar;
+ };
+
+ $form->submit([
+ 'foo' => '2020-11-02T15:21:00Z',
+ 'bar' => '2020-11-02T15:21:00Z',
+ ]);
+
+ $this->assertTrue($form->valid());
+
+ $form->submit([
+ 'foo' => '2020-11-02T15:21:00Z',
+ 'bar' => '2020-11-02T15:20:50Z',
+ ]);
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'foo' => 'This value should be less than or equal to Nov 2, 2020, 3:20 PM.',
+ ], self::normalizeSpace($form->error()->toArray()));
+ }
+
+ public function test_code_generator()
+ {
+ $form = new class extends AttributeForm {
+ #[Dependencies('bar'), BeforeField('bar', 'my error', true)]
+ public DateTimeElement $foo;
+ public DateTimeElement $bar;
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\Date\DateTimeElement;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', DateTimeElement::class);
+ $foo->depends('bar');
+ $foo->beforeField('bar', 'my error', true);
+
+ $bar = $builder->add('bar', DateTimeElement::class);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ $form->bar = $inner['bar']->element();
+ }
+}
+
+PHP
+ , $form
+ );
+ }
+
+ public static function normalizeSpace(array|string $value): array|string
+ {
+ if (\is_array($value)) {
+ return \array_map([self::class, 'normalizeSpace'], $value);
+ }
+
+ return \preg_replace('/\p{Zs}+/u', ' ', $value);
+ }
+}
diff --git a/tests/Element/RequiredTest.php b/tests/Element/RequiredTest.php
new file mode 100644
index 0000000..6df30e7
--- /dev/null
+++ b/tests/Element/RequiredTest.php
@@ -0,0 +1,84 @@
+submit(['foo' => '']);
+ $this->assertEquals([
+ 'foo' => 'This value should not be blank.',
+ 'bar' => 'my message',
+ ], $form->error()->toArray());
+
+ $form->submit(['foo' => '1.2', 'bar' => '4.5']);
+ $this->assertTrue($form->valid());
+ }
+
+ public function test_code_generator()
+ {
+ $form = new class extends AttributeForm {
+ #[Required]
+ public FloatElement $foo;
+ #[Required('my message')]
+ public FloatElement $bar;
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\FloatElement;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', FloatElement::class);
+ $foo->required(null);
+
+ $bar = $builder->add('bar', FloatElement::class);
+ $bar->required('my message');
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ $form->bar = $inner['bar']->element();
+ }
+}
+
+PHP
+ , $form
+ );
+ }
+}
diff --git a/tests/Element/TransformerTest.php b/tests/Element/TransformerTest.php
index 6da284c..8f24d2e 100644
--- a/tests/Element/TransformerTest.php
+++ b/tests/Element/TransformerTest.php
@@ -2,6 +2,7 @@
namespace Tests\Form\Attribute\Element;
+use Bdf\Form\Aggregate\ArrayElement;
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Element\Transformer;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
@@ -29,6 +30,23 @@ public function test(AttributesProcessorInterface $processor)
$this->assertEquals('A_A', $view['foo']->value());
}
+ /**
+ * @dataProvider provideAttributesProcessor
+ */
+ public function testWithArray(AttributesProcessorInterface $processor)
+ {
+ $form = new class(null, $processor) extends AttributeForm {
+ #[Transformer(AArrayTransformer::class, ['A'], array: true)]
+ public ArrayElement $foo;
+ };
+
+ $form->submit(['foo' => ['_', '-']]);
+ $this->assertEquals(['A_', 'A-'], $form->foo->value());
+
+ $view = $form->view();
+ $this->assertEquals(['A_A', 'A-A'], $view['foo']->value());
+ }
+
public function test_code_generator()
{
$form = new class extends AttributeForm {
@@ -69,6 +87,51 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
}
}
+PHP
+ , $form
+);
+ }
+
+ public function test_code_generator_with_array()
+ {
+ $form = new class extends AttributeForm {
+ #[Transformer(AArrayTransformer::class, ['A'], array: true)]
+ public ArrayElement $foo;
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Tests\Form\Attribute\Element\AArrayTransformer;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', ArrayElement::class);
+ $foo->arrayTransformer(new AArrayTransformer('A'));
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ }
+}
+
PHP
, $form
);
@@ -92,3 +155,21 @@ public function transformFromHttp($value, ElementInterface $input)
return $this->c . $value;
}
}
+
+class AArrayTransformer implements TransformerInterface
+{
+ public function __construct(
+ public string $c
+ ) {
+ }
+
+ public function transformToHttp($value, ElementInterface $input)
+ {
+ return array_map(fn($v) => $v . $this->c, $value);
+ }
+
+ public function transformFromHttp($value, ElementInterface $input)
+ {
+ return array_map(fn($v) => $this->c . $v, $value);
+ }
+}
diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php
index 987e262..e84898f 100644
--- a/tests/FunctionalTest.php
+++ b/tests/FunctionalTest.php
@@ -4,6 +4,7 @@
use Attribute;
use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Element\AsTransformer;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Button\ButtonInterface;
use Bdf\Form\Button\SubmitButton;
@@ -150,7 +151,6 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?
$bar->hydrator(new Setter());
$bar->satisfy(new GreaterThan(5));
-
$foo = $builder->add('foo', StringElement::class);
$foo->satisfy(new NotBlank());
$foo->extractor(new Getter());
@@ -178,6 +178,70 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
);
}
+ /**
+ *
+ */
+ public function test_inheritance_code_generator_with_method()
+ {
+ $form = new ChildFormWithMethod();
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\IntegerElement;
+use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Symfony\Component\Validator\Constraints\GreaterThan;
+use Symfony\Component\Validator\Constraints\NotBlank;
+use Tests\Form\Attribute\BaseFormWithMethod;
+use Tests\Form\Attribute\ChildFormWithMethod;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $bar = $builder->add('bar', IntegerElement::class);
+ $bar->satisfy(new NotBlank());
+ $bar->extractor(new Getter());
+ $bar->hydrator(new Setter());
+ $bar->satisfy(new GreaterThan(5));
+
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->extractor(new Getter());
+ $foo->hydrator(new Setter());
+ $foo->transformer([$form, 'transform']);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ (\Closure::bind(function () use ($inner, $form) {
+ $form->bar = $inner['bar']->element();
+ }, null, ChildFormWithMethod::class))();
+ (\Closure::bind(function () use ($inner, $form) {
+ $form->foo = $inner['foo']->element();
+ }, null, BaseFormWithMethod::class))();
+ }
+}
+
+PHP
+ , $form
+ );
+ }
+
/**
* @dataProvider provideAttributesProcessor
*/
@@ -261,3 +325,21 @@ public function transformFromHttp($value, ElementInterface $input)
return base64_encode($value);
}
}
+
+class BaseFormWithMethod extends AttributeForm
+{
+ #[Getter, Setter]
+ private StringElement $foo;
+
+ #[AsTransformer('foo')]
+ public function transform(string $value): string
+ {
+ return str_rot13($value);
+ }
+}
+
+class ChildFormWithMethod extends BaseFormWithMethod
+{
+ #[NotBlank, Getter, Setter, GreaterThan(5)]
+ private IntegerElement $bar;
+}
diff --git a/tests/Php81/Aggregate/ArrayConstraintTest.php b/tests/Php81/Aggregate/ArrayConstraintTest.php
new file mode 100644
index 0000000..67c44c5
--- /dev/null
+++ b/tests/Php81/Aggregate/ArrayConstraintTest.php
@@ -0,0 +1,97 @@
+ 'Not unique']))]
+ public ArrayElement $values;
+ };
+
+ $form->submit(['values' => ['aaa', 'aaa']]);
+ $this->assertFalse($form->valid());
+ $this->assertEquals(['values' => 'Not unique'], $form->error()->toArray());
+
+ $form->submit(['values' => ['aaa', 'bbb']]);
+ $this->assertTrue($form->valid());
+ }
+
+ /**
+ * @dataProvider provideAttributesProcessor
+ */
+ public function test_disallow_constraint_instance_with_option_arg(AttributesProcessorInterface $processor)
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Cannot use options with constraint instance');
+
+ $form = new class(null, $processor) extends AttributeForm {
+ #[ArrayConstraint(new Unique(['message' => 'Not unique']), ['foo' => 'bar'])]
+ public ArrayElement $values;
+ };
+
+ $form->submit(['values' => ['aaa', 'aaa']]);
+ }
+
+ /**
+ * @return void
+ */
+ public function test_code_generator()
+ {
+ $form = new class extends AttributeForm {
+ #[ArrayConstraint(new Unique(['message' => 'Not unique']))]
+ public ArrayElement $values;
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Symfony\Component\Validator\Constraints\Unique;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $values = $builder->add('values', ArrayElement::class);
+ $values->arrayConstraint(new Unique(['message' => 'Not unique', 'groups' => ['Default']]));
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->values = $inner['values']->element();
+ }
+}
+
+PHP
+ , $form);
+ }
+}
diff --git a/tests/Php81/Aggregate/CallbackArrayConstraintTest.php b/tests/Php81/Aggregate/CallbackArrayConstraintTest.php
new file mode 100644
index 0000000..7c64ffc
--- /dev/null
+++ b/tests/Php81/Aggregate/CallbackArrayConstraintTest.php
@@ -0,0 +1,108 @@
+submit(['foo' => ['a']]);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals('Foo size must be a multiple of 2', $form->foo->error()->global());
+
+ $form->submit(['foo' => ['a', 'b']]);
+
+ $this->assertTrue($form->valid());
+ $this->assertNull($form->foo->error()->global());
+
+ $form->submit(['bar' => ['a']]);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals('The value is invalid', $form->bar->error()->global());
+
+ $form->submit(['bar' => ['a', 'b']]);
+
+ $this->assertTrue($form->valid());
+ $this->assertNull($form->bar->error()->global());
+ }
+
+ public function test_code_generator()
+ {
+ $form = new class extends AttributeForm {
+ #[CallbackArrayConstraint('validateFoo', message: 'Foo size must be a multiple of 2')]
+ public ArrayElement $foo;
+
+ #[CallbackArrayConstraint('validateFoo')]
+ public ArrayElement $bar;
+
+ public function validateFoo(array $value): bool
+ {
+ return count($value) % 2 === 0;
+ }
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Constraint\Closure as ClosureConstraint;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', ArrayElement::class);
+ $foo->arrayConstraint(new ClosureConstraint(['callback' => [$form, 'validateFoo'], 'message' => 'Foo size must be a multiple of 2']));
+
+ $bar = $builder->add('bar', ArrayElement::class);
+ $bar->arrayConstraint(new ClosureConstraint([$form, 'validateFoo']));
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ $form->bar = $inner['bar']->element();
+ }
+}
+
+PHP
+ , $form
+ );
+ }
+}
diff --git a/tests/Php81/Constraint/SatisfyTest.php b/tests/Php81/Constraint/SatisfyTest.php
new file mode 100644
index 0000000..61d8011
--- /dev/null
+++ b/tests/Php81/Constraint/SatisfyTest.php
@@ -0,0 +1,76 @@
+ 3]))]
+ public StringElement $foo;
+ };
+
+ $form->submit(['foo' => 'ab']);
+ $this->assertFalse($form->valid());
+ $this->assertEquals(['foo' => 'This value is too short. It should have 3 characters or more.'], $form->error()->toArray());
+
+ $form->submit(['foo' => 'abc']);
+ $this->assertTrue($form->valid());
+ }
+
+ public function test_code_generator()
+ {
+ $form = new class extends AttributeForm {
+ #[Satisfy(new Length(['min' => 3]))]
+ public StringElement $foo;
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\StringElement;
+use Symfony\Component\Validator\Constraints\Length;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->satisfy(new Length(['min' => 3, 'groups' => ['Default']]));
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ }
+}
+
+PHP
+ , $form
+);
+ }
+}
diff --git a/tests/Processor/CodeGenerator/ObjectInstantiationTest.php b/tests/Processor/CodeGenerator/ObjectInstantiationTest.php
new file mode 100644
index 0000000..8d1c6be
--- /dev/null
+++ b/tests/Processor/CodeGenerator/ObjectInstantiationTest.php
@@ -0,0 +1,40 @@
+assertEquals("new Symfony\Component\Validator\Constraints\NotBlank(['groups' => ['Default']])", ObjectInstantiation::singleArrayParameter($o)->render());
+
+ $this->assertEquals($o, eval('return ' . ObjectInstantiation::singleArrayParameter($o)->render().';'));
+ }
+
+ public function test_with_parameters()
+ {
+ $o = new Range(['min' => 1, 'max' => 10]);
+
+ $this->assertEquals("new Symfony\Component\Validator\Constraints\Range(['min' => 1, 'max' => 10, 'groups' => ['Default']])", ObjectInstantiation::singleArrayParameter($o)->render());
+
+ $this->assertEquals($o, eval('return ' . ObjectInstantiation::singleArrayParameter($o)->render().';'));
+ }
+
+ public function test_with_simplified_class_name()
+ {
+ $o = new Range(['min' => 1, 'max' => 10]);
+ $generator = new ClassGenerator(new PhpNamespace('Foo'), new ClassType('Bar'));
+
+ $this->assertEquals("new Range(['min' => 1, 'max' => 10, 'groups' => ['Default']])", ObjectInstantiation::singleArrayParameter($o)->render($generator));
+ }
+}
diff --git a/tests/Processor/GenerateConfiguratorStrategyTest.php b/tests/Processor/GenerateConfiguratorStrategyTest.php
index 2b6f92d..7ad7fc7 100644
--- a/tests/Processor/GenerateConfiguratorStrategyTest.php
+++ b/tests/Processor/GenerateConfiguratorStrategyTest.php
@@ -208,13 +208,11 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?
{
$d = $builder->add('d', StringElement::class);
-
$builder->submit('b')
;
$c = $builder->add('c', StringElement::class);
-
$a = $builder->add('a', StringElement::class);
return $this;