diff --git a/src/Aggregate/ArrayElementBuilder.php b/src/Aggregate/ArrayElementBuilder.php
index eacd25f..23ce742 100644
--- a/src/Aggregate/ArrayElementBuilder.php
+++ b/src/Aggregate/ArrayElementBuilder.php
@@ -18,6 +18,8 @@
use Bdf\Form\Phone\PhoneElement;
use Bdf\Form\Registry\Registry;
use Bdf\Form\Registry\RegistryInterface;
+use Bdf\Form\Struct\StructForm;
+use Bdf\Form\Struct\StructFormBuilder;
use Bdf\Form\Transformer\TransformerInterface;
use Bdf\Form\Util\MagicCallForwarding;
use Bdf\Form\Util\TransformerBuilderTrait;
@@ -329,6 +331,35 @@ public function enum(string $enumClass, ?callable $configurator = null): static
});
}
+ /**
+ * Define as array of struct
+ *
+ *
+ * $builder->array('coordinates')->struct(Coordinate::class, function(StructFormBuilder $builder) {
+ * // ...
+ * })->getset();
+ *
+ *
+ * @param class-string $className The struct class name
+ * @param callable(StructFormBuilder):void|null $configurator Callback for configure the inner element builder
+ *
+ * @return static
+ * @psalm-this-out ArrayElementBuilder
+ *
+ * @template S as object
+ * @since 2.0
+ */
+ public function struct(string $className, ?callable $configurator = null): static
+ {
+ return $this->element(StructForm::class, function (StructFormBuilder $builder) use ($className, $configurator) {
+ $builder->class($className);
+
+ if ($configurator !== null) {
+ $configurator($builder);
+ }
+ });
+ }
+
/**
* Define as array of embedded forms
*
diff --git a/src/Aggregate/Form.php b/src/Aggregate/Form.php
index 3128546..9127f6c 100644
--- a/src/Aggregate/Form.php
+++ b/src/Aggregate/Form.php
@@ -20,7 +20,10 @@
use Iterator;
use Override;
-use function assert;
+use function is_array;
+use function is_object;
+use function sprintf;
+use function trigger_error;
/**
* The base form element
@@ -216,7 +219,7 @@ public function value(): array|object|null
}
/** @var T $value */
- return $this->value = $value;
+ return $this->value = $this->generator->finalize($value);
}
#[Override]
@@ -296,6 +299,11 @@ public function __clone()
#[Override]
public function attach($entity): FormInterface
{
+ /** @psalm-suppress RedundantCondition */
+ if (!is_object($entity) && !is_array($entity)) {
+ @trigger_error(sprintf('Attaching a non-object and non-array value is deprecated since bdf-form 2.0 and will be removed. Got %s.', get_debug_type($entity)), E_USER_DEPRECATED);
+ }
+
$this->generator->attach($entity);
$this->value = null; // The value is only attached : it must be filled when calling value()
diff --git a/src/Aggregate/FormBuilder.php b/src/Aggregate/FormBuilder.php
index cc41590..863d21a 100644
--- a/src/Aggregate/FormBuilder.php
+++ b/src/Aggregate/FormBuilder.php
@@ -36,6 +36,8 @@
use Bdf\Form\Phone\PhoneElementBuilder;
use Bdf\Form\Registry\RegistryInterface;
use Bdf\Form\RootElementInterface;
+use Bdf\Form\Struct\StructForm;
+use Bdf\Form\Struct\StructFormBuilder;
use Bdf\Form\Transformer\TransformerInterface;
use Bdf\Form\Validator\ValueValidatorInterface;
use Override;
@@ -239,6 +241,25 @@ public function enum(string $name, string $enumClass): ChildBuilderInterface
return $builder->enumClass($enumClass);
}
+ /**
+ * {@inheritdoc}
+ *
+ * @param non-empty-string $name The child name
+ * @param class-string $structClass The struct class name
+ * @return ChildBuilder
+ *
+ * @psalm-suppress MoreSpecificReturnType
+ * @psalm-suppress LessSpecificReturnStatement
+ */
+ #[Override]
+ public function struct(string $name, string $structClass): ChildBuilderInterface
+ {
+ /** @var ChildBuilderInterface $builder */
+ $builder = $this->add($name, StructForm::class);
+
+ return $builder->class($structClass);
+ }
+
/**
* {@inheritdoc}
*
@@ -390,7 +411,7 @@ public function generator(ValueGeneratorInterface $generator): FormBuilderInterf
* {@inheritdoc}
*/
#[Override]
- public function generates($entity): FormBuilderInterface
+ public function generates(mixed $entity): FormBuilderInterface
{
return $this->generator(new ValueGenerator($entity));
}
diff --git a/src/Aggregate/FormBuilderInterface.php b/src/Aggregate/FormBuilderInterface.php
index 6035349..342984b 100644
--- a/src/Aggregate/FormBuilderInterface.php
+++ b/src/Aggregate/FormBuilderInterface.php
@@ -19,6 +19,7 @@
use Bdf\Form\Leaf\StringElementBuilder;
use Bdf\Form\Phone\PhoneChildBuilder;
use Bdf\Form\Phone\PhoneElementBuilder;
+use Bdf\Form\Struct\StructFormBuilder;
use Override;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
@@ -189,6 +190,23 @@ public function phone(string $name): ChildBuilderInterface;
*/
public function enum(string $name, string $enumClass): ChildBuilderInterface;
+ /**
+ * Add a new embedded form following the given struct
+ *
+ *
+ * $builder->struct('options', Options::class)->getset();
+ *
+ *
+ * @param non-empty-string $name The child name
+ * @param class-string $structClass The struct class name
+ *
+ * @return ChildBuilder|StructFormBuilder
+ * @psalm-return ChildBuilderInterface
+ *
+ * @since 2.0
+ */
+ public function struct(string $name, string $structClass): ChildBuilderInterface;
+
/**
* Add a new csrf token on form
*
@@ -315,7 +333,7 @@ public function generator(ValueGeneratorInterface $generator): FormBuilderInterf
* });
*
*
- * @param callable|class-string|object|array $entity The entity to generate
+ * @param (callable(ElementInterface):array|object)|class-string|array|object $entity The entity to generate
*
* @return $this
*
diff --git a/src/Aggregate/FormInterface.php b/src/Aggregate/FormInterface.php
index 454c613..66d63f3 100644
--- a/src/Aggregate/FormInterface.php
+++ b/src/Aggregate/FormInterface.php
@@ -33,7 +33,7 @@ interface FormInterface extends ChildAggregateInterface
* $this->repository->save($form->value());
*
*
- * @param T|class-string|callable():T $entity The entity object, or class name
+ * @param T $entity The entity object, or initial array values
*
* @return $this
*
diff --git a/src/Aggregate/Value/ClosureValueGenerator.php b/src/Aggregate/Value/ClosureValueGenerator.php
new file mode 100644
index 0000000..abf9d32
--- /dev/null
+++ b/src/Aggregate/Value/ClosureValueGenerator.php
@@ -0,0 +1,51 @@
+
+ */
+final class ClosureValueGenerator implements ValueGeneratorInterface
+{
+ /**
+ * @var T|null
+ */
+ private array|object|null $attachment = null;
+
+ public function __construct(
+ /**
+ * @var Closure(ElementInterface):T
+ */
+ private readonly Closure $generator,
+ ) {}
+
+ #[Override]
+ public function attach(mixed $entity): void
+ {
+ $this->attachment = $entity;
+ }
+
+ #[Override]
+ public function generate(ElementInterface $element): object|array
+ {
+ if ($this->attachment !== null) {
+ return $this->attachment;
+ }
+
+ return ($this->generator)($element);
+ }
+
+ #[Override]
+ public function finalize(object|array $value): object|array
+ {
+ /** @var T */
+ return $value;
+ }
+}
diff --git a/src/Aggregate/Value/ConstructorValueGenerator.php b/src/Aggregate/Value/ConstructorValueGenerator.php
new file mode 100644
index 0000000..f47654d
--- /dev/null
+++ b/src/Aggregate/Value/ConstructorValueGenerator.php
@@ -0,0 +1,62 @@
+
+ */
+final class ConstructorValueGenerator implements ValueGeneratorInterface
+{
+ /**
+ * @var T|array|null
+ */
+ private array|object|null $attachment = null;
+
+ public function __construct(
+ /**
+ * @var class-string
+ */
+ private readonly string $class,
+ ) {}
+
+ #[Override]
+ public function attach(mixed $entity): void
+ {
+ if (is_array($entity) || $entity instanceof $this->class) {
+ $this->attachment = $entity;
+ return;
+ }
+
+ throw new InvalidArgumentException(sprintf('Expected array or instance of %s, %s given on %s::attach()', $this->class, get_debug_type($entity), self::class));
+ }
+
+ #[Override]
+ public function generate(ElementInterface $element): object|array
+ {
+ return $this->attachment ?? [];
+ }
+
+ #[Override]
+ public function finalize(object|array $value): object
+ {
+ if (is_array($value)) {
+ $value = new ($this->class)(...$value);
+ }
+
+ assert($value instanceof $this->class);
+
+ return $value;
+ }
+}
diff --git a/src/Aggregate/Value/DefaultConstructorValueGenerator.php b/src/Aggregate/Value/DefaultConstructorValueGenerator.php
new file mode 100644
index 0000000..ebf8f4f
--- /dev/null
+++ b/src/Aggregate/Value/DefaultConstructorValueGenerator.php
@@ -0,0 +1,62 @@
+
+ */
+final class DefaultConstructorValueGenerator implements ValueGeneratorInterface
+{
+ /**
+ * @var T|null
+ */
+ private ?object $attachment = null;
+
+ public function __construct(
+ /**
+ * @var class-string
+ */
+ private readonly string $class,
+ ) {}
+
+ #[Override]
+ public function attach(mixed $entity): void
+ {
+ if (!$entity instanceof $this->class) {
+ throw new InvalidArgumentException(sprintf('Cannot attach a value of type %s, expecting %s', get_debug_type($entity), $this->class));
+ }
+
+ $this->attachment = $entity;
+ }
+
+ #[Override]
+ public function generate(ElementInterface $element): object
+ {
+ if ($this->attachment !== null) {
+ return $this->attachment;
+ }
+
+ return new $this->class;
+ }
+
+ #[Override]
+ public function finalize(object|array $value): object
+ {
+ assert($value instanceof $this->class);
+
+ return $value;
+ }
+}
diff --git a/src/Aggregate/Value/ObjectValueGenerator.php b/src/Aggregate/Value/ObjectValueGenerator.php
new file mode 100644
index 0000000..f1b97cb
--- /dev/null
+++ b/src/Aggregate/Value/ObjectValueGenerator.php
@@ -0,0 +1,62 @@
+
+ */
+final class ObjectValueGenerator implements ValueGeneratorInterface
+{
+ /**
+ * @var T|null
+ */
+ private ?object $attachment = null;
+
+ public function __construct(
+ /**
+ * @var T
+ */
+ private readonly object $value,
+ ) {}
+
+ #[Override]
+ public function attach(mixed $entity): void
+ {
+ if (!$entity instanceof $this->value) {
+ throw new InvalidArgumentException(sprintf('Cannot attach a value of type %s, expecting %s', get_debug_type($entity), get_class($this->value)));
+ }
+
+ $this->attachment = $entity;
+ }
+
+ #[Override]
+ public function generate(ElementInterface $element): object
+ {
+ if ($this->attachment !== null) {
+ return $this->attachment;
+ }
+
+ return clone $this->value;
+ }
+
+ #[Override]
+ public function finalize(object|array $value): object
+ {
+ assert($value instanceof $this->value);
+
+ /** @var T */
+ return $value;
+ }
+}
diff --git a/src/Aggregate/Value/SimpleValueGenerator.php b/src/Aggregate/Value/SimpleValueGenerator.php
new file mode 100644
index 0000000..aa75f76
--- /dev/null
+++ b/src/Aggregate/Value/SimpleValueGenerator.php
@@ -0,0 +1,41 @@
+
+ */
+final class SimpleValueGenerator implements ValueGeneratorInterface
+{
+ public function __construct(
+ /**
+ * @var T
+ */
+ private array|object $value = [],
+ ) {}
+
+ #[Override]
+ public function attach(mixed $entity): void
+ {
+ $this->value = $entity;
+ }
+
+ #[Override]
+ public function generate(ElementInterface $element): object|array
+ {
+ return $this->value;
+ }
+
+ #[Override]
+ public function finalize(object|array $value): object|array
+ {
+ /** @var T */
+ return $value;
+ }
+}
diff --git a/src/Aggregate/Value/ValueGenerator.php b/src/Aggregate/Value/ValueGenerator.php
index 1afedcb..d2afc7c 100644
--- a/src/Aggregate/Value/ValueGenerator.php
+++ b/src/Aggregate/Value/ValueGenerator.php
@@ -4,6 +4,7 @@
use Bdf\Form\ElementInterface;
use Override;
+use ReflectionClass;
use function class_exists;
use function is_callable;
@@ -20,59 +21,75 @@
* (new ValueGenerator(function (FormInterface $form) { return new MyEntity(...); }))->generate($form); // Custom generator
*
*
- * @template T
+ * @template T as array|object
* @implements ValueGeneratorInterface
+ *
+ * @psalm-suppress InvalidDocblock
*/
final class ValueGenerator implements ValueGeneratorInterface
{
/**
- * @var callable():T|T|class-string
- */
- private mixed $value;
-
- /**
- * @var callable():T|T|class-string|null
+ * @var ValueGeneratorInterface
*/
- private mixed $attachment = null;
+ private ValueGeneratorInterface $generator;
/**
* ValueGenerator constructor.
*
- * @param callable():T|T|class-string $value
+ * @param callable(ElementInterface):T|T|class-string $value
*/
public function __construct(mixed $value = [])
{
- /** @psalm-suppress PropertyTypeCoercion */
- $this->value = $value;
+ $this->generator = self::fromValue($value, true);
}
#[Override]
public function attach(mixed $entity): void
{
- /** @psalm-suppress PropertyTypeCoercion */
- $this->attachment = $entity;
+ $this->generator = self::fromValue($entity, false);
+ }
+
+ #[Override]
+ public function generate(ElementInterface $element): array|object
+ {
+ return $this->generator->generate($element);
}
#[Override]
- public function generate(ElementInterface $element): mixed
+ public function finalize(object|array $value): object|array
{
- $value = $this->attachment ?? $this->value;
+ return $this->generator->finalize($value);
+ }
+ /**
+ * @param callable(ElementInterface):U|U|class-string $value
+ * @return ValueGeneratorInterface
+ * @template U as array|object
+ *
+ * @psalm-suppress InvalidReturnStatement
+ * @psalm-suppress InvalidReturnType
+ */
+ private static function fromValue(mixed $value, bool $cloneObjectValue): ValueGeneratorInterface
+ {
if (is_string($value) && class_exists($value)) {
- /** @var T */
- return new $value;
+ $calUseDefaultConstructor = (new ReflectionClass($value)->getConstructor()?->getNumberOfRequiredParameters() ?? 0) === 0;
+
+ return $calUseDefaultConstructor
+ ? new DefaultConstructorValueGenerator($value)
+ : new ConstructorValueGenerator($value)
+ ;
}
if (is_callable($value)) {
- return ($value)($element);
+ /** @psalm-suppress PossiblyInvalidFunctionCall */
+ return new ClosureValueGenerator($value(...));
}
- // Only clone value if it's not attached
- if ($this->attachment === null && is_object($value)) {
- return clone $value;
+ if (is_object($value) && $cloneObjectValue) {
+ return new ObjectValueGenerator($value);
}
- /** @var T */
- return $value;
+ /** @psalm-suppress PossiblyInvalidArgument */
+ return new SimpleValueGenerator($value);
}
}
diff --git a/src/Aggregate/Value/ValueGeneratorInterface.php b/src/Aggregate/Value/ValueGeneratorInterface.php
index f436d2a..bcc9eca 100644
--- a/src/Aggregate/Value/ValueGeneratorInterface.php
+++ b/src/Aggregate/Value/ValueGeneratorInterface.php
@@ -10,7 +10,7 @@
*
* @see ElementInterface::value()
*
- * @template T
+ * @template T as object|array
*/
interface ValueGeneratorInterface
{
@@ -20,7 +20,7 @@ interface ValueGeneratorInterface
*
* If the attached value is an object, generate() should return this object
*
- * @param T|callable():T|class-string $entity
+ * @param T $entity
* @see FormInterface::attach()
*/
public function attach(mixed $entity): void;
@@ -29,10 +29,23 @@ public function attach(mixed $entity): void;
* Generate the value
* This method should be stateless : calling this method multiple times with same argument should return the same value
*
+ * If the value needs finalization, this method may return a temporary value used as builder (for example, an array of constructor arguments),
+ * and the finalization will be done by the form after all generators are called.
+ *
+ * The returned value must be compatible with hydrators to allows filling the properties, and must be mutable
+ *
* @param ElementInterface $element The source element
*
- * @return T
+ * @return object|array
* @see FormInterface::value()
*/
- public function generate(ElementInterface $element): mixed;
+ public function generate(ElementInterface $element): object|array;
+
+ /**
+ * Finalize the value generation
+ *
+ * @param object|array $value The value generated using {@see ValueGeneratorInterface::generate()} and filled by hydrators
+ * @return T
+ */
+ public function finalize(object|array $value): object|array;
}
diff --git a/src/Attribute/Aggregate/ArrayConstraint.php b/src/Attribute/Aggregate/ArrayConstraint.php
index c179498..b4a2d90 100644
--- a/src/Attribute/Aggregate/ArrayConstraint.php
+++ b/src/Attribute/Aggregate/ArrayConstraint.php
@@ -62,13 +62,13 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$builder->arrayConstraint($this->constraint);
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$constraint = ObjectInstantiation::promotedProperties($this->constraint)->render($generator);
$generator->line('$?->arrayConstraint(?);', [$name, $constraint]);
diff --git a/src/Attribute/Aggregate/CallbackArrayConstraint.php b/src/Attribute/Aggregate/CallbackArrayConstraint.php
index 202a6d5..1fb1d9a 100644
--- a/src/Attribute/Aggregate/CallbackArrayConstraint.php
+++ b/src/Attribute/Aggregate/CallbackArrayConstraint.php
@@ -13,6 +13,9 @@
use Override;
use Symfony\Component\Validator\Constraint;
+use function is_object;
+use function sprintf;
+
/**
* Define a custom constraint for an array element, using a validation method
*
@@ -76,21 +79,31 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
- $constraint = new Closure($form->{$this->methodName}(...), $this->message);
+ if (is_object($context)) {
+ $constraint = new Closure($context->{$this->methodName}(...), $this->message);
+ } else {
+ $constraint = new Closure($context::{$this->methodName}(...), $this->message);
+ }
$builder->arrayConstraint($constraint);
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->use(Closure::class, 'ClosureConstraint');
+ if (is_object($context)) {
+ $closure = sprintf('$context->%s(...)', $this->methodName);
+ } else {
+ $closure = sprintf('\%s::%s(...)', $context, $this->methodName);
+ }
+
$parameters = $this->message !== null
- ? new Literal('$form->?(...), ?', [$this->methodName, $this->message])
- : new Literal('$form->?(...)', [$this->methodName])
+ ? new Literal($closure . ', ?', [$this->message])
+ : new Literal($closure)
;
$generator->line('$?->arrayConstraint(new ClosureConstraint(?));', [$name, $parameters]);
diff --git a/src/Attribute/Aggregate/Count.php b/src/Attribute/Aggregate/Count.php
index 6e30f6c..f3d7c8c 100644
--- a/src/Attribute/Aggregate/Count.php
+++ b/src/Attribute/Aggregate/Count.php
@@ -43,7 +43,7 @@
final class Count extends CountConstraint implements ChildBuilderAttributeInterface
{
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$builder->arrayConstraint($this);
}
@@ -55,7 +55,7 @@ public function validatedBy(): string
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$defaultParameters = get_class_vars(CountConstraint::class);
/** @var array{
diff --git a/src/Attribute/Aggregate/ElementType.php b/src/Attribute/Aggregate/ElementType.php
index f77dd69..ebaae8f 100644
--- a/src/Attribute/Aggregate/ElementType.php
+++ b/src/Attribute/Aggregate/ElementType.php
@@ -13,6 +13,9 @@
use Nette\PhpGenerator\Literal;
use Override;
+use function is_object;
+use function is_string;
+
/**
* Attribute for define the array element type
* You can also define a configuration method (not required)
@@ -68,21 +71,25 @@ public function __construct(
}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
- $configurator = $this->configurator !== null ? [$form, $this->configurator] : null;
+ $configurator = match (true) {
+ $this->configurator !== null && is_object($context) => $context->{$this->configurator}(...),
+ $this->configurator !== null && is_string($context) => $context::{$this->configurator}(...),
+ default => null,
+ };
$builder->element($this->elementType, $configurator);
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$elementType = new Literal($generator->useAndSimplifyType($this->elementType));
- if ($this->configurator !== null) {
- $generator->line('$?->element(?::class, [$form, ?]);', [$name, $elementType, $this->configurator]);
- } else {
- $generator->line('$?->element(?::class);', [$name, $elementType]);
- }
+ match (true) {
+ $this->configurator !== null && is_object($context) => $generator->line('$?->element(?::class, $context->?(...));', [$name, $elementType, $this->configurator]),
+ $this->configurator !== null && is_string($context) => $generator->line('$?->element(?::class, ?::?(...));', [$name, $elementType, new Literal($generator->useAndSimplifyType($context)), $this->configurator]),
+ default => $generator->line('$?->element(?::class);', [$name, $elementType]),
+ };
}
}
diff --git a/src/Attribute/Aggregate/Optional.php b/src/Attribute/Aggregate/Optional.php
new file mode 100644
index 0000000..5ed6c29
--- /dev/null
+++ b/src/Attribute/Aggregate/Optional.php
@@ -0,0 +1,21 @@
+
+ */
+#[Attribute(Attribute::TARGET_PROPERTY)]
+readonly class BuilderMethodCall implements ChildBuilderAttributeInterface
+{
+ public function __construct(
+ /**
+ * The method name to call
+ *
+ * @var non-empty-string
+ */
+ public string $method,
+
+ /**
+ * Calling arguments.
+ * If an associative array is given, named arguments will be used to call the method
+ */
+ public array $arguments,
+ ) {}
+
+ #[Override]
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
+ {
+ $builder->{$this->method}(...$this->arguments);
+ }
+
+ #[Override]
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
+ {
+ if (array_is_list($this->arguments)) {
+ $generator->line('$?->?(...?);', [$name, $this->method, $this->arguments]);
+ } else {
+ $generator->line('$?->?(...?:);', [$name, $this->method, $this->arguments]);
+ }
+ }
+}
diff --git a/src/Attribute/Button/ButtonBuilderAttributeInterface.php b/src/Attribute/Button/ButtonBuilderAttributeInterface.php
index 3dab8c1..69e58ee 100644
--- a/src/Attribute/Button/ButtonBuilderAttributeInterface.php
+++ b/src/Attribute/Button/ButtonBuilderAttributeInterface.php
@@ -18,19 +18,19 @@ interface ButtonBuilderAttributeInterface
/**
* Configure the button builder
*
- * @param AttributeForm $form The current form instance
+ * @param object|class-string $context The form instance or the DTO class name
* @param ButtonBuilderInterface $builder Builder to configure
*/
- public function applyOnButtonBuilder(AttributeForm $form, ButtonBuilderInterface $builder): void;
+ public function applyOnButtonBuilder(object|string $context, ButtonBuilderInterface $builder): void;
/**
* Generate the code corresponding to the attribute
* The generated code must perform same action as `applyOnButtonBuilder()`
*
* @param AttributesProcessorGenerator $generator Code generator for the "configureBuilder" method
- * @param AttributeForm $form The current form instance
+ * @param object|class-string $context The form instance or the DTO class name
*
* @return void
*/
- public function generateCodeForButtonBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void;
+ public function generateCodeForButtonBuilder(AttributesProcessorGenerator $generator, object|string $context): void;
}
diff --git a/src/Attribute/Button/Groups.php b/src/Attribute/Button/Groups.php
index 5380811..eff286c 100644
--- a/src/Attribute/Button/Groups.php
+++ b/src/Attribute/Button/Groups.php
@@ -55,13 +55,13 @@ public function __construct(string ...$groups)
}
#[Override]
- public function applyOnButtonBuilder(AttributeForm $form, ButtonBuilderInterface $builder): void
+ public function applyOnButtonBuilder(object|string $context, ButtonBuilderInterface $builder): void
{
$builder->groups($this->groups);
}
#[Override]
- public function generateCodeForButtonBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForButtonBuilder(AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->line(' ->groups(?)', [$this->groups]);
}
diff --git a/src/Attribute/Button/Value.php b/src/Attribute/Button/Value.php
index c18985c..5aa3a26 100644
--- a/src/Attribute/Button/Value.php
+++ b/src/Attribute/Button/Value.php
@@ -44,13 +44,13 @@ public function __construct(
) {}
#[Override]
- public function applyOnButtonBuilder(AttributeForm $form, ButtonBuilderInterface $builder): void
+ public function applyOnButtonBuilder(object|string $context, ButtonBuilderInterface $builder): void
{
$builder->value($this->value);
}
#[Override]
- public function generateCodeForButtonBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForButtonBuilder(AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->line(' ->value(?)', [$this->value]);
}
diff --git a/src/Attribute/Child/CallbackFilter.php b/src/Attribute/Child/CallbackFilter.php
index ebedaf9..6927a4c 100644
--- a/src/Attribute/Child/CallbackFilter.php
+++ b/src/Attribute/Child/CallbackFilter.php
@@ -3,13 +3,15 @@
namespace Bdf\Form\Attribute\Child;
use Attribute;
-use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\ChildBuilderAttributeInterface;
use Bdf\Form\Attribute\Element\CallbackTransformer;
use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator;
use Bdf\Form\Child\ChildBuilderInterface;
+use Nette\PhpGenerator\Literal;
use Override;
+use function is_object;
+
/**
* Add a filter on the child element, by using method
*
@@ -57,14 +59,22 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
- $builder->filter([$form, $this->method]);
+ if (is_object($context)) {
+ $builder->filter($context->{$this->method}(...));
+ } else {
+ $builder->filter($context::{$this->method}(...));
+ }
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
- $generator->line('$?->filter([$form, ?]);', [$name, $this->method]);
+ if (is_object($context)) {
+ $generator->line('$?->filter($context->?(...));', [$name, $this->method]);
+ } else {
+ $generator->line('$?->filter(?::?(...));', [$name, new Literal($generator->useAndSimplifyType($context)), $this->method]);
+ }
}
}
diff --git a/src/Attribute/Child/CallbackModelTransformer.php b/src/Attribute/Child/CallbackModelTransformer.php
index ecaba17..817da51 100644
--- a/src/Attribute/Child/CallbackModelTransformer.php
+++ b/src/Attribute/Child/CallbackModelTransformer.php
@@ -3,21 +3,19 @@
namespace Bdf\Form\Attribute\Child;
use Attribute;
-use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\ChildBuilderAttributeInterface;
use Bdf\Form\Attribute\Element\CallbackTransformer;
use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator;
use Bdf\Form\Attribute\Processor\CodeGenerator\TransformerClassGenerator;
-use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy;
use Bdf\Form\Child\ChildBuilderInterface;
use Bdf\Form\ElementInterface;
use Bdf\Form\Transformer\TransformerInterface;
-use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Literal;
-use Nette\PhpGenerator\Method;
-use Nette\PhpGenerator\PsrPrinter;
use Override;
+use function is_object;
+use function is_string;
+
/**
* Add a model transformer on the child element, by using method
*
@@ -104,20 +102,27 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
if ($this->callback !== null) {
- $builder->modelTransformer([$form, $this->callback]);
+ $callback = is_string($context)
+ ? $context::{$this->callback}(...)
+ : $context->{$this->callback}(...)
+ ;
+
+ $builder->modelTransformer($callback);
return;
}
- $builder->modelTransformer(new class ($form, $this->toInput, $this->toEntity) implements TransformerInterface {
+ $builder->modelTransformer(new readonly class ($context, $this->toInput, $this->toEntity) implements TransformerInterface {
public function __construct(
- private AttributeForm $form,
+ /**
+ * @var object|class-string
+ */
+ private object|string $context,
private ?string $toInput,
private ?string $toEntity,
- ) {
- }
+ ) {}
#[Override]
public function transformToHttp(mixed $value, ElementInterface $input): mixed
@@ -126,7 +131,10 @@ public function transformToHttp(mixed $value, ElementInterface $input): mixed
return $value;
}
- return $this->form->{$this->toInput}($value, $input);
+ return is_string($this->context)
+ ? $this->context::{$this->toInput}($value, $input)
+ : $this->context->{$this->toInput}($value, $input)
+ ;
}
#[Override]
@@ -136,37 +144,44 @@ public function transformFromHttp(mixed $value, ElementInterface $input): mixed
return $value;
}
- return $this->form->{$this->toEntity}($value, $input);
+ return is_string($this->context)
+ ? $this->context::{$this->toEntity}($value, $input)
+ : $this->context->{$this->toEntity}($value, $input)
+ ;
}
});
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
if ($this->callback !== null) {
- $generator->line('$?->modelTransformer([$form, ?]);', [$name, $this->callback]);
+ if (is_object($context)) {
+ $generator->line('$?->modelTransformer($context->?(...));', [$name, $this->callback]);
+ } else {
+ $generator->line('$?->modelTransformer(?::?(...));', [$name, new Literal($generator->useAndSimplifyType($context)), $this->callback]);
+ }
return;
}
$transformer = new TransformerClassGenerator($generator->namespace(), $generator->printer());
- $transformer->withPromotedProperty('form')->setPrivate();
+ $transformer->withPromotedProperty('context')->setPrivate();
- if ($this->toInput !== null) {
- $transformer->toHttp()->setBody('return $this->form->?($value, $input);', [$this->toInput]);
- } else {
- $transformer->toHttp()->setBody('return $value;');
- }
+ match (true) {
+ $this->toInput !== null && is_object($context) => $transformer->toHttp()->setBody('return $this->context->?($value, $input);', [$this->toInput]),
+ $this->toInput !== null && is_string($context) => $transformer->toHttp()->setBody('return $this->context::?($value, $input);', [$this->toInput]),
+ default => $transformer->toHttp()->setBody('return $value;'),
+ };
- if ($this->toEntity !== null) {
- $transformer->fromHttp()->setBody('return $this->form->?($value, $input);', [$this->toEntity]);
- } else {
- $transformer->fromHttp()->setBody('return $value;');
- }
+ match (true) {
+ $this->toEntity !== null && is_object($context) => $transformer->fromHttp()->setBody('return $this->context->?($value, $input);', [$this->toEntity]),
+ $this->toEntity !== null && is_string($context) => $transformer->fromHttp()->setBody('return $this->context::?($value, $input);', [$this->toEntity]),
+ default => $transformer->fromHttp()->setBody('return $value;'),
+ };
$generator->line(
- '$?->modelTransformer(new class ($form) ?);',
+ '$?->modelTransformer(new class ($context) ?);',
[$name, new Literal($transformer->generateClass())]
);
}
diff --git a/src/Attribute/Child/Configure.php b/src/Attribute/Child/Configure.php
index 26baaa1..e66eb14 100644
--- a/src/Attribute/Child/Configure.php
+++ b/src/Attribute/Child/Configure.php
@@ -8,9 +8,12 @@
use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator;
use Bdf\Form\Attribute\Processor\MethodChildBuilderAttributeInterface;
use Bdf\Form\Child\ChildBuilderInterface;
+use Nette\PhpGenerator\Literal;
use Override;
use ReflectionMethod;
+use function is_object;
+
/**
* Define a custom configuration method for an element
*
@@ -65,15 +68,23 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
- $form->{$this->target}($builder);
+ if (is_object($context)) {
+ $context->{$this->target}($builder);
+ } else {
+ $context::{$this->target}($builder);
+ }
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
- $generator->line('$form->?($?);', [$this->target, $name]);
+ if (is_object($context)) {
+ $generator->line('$context->?($?);', [$this->target, $name]);
+ } else {
+ $generator->line('?::?($?);', [new Literal($generator->useAndSimplifyType($context)), $this->target, $name]);
+ }
}
#[Override]
diff --git a/src/Attribute/Child/DefaultValue.php b/src/Attribute/Child/DefaultValue.php
index bd5dcee..61beeec 100644
--- a/src/Attribute/Child/DefaultValue.php
+++ b/src/Attribute/Child/DefaultValue.php
@@ -48,13 +48,13 @@ public function __construct(
}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$builder->default($this->default);
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->line('$?->default(?);', [$name, $this->default]);
}
diff --git a/src/Attribute/Child/Dependencies.php b/src/Attribute/Child/Dependencies.php
index a8d9bda..39f1bd3 100644
--- a/src/Attribute/Child/Dependencies.php
+++ b/src/Attribute/Child/Dependencies.php
@@ -54,13 +54,13 @@ public function __construct(string ...$dependencies)
}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$builder->depends(...$this->dependencies);
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->line('$?->depends(...?);', [$name, $this->dependencies]);
}
diff --git a/src/Attribute/Child/GetSet.php b/src/Attribute/Child/GetSet.php
index 1380a93..bb5bb91 100644
--- a/src/Attribute/Child/GetSet.php
+++ b/src/Attribute/Child/GetSet.php
@@ -59,13 +59,13 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$builder->hydrator(new Setter($this->propertyName))->extractor(new Getter($this->propertyName));
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->use(Setter::class)->use(Getter::class);
$generator->line('$?->hydrator(new Setter(?))->extractor(new Getter(?));', [$name, $this->propertyName, $this->propertyName]);
diff --git a/src/Attribute/Child/HttpField.php b/src/Attribute/Child/HttpField.php
index 4c28ed9..0ea2bcb 100644
--- a/src/Attribute/Child/HttpField.php
+++ b/src/Attribute/Child/HttpField.php
@@ -3,10 +3,8 @@
namespace Bdf\Form\Attribute\Child;
use Attribute;
-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\ChildBuilder;
use Bdf\Form\Child\ChildBuilderInterface;
use Bdf\Form\Child\Http\ArrayOffsetHttpFields;
@@ -56,7 +54,7 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
if (!$builder instanceof ChildBuilder) {
throw new LogicException('The HttpField attribute can only be used on a ChildBuilder instance');
@@ -66,7 +64,7 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->use(ArrayOffsetHttpFields::class);
$generator->line('$?->httpFields(new ArrayOffsetHttpFields(?));', [$name, $this->name]);
diff --git a/src/Attribute/Child/ModelTransformer.php b/src/Attribute/Child/ModelTransformer.php
index 2fcfe4c..8827bd9 100644
--- a/src/Attribute/Child/ModelTransformer.php
+++ b/src/Attribute/Child/ModelTransformer.php
@@ -58,13 +58,13 @@ public function __construct(
}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$builder->modelTransformer(new $this->transformerClass(...$this->constructorArguments));
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$transformer = $generator->useAndSimplifyType($this->transformerClass);
$generator->line('$?->modelTransformer(new ?(...?));', [$name, new Literal($transformer), $this->constructorArguments]);
diff --git a/src/Attribute/ChildBuilderAttributeInterface.php b/src/Attribute/ChildBuilderAttributeInterface.php
index 02aee07..450282f 100644
--- a/src/Attribute/ChildBuilderAttributeInterface.php
+++ b/src/Attribute/ChildBuilderAttributeInterface.php
@@ -18,10 +18,10 @@ interface ChildBuilderAttributeInterface
/**
* Configure the child builder
*
- * @param AttributeForm $form The current form instance
+ * @param object|class-string $context The form instance or the DTO class name
* @param ChildBuilderInterface $builder The builder to configure
*/
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void;
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void;
/**
* Generate the code corresponding to the attribute
@@ -29,9 +29,9 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $
*
* @param non-empty-string $name The variable name without $
* @param AttributesProcessorGenerator $generator Code generator for the "configureBuilder" method
- * @param AttributeForm $form The current form instance
+ * @param object|class-string $context The form instance or the DTO class name
*
* @return void
*/
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void;
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void;
}
diff --git a/src/Attribute/Constraint/CallbackConstraint.php b/src/Attribute/Constraint/CallbackConstraint.php
index 7b99553..39fc2d4 100644
--- a/src/Attribute/Constraint/CallbackConstraint.php
+++ b/src/Attribute/Constraint/CallbackConstraint.php
@@ -14,6 +14,9 @@
use Override;
use Symfony\Component\Validator\Constraint;
+use function is_object;
+use function sprintf;
+
/**
* Define a custom constraint for an element, using a validation method
*
@@ -79,21 +82,31 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
- $constraint = new Closure($form->{$this->methodName}(...), $this->message);
+ if (is_object($context)) {
+ $constraint = new Closure($context->{$this->methodName}(...), $this->message);
+ } else {
+ $constraint = new Closure($context::{$this->methodName}(...), $this->message);
+ }
$builder->satisfy($constraint);
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->use(Closure::class, 'ClosureConstraint');
+ if (is_object($context)) {
+ $closure = sprintf('$context->%s(...)', $this->methodName);
+ } else {
+ $closure = sprintf('%s::%s(...)', $generator->useAndSimplifyType($context), $this->methodName);
+ }
+
$parameters = $this->message !== null
- ? new Literal('$form->?(...), ?', [$this->methodName, $this->message])
- : new Literal('$form->?(...)', [$this->methodName])
+ ? new Literal($closure . ', ?', [$this->message])
+ : new Literal($closure)
;
$generator->line('$?->satisfy(new ClosureConstraint(?));', [$name, $parameters]);
diff --git a/src/Attribute/Constraint/Satisfy.php b/src/Attribute/Constraint/Satisfy.php
index 81f6892..c8cbef1 100644
--- a/src/Attribute/Constraint/Satisfy.php
+++ b/src/Attribute/Constraint/Satisfy.php
@@ -60,13 +60,13 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$builder->satisfy($this->constraint);
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$constraint = ObjectInstantiation::promotedProperties($this->constraint)->render($generator);
$generator->line('$?->satisfy(?);', [$name, $constraint]);
diff --git a/src/Attribute/Element/CallbackTransformer.php b/src/Attribute/Element/CallbackTransformer.php
index 0814916..7a251e5 100644
--- a/src/Attribute/Element/CallbackTransformer.php
+++ b/src/Attribute/Element/CallbackTransformer.php
@@ -21,6 +21,9 @@
use Nette\PhpGenerator\PsrPrinter;
use Override;
+use function is_object;
+use function is_string;
+
/**
* Add a HTTP transformer on the child element, by using method
*
@@ -106,16 +109,23 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
if ($this->callback !== null) {
- $builder->transformer([$form, $this->callback]);
+ $callback = is_string($context)
+ ? $context::{$this->callback}(...)
+ : $context->{$this->callback}(...)
+ ;
+ $builder->transformer($callback);
return;
}
- $builder->transformer(new class ($form, $this->fromHttp, $this->toHttp) implements TransformerInterface {
+ $builder->transformer(new class ($context, $this->fromHttp, $this->toHttp) implements TransformerInterface {
public function __construct(
- private AttributeForm $form,
+ /**
+ * @var object|class-string
+ */
+ private object|string $context,
private ?string $fromHttp,
private ?string $toHttp,
) {
@@ -128,7 +138,10 @@ public function transformToHttp(mixed $value, ElementInterface $input): mixed
return $value;
}
- return $this->form->{$this->toHttp}($value, $input);
+ return is_object($this->context)
+ ? $this->context->{$this->toHttp}($value, $input)
+ : $this->context::{$this->toHttp}($value, $input)
+ ;
}
#[Override]
@@ -138,37 +151,44 @@ public function transformFromHttp(mixed $value, ElementInterface $input): mixed
return $value;
}
- return $this->form->{$this->fromHttp}($value, $input);
+ return is_object($this->context)
+ ? $this->context->{$this->fromHttp}($value, $input)
+ : $this->context::{$this->fromHttp}($value, $input)
+ ;
}
});
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
if ($this->callback !== null) {
- $generator->line('$?->transformer([$form, ?]);', [$name, $this->callback]);
+ if (is_object($context)) {
+ $generator->line('$?->transformer($context->?(...));', [$name, $this->callback]);
+ } else {
+ $generator->line('$?->transformer(?::?(...));', [$name, new Literal($generator->useAndSimplifyType($context)), $this->callback]);
+ }
return;
}
$transformer = new TransformerClassGenerator($generator->namespace(), $generator->printer());
- $transformer->withPromotedProperty('form')->setPrivate();
+ $transformer->withPromotedProperty('context')->setPrivate();
- if ($this->toHttp !== null) {
- $transformer->toHttp()->setBody('return $this->form->?($value, $input);', [$this->toHttp]);
- } else {
- $transformer->toHttp()->setBody('return $value;');
- }
+ match (true) {
+ $this->toHttp !== null && is_object($context) => $transformer->toHttp()->setBody('return $this->context->?($value, $input);', [$this->toHttp]),
+ $this->toHttp !== null && is_string($context) => $transformer->toHttp()->setBody('return $this->context::?($value, $input);', [$this->toHttp]),
+ default => $transformer->toHttp()->setBody('return $value;'),
+ };
- if ($this->fromHttp !== null) {
- $transformer->fromHttp()->setBody('return $this->form->?($value, $input);', [$this->fromHttp]);
- } else {
- $transformer->fromHttp()->setBody('return $value;');
- }
+ match (true) {
+ $this->fromHttp !== null && is_object($context) => $transformer->fromHttp()->setBody('return $this->context->?($value, $input);', [$this->fromHttp]),
+ $this->fromHttp !== null && is_string($context) => $transformer->fromHttp()->setBody('return $this->context::?($value, $input);', [$this->fromHttp]),
+ default => $transformer->fromHttp()->setBody('return $value;'),
+ };
$generator->line(
- '$?->transformer(new class ($form) ?);',
+ '$?->transformer(new class ($context) ?);',
[$name, new Literal($transformer->generateClass())]
);
}
diff --git a/src/Attribute/Element/Choices.php b/src/Attribute/Element/Choices.php
index 333925a..21fbee4 100644
--- a/src/Attribute/Element/Choices.php
+++ b/src/Attribute/Element/Choices.php
@@ -19,6 +19,9 @@
use Nette\PhpGenerator\Literal;
use Override;
+use function is_object;
+use function is_string;
+
/**
* Define available values choice for the element
*
@@ -105,7 +108,7 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$options = $this->options;
@@ -113,16 +116,22 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $
$options['message'] = $this->message;
}
+ $choices = $this->choices;
+
+ if (is_string($choices)) {
+ $choices = is_object($context)
+ ? new LazyChoice($context->{$this->choices}(...))
+ : new LazyChoice($context::{$this->choices}(...))
+ ;
+ }
+
// Q&D fix for psalm because it does not recognize trait as type
/** @var StringElementBuilder $builder */
- $builder->choices(
- is_string($this->choices) ? new LazyChoice($form->{$this->choices}(...)) : $this->choices,
- ...$options
- );
+ $builder->choices($choices, ...$options);
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$options = $this->options;
@@ -132,7 +141,12 @@ public function generateCodeForChildBuilder(string $name, AttributesProcessorGen
if (is_string($this->choices)) {
$generator->use(LazyChoice::class);
- $choices = new Literal('new LazyChoice($form->?(...))', [$this->choices]);
+
+ if (is_object($context)) {
+ $choices = new Literal('new LazyChoice($context->?(...))', [$this->choices]);
+ } else {
+ $choices = new Literal('new LazyChoice(?::?(...))', [new Literal($generator->useAndSimplifyType($context)), $this->choices]);
+ }
} else {
$choices = $this->choices;
}
diff --git a/src/Attribute/Element/Date/AfterField.php b/src/Attribute/Element/Date/AfterField.php
index 8c8e5b5..9fe1e78 100644
--- a/src/Attribute/Element/Date/AfterField.php
+++ b/src/Attribute/Element/Date/AfterField.php
@@ -64,13 +64,13 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$builder->afterField($this->field, $this->message, $this->orEqual);
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->line('$?->afterField(?, ?, ?);', [$name, $this->field, $this->message, $this->orEqual]);
}
diff --git a/src/Attribute/Element/Date/BeforeField.php b/src/Attribute/Element/Date/BeforeField.php
index 74f4911..c234cf0 100644
--- a/src/Attribute/Element/Date/BeforeField.php
+++ b/src/Attribute/Element/Date/BeforeField.php
@@ -64,13 +64,13 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$builder->beforeField($this->field, $this->message, $this->orEqual);
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->line('$?->beforeField(?, ?, ?);', [$name, $this->field, $this->message, $this->orEqual]);
}
diff --git a/src/Attribute/Element/Date/DateFormat.php b/src/Attribute/Element/Date/DateFormat.php
index fb7713d..de6fce1 100644
--- a/src/Attribute/Element/Date/DateFormat.php
+++ b/src/Attribute/Element/Date/DateFormat.php
@@ -50,13 +50,13 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$builder->format($this->format);
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->line('$?->format(?);', [$name, $this->format]);
}
diff --git a/src/Attribute/Element/Date/DateTimeClass.php b/src/Attribute/Element/Date/DateTimeClass.php
index 774884c..0893cd8 100644
--- a/src/Attribute/Element/Date/DateTimeClass.php
+++ b/src/Attribute/Element/Date/DateTimeClass.php
@@ -49,13 +49,13 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$builder->className($this->className);
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->line('$?->className(?::class);', [$name, new Literal($generator->useAndSimplifyType($this->className))]);
}
diff --git a/src/Attribute/Element/Date/ImmutableDateTime.php b/src/Attribute/Element/Date/ImmutableDateTime.php
index 0d781eb..81f17d8 100644
--- a/src/Attribute/Element/Date/ImmutableDateTime.php
+++ b/src/Attribute/Element/Date/ImmutableDateTime.php
@@ -39,13 +39,13 @@
final readonly class ImmutableDateTime implements ChildBuilderAttributeInterface
{
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$builder->immutable();
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->line('$?->immutable();', [$name]);
}
diff --git a/src/Attribute/Element/Date/Timezone.php b/src/Attribute/Element/Date/Timezone.php
index 58ac15f..c33fc2c 100644
--- a/src/Attribute/Element/Date/Timezone.php
+++ b/src/Attribute/Element/Date/Timezone.php
@@ -48,13 +48,13 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$builder->timezone($this->timezone);
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->line('$?->timezone(?);', [$name, $this->timezone]);
}
diff --git a/src/Attribute/Element/IgnoreTransformerException.php b/src/Attribute/Element/IgnoreTransformerException.php
index 1e825ee..98a4530 100644
--- a/src/Attribute/Element/IgnoreTransformerException.php
+++ b/src/Attribute/Element/IgnoreTransformerException.php
@@ -53,13 +53,13 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$builder->ignoreTransformerException($this->ignore);
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->line('$?->ignoreTransformerException(?);', [$name, $this->ignore]);
}
diff --git a/src/Attribute/Element/Raw.php b/src/Attribute/Element/Raw.php
index 3683bb9..21e60b5 100644
--- a/src/Attribute/Element/Raw.php
+++ b/src/Attribute/Element/Raw.php
@@ -48,13 +48,13 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$builder->raw($this->flag);
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->line('$?->raw(?);', [$name, $this->flag]);
}
diff --git a/src/Attribute/Element/Required.php b/src/Attribute/Element/Required.php
index 3d4668c..c29a426 100644
--- a/src/Attribute/Element/Required.php
+++ b/src/Attribute/Element/Required.php
@@ -49,13 +49,13 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
$builder->required($this->message);
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->line('$?->required(?);', [$name, $this->message]);
}
diff --git a/src/Attribute/Element/StructClass.php b/src/Attribute/Element/StructClass.php
new file mode 100644
index 0000000..c46b880
--- /dev/null
+++ b/src/Attribute/Element/StructClass.php
@@ -0,0 +1,27 @@
+transformerClass(...$this->constructorArguments);
@@ -90,7 +90,7 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$transformer = $generator->useAndSimplifyType($this->transformerClass);
$code = $this->array ? '$?->arrayTransformer(new ?(...?));' : '$?->transformer(new ?(...?));';
diff --git a/src/Attribute/Element/TransformerError.php b/src/Attribute/Element/TransformerError.php
index 3051690..7ca543d 100644
--- a/src/Attribute/Element/TransformerError.php
+++ b/src/Attribute/Element/TransformerError.php
@@ -10,8 +10,11 @@
use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy;
use Bdf\Form\Child\ChildBuilderInterface;
use Bdf\Form\Transformer\TransformerInterface;
+use Nette\PhpGenerator\Literal;
use Override;
+use function is_object;
+
/**
* Fine grain configure error triggered by transformers
*
@@ -73,7 +76,7 @@ public function __construct(
) {}
#[Override]
- public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
if ($this->message !== null) {
$builder->transformerErrorMessage($this->message);
@@ -84,12 +87,16 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $
}
if ($this->validationCallback !== null) {
- $builder->transformerExceptionValidation([$form, $this->validationCallback]);
+ if (is_object($context)) {
+ $builder->transformerExceptionValidation($context->{$this->validationCallback}(...));
+ } else {
+ $builder->transformerExceptionValidation($context::{$this->validationCallback}(...));
+ }
}
}
#[Override]
- public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->line('$?', [$name]);
@@ -102,7 +109,11 @@ public function generateCodeForChildBuilder(string $name, AttributesProcessorGen
}
if ($this->validationCallback !== null) {
- $generator->line(' ->transformerExceptionValidation([$form, ?])', [$this->validationCallback]);
+ if (is_object($context)) {
+ $generator->line(' ->transformerExceptionValidation($context->?(...))', [$this->validationCallback]);
+ } else {
+ $generator->line(' ->transformerExceptionValidation(?::?(...))', [new Literal($generator->useAndSimplifyType($context)), $this->validationCallback]);
+ }
}
$generator->line(';');
diff --git a/src/Attribute/Form/CallbackGenerator.php b/src/Attribute/Form/CallbackGenerator.php
index 386b2b9..5ed15f8 100644
--- a/src/Attribute/Form/CallbackGenerator.php
+++ b/src/Attribute/Form/CallbackGenerator.php
@@ -8,9 +8,13 @@
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator;
use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy;
+use Nette\PhpGenerator\Literal;
use Nette\PhpGenerator\Method;
use Override;
+use function is_object;
+use function is_string;
+
/**
* Define the value generator of the form, using a callback method
*
@@ -54,14 +58,22 @@ public function __construct(
) {}
#[Override]
- public function applyOnFormBuilder(AttributeForm $form, FormBuilderInterface $builder): void
+ public function applyOnFormBuilder(object|string $context, FormBuilderInterface $builder): void
{
- $builder->generates([$form, $this->callback]);
+ if (is_object($context)) {
+ $builder->generates($context->{$this->callback}(...));
+ } else {
+ $builder->generates($context::{$this->callback}(...));
+ }
}
#[Override]
- public function generateCodeForFormBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForFormBuilder(AttributesProcessorGenerator $generator, object|string $context): void
{
- $generator->line('$builder->generates([$this, ?]);', [$this->callback]);
+ if (is_object($context)) {
+ $generator->line('$builder->generates([$this, ?]);', [$this->callback]);
+ } else {
+ $generator->line('$builder->generates(?::?(...));', [new Literal($generator->useAndSimplifyType($context)), $this->callback]);
+ }
}
}
diff --git a/src/Attribute/Form/Csrf.php b/src/Attribute/Form/Csrf.php
index 5e5c25c..33f512a 100644
--- a/src/Attribute/Form/Csrf.php
+++ b/src/Attribute/Form/Csrf.php
@@ -78,7 +78,7 @@ public function __construct(
) {}
#[Override]
- public function applyOnFormBuilder(AttributeForm $form, FormBuilderInterface $builder): void
+ public function applyOnFormBuilder(object|string $context, FormBuilderInterface $builder): void
{
$csrf = $builder->csrf($this->name);
@@ -96,7 +96,7 @@ public function applyOnFormBuilder(AttributeForm $form, FormBuilderInterface $bu
}
#[Override]
- public function generateCodeForFormBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForFormBuilder(AttributesProcessorGenerator $generator, object|string $context): void
{
$parameters = [$this->name];
$line = '$builder->csrf(?)';
diff --git a/src/Attribute/Form/FormBuilderAttributeInterface.php b/src/Attribute/Form/FormBuilderAttributeInterface.php
index 240dba8..d973706 100644
--- a/src/Attribute/Form/FormBuilderAttributeInterface.php
+++ b/src/Attribute/Form/FormBuilderAttributeInterface.php
@@ -19,19 +19,19 @@ interface FormBuilderAttributeInterface
/**
* Configure the given builder
*
- * @param AttributeForm $form The form to configure
+ * @param object|class-string $context The form instance or the DTO class name
* @param FormBuilderInterface $builder The form builder
*/
- public function applyOnFormBuilder(AttributeForm $form, FormBuilderInterface $builder): void;
+ public function applyOnFormBuilder(object|string $context, FormBuilderInterface $builder): void;
/**
* Generate the code corresponding to the attribute
* The generated code must perform same action as `applyOnFormBuilder()`
*
* @param AttributesProcessorGenerator $generator Code generator for the "configureBuilder" method
- * @param AttributeForm $form The current form instance
+ * @param object|class-string $context The form instance or the DTO class name
*
* @return void
*/
- public function generateCodeForFormBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void;
+ public function generateCodeForFormBuilder(AttributesProcessorGenerator $generator, object|string $context): void;
}
diff --git a/src/Attribute/Form/Generates.php b/src/Attribute/Form/Generates.php
index 3c6f131..24a453f 100644
--- a/src/Attribute/Form/Generates.php
+++ b/src/Attribute/Form/Generates.php
@@ -50,13 +50,13 @@ public function __construct(
) {}
#[Override]
- public function applyOnFormBuilder(AttributeForm $form, FormBuilderInterface $builder): void
+ public function applyOnFormBuilder(object|string $context, FormBuilderInterface $builder): void
{
$builder->generates($this->className);
}
#[Override]
- public function generateCodeForFormBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void
+ public function generateCodeForFormBuilder(AttributesProcessorGenerator $generator, object|string $context): void
{
$generator->line(
'$builder->generates(?::class);',
diff --git a/src/Attribute/Processor/AttributesProcessorInterface.php b/src/Attribute/Processor/AttributesProcessorInterface.php
index 9de13b1..158355f 100644
--- a/src/Attribute/Processor/AttributesProcessorInterface.php
+++ b/src/Attribute/Processor/AttributesProcessorInterface.php
@@ -15,12 +15,12 @@ interface AttributesProcessorInterface
/**
* Configure the form builder
*
- * @param AttributeForm $form The form to analyze
+ * @param object|class-string $context The form or DTO class name to analyze
* @param FormBuilderInterface $builder Builder to configure
*
* @return PostConfigureInterface|null The post configuration action to perform
*
* @see AttributeForm::configure() Should be called in this method
*/
- public function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface;
+ public function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface;
}
diff --git a/src/Attribute/Processor/CompileAttributesProcessor.php b/src/Attribute/Processor/CompileAttributesProcessor.php
index addb6f3..fa3ddc1 100644
--- a/src/Attribute/Processor/CompileAttributesProcessor.php
+++ b/src/Attribute/Processor/CompileAttributesProcessor.php
@@ -4,10 +4,12 @@
use Bdf\Form\Aggregate\FormBuilder;
use Bdf\Form\Aggregate\FormBuilderInterface;
-use Bdf\Form\Attribute\AttributeForm;
+use Closure;
use LogicException;
use Override;
+use function is_string;
+
/**
* Processor for compile attributes to native PHP code for build the form
*
@@ -29,7 +31,7 @@ public function __construct(
*
* The class name must be contained into a namespace
*
- * @var callable(AttributeForm):non-empty-string
+ * @var callable(class-string):non-empty-string
*/
private mixed $classNameResolver,
/**
@@ -38,6 +40,13 @@ public function __construct(
* @var callable(class-string):non-empty-string
*/
private mixed $fileNameResolver,
+
+ /**
+ * Factory for the inner processor used to generate the code, if the generated class do not exist yet
+ *
+ * @var (Closure(ReflectionStrategyInterface):AttributesProcessorInterface)|null
+ */
+ private ?Closure $innerProcessorFactory = null
) {}
/**
@@ -46,20 +55,22 @@ public function __construct(
* @psalm-suppress PossiblyUnusedReturnValue
*/
#[Override]
- public function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): PostConfigureInterface
+ public function configureBuilder(string|object $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
- /** @var class-string $className */
- $className = ($this->classNameResolver)($form);
+ $formClassName = is_string($context) ? $context : $context::class;
+
+ /** @var class-string $className */
+ $className = ($this->classNameResolver)($formClassName);
if (!class_exists($className)) {
/** @psalm-suppress ArgumentTypeCoercion */
- $this->loadProcessor($className, $form, $builder);
+ $this->loadProcessor($className, $context, $builder);
}
$generated = new $className();
- $generated->configureBuilder($form, $builder);
+ $generated->configureBuilder($context, $builder);
- return $generated;
+ return $generated instanceof PostConfigureInterface ? $generated : null;
}
/**
@@ -67,34 +78,34 @@ public function configureBuilder(AttributeForm $form, FormBuilderInterface $buil
* Unlike `configureBuilder()` process, the class will be regenerated if already exists,
* and the class will not be included
*
- * @param AttributeForm $form Form to generate
+ * @param class-string|object $context Form to generate
*
* @return void
*/
- public function generate(AttributeForm $form): void
+ public function generate(string|object $context): void
{
/** @var class-string $className */
- $className = ($this->classNameResolver)($form);
+ $className = ($this->classNameResolver)(is_string($context) ? $context : $context::class);
$fileName = ($this->fileNameResolver)($className);
- $this->generateProcessor($fileName, $className, $form, new FormBuilder());
+ $this->generateProcessor($fileName, $className, $context, new FormBuilder());
}
/**
* Try to load the processor from its file
*
- * @param class-string $className Generated processor class name
- * @param AttributeForm $form Form to build
+ * @param class-string $className Generated processor class name
+ * @param class-string|object $context Form to build
* @param FormBuilderInterface $builder Builder to configure
*
* @return void
*/
- private function loadProcessor(string $className, AttributeForm $form, FormBuilderInterface $builder): void
+ private function loadProcessor(string $className, string|object $context, FormBuilderInterface $builder): void
{
$fileName = ($this->fileNameResolver)($className);
if (!file_exists($fileName)) {
- $this->generateProcessor($fileName, $className, $form, $builder);
+ $this->generateProcessor($fileName, $className, $context, $builder);
}
require_once $fileName;
@@ -108,18 +119,21 @@ private function loadProcessor(string $className, AttributeForm $form, FormBuild
* Generate the processor class and save it into the given file
*
* @param string $fileName Target file
- * @param class-string $className Generated processor class name
- * @param AttributeForm $form Form to build
+ * @param class-string $className Generated processor class name
+ * @param class-string|object $context Form to build
* @param FormBuilderInterface $builder Builder to configure
*
* @return void
*/
- private function generateProcessor(string $fileName, string $className, AttributeForm $form, FormBuilderInterface $builder): void
+ private function generateProcessor(string $fileName, string $className, string|object $context, FormBuilderInterface $builder): void
{
$generator = new GenerateConfiguratorStrategy($className);
- $processor = new ReflectionProcessor($generator);
+ $processor = $this->innerProcessorFactory
+ ? ($this->innerProcessorFactory)($generator)
+ : new ReflectionProcessor($generator)
+ ;
- $processor->configureBuilder($form, $builder);
+ $processor->configureBuilder($context, $builder);
$code = $generator->code();
diff --git a/src/Attribute/Processor/ConfigureFormBuilderStrategy.php b/src/Attribute/Processor/ConfigureFormBuilderStrategy.php
index 7a40b8b..79fab1d 100644
--- a/src/Attribute/Processor/ConfigureFormBuilderStrategy.php
+++ b/src/Attribute/Processor/ConfigureFormBuilderStrategy.php
@@ -3,100 +3,54 @@
namespace Bdf\Form\Attribute\Processor;
use Bdf\Form\Aggregate\FormBuilderInterface;
-use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Aggregate\FormInterface;
use Bdf\Form\Attribute\Button\ButtonBuilderAttributeInterface;
-use Bdf\Form\Attribute\ChildBuilderAttributeInterface;
-use Bdf\Form\Attribute\Form\FormBuilderAttributeInterface;
-use Bdf\Form\Attribute\Processor\Element\ConstraintAttributeProcessor;
-use Bdf\Form\Attribute\Processor\Element\ElementAttributeProcessorInterface;
-use Bdf\Form\Attribute\Processor\Element\ExtractorAttributeProcessor;
-use Bdf\Form\Attribute\Processor\Element\FilterAttributeProcessor;
-use Bdf\Form\Attribute\Processor\Element\HydratorAttributeProcessor;
-use Bdf\Form\Attribute\Processor\Element\TransformerAttributeProcessor;
use Override;
use ReflectionAttribute;
-use ReflectionClass;
use ReflectionProperty;
+use function is_object;
+
/**
* Strategy for directly configure the form builder using attributes
*/
final class ConfigureFormBuilderStrategy implements ReflectionStrategyInterface
{
- /**
- * @var list
- */
- private array $elementProcessors = [];
-
- public function __construct()
- {
- $this->registerElementAttributeProcessor(new ConstraintAttributeProcessor());
- $this->registerElementAttributeProcessor(new FilterAttributeProcessor());
- $this->registerElementAttributeProcessor(new TransformerAttributeProcessor());
- $this->registerElementAttributeProcessor(new HydratorAttributeProcessor());
- $this->registerElementAttributeProcessor(new ExtractorAttributeProcessor());
- }
-
#[Override]
- public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void
+ public function onFormClass(ProcessorMetadata $metadata, object|string $context, FormBuilderInterface $builder): void
{
- foreach ($formClass->getAttributes(FormBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
- $attribute->newInstance()->applyOnFormBuilder($form, $builder);
+ foreach ($metadata->formAttributes as $attribute) {
+ $attribute->applyOnFormBuilder($context, $builder);
}
}
#[Override]
- public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void
+ public function onButtonProperty(ReflectionProperty $property, string $name, object|string $context, FormBuilderInterface $builder, ProcessorMetadata $metadata): void
{
$submitBuilder = $builder->submit($name);
foreach ($property->getAttributes(ButtonBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
- $attribute->newInstance()->applyOnButtonBuilder($form, $submitBuilder);
+ $attribute->newInstance()->applyOnButtonBuilder($context, $submitBuilder);
}
}
#[Override]
- public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void
+ public function onElementProperty(ElementPropertyMetadata $metadata, object|string $context, FormBuilderInterface $builder): void
{
- $elementBuilder = $builder->add($name, $elementType);
-
- foreach ($property->getAttributes() as $attribute) {
- $attributeInstance = $attribute->newInstance();
-
- if ($attributeInstance instanceof ChildBuilderAttributeInterface) {
- $attributeInstance->applyOnChildBuilder($form, $elementBuilder);
- continue;
- }
+ $elementBuilder = $builder->add($metadata->name, $metadata->elementType);
- foreach ($this->elementProcessors as $configurator) {
- if ($attributeInstance instanceof ($configurator->type())) {
- $configurator->process($elementBuilder, $attributeInstance);
- }
- }
- }
-
- foreach ($metadata->registeredChildAttributes($name) as $attribute) {
- $attribute->applyOnChildBuilder($form, $elementBuilder);
+ foreach ($metadata->attributes as $attribute) {
+ $attribute->applyOnChildBuilder($context, $elementBuilder);
}
}
#[Override]
- public function onPostConfigure(ProcessorMetadata $metadata, AttributeForm $form): ?PostConfigureInterface
+ public function onPostConfigure(ProcessorMetadata $metadata, object|string $context): ?PostConfigureInterface
{
- return new PostConfigureReflectionSetProperties($metadata->elementProperties(), $metadata->buttonProperties());
- }
+ if (!is_object($context)) {
+ return null;
+ }
- /**
- * Register a new processor for element attributes
- *
- * @param ElementAttributeProcessorInterface $processor
- *
- * @return void
- *
- * @template T as object
- */
- private function registerElementAttributeProcessor(ElementAttributeProcessorInterface $processor): void
- {
- $this->elementProcessors[] = $processor;
+ return new PostConfigureReflectionSetProperties($metadata->elementProperties(), $metadata->buttonProperties());
}
}
diff --git a/src/Attribute/Processor/Element/ElementAttributeChildBuilderAdapter.php b/src/Attribute/Processor/Element/ElementAttributeChildBuilderAdapter.php
new file mode 100644
index 0000000..7a16754
--- /dev/null
+++ b/src/Attribute/Processor/Element/ElementAttributeChildBuilderAdapter.php
@@ -0,0 +1,57 @@
+
+ */
+final readonly class ElementAttributeChildBuilderAdapter implements ChildBuilderAttributeInterface
+{
+ public function __construct(
+ /**
+ * @var ElementAttributeProcessorInterface
+ */
+ private ElementAttributeProcessorInterface $processor,
+
+ /**
+ * @var ReflectionAttribute
+ */
+ private ReflectionAttribute $attribute,
+ ) {}
+
+ #[Override]
+ public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
+ {
+ $this->processor->process($builder, $this->attribute->newInstance());
+ }
+
+ #[Override]
+ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
+ {
+ $this->processor->generateCode($name, $generator, $this->attribute);
+ }
+
+ /**
+ * Check if the current attribute is an instance of the given type
+ *
+ * @param class-string $type
+ * @return bool
+ */
+ public function is(string $type): bool
+ {
+ return
+ $this->attribute->getName() === $type
+ || is_subclass_of($this->attribute->getName(), $type)
+ ;
+ }
+}
diff --git a/src/Attribute/Processor/ElementPropertyMetadata.php b/src/Attribute/Processor/ElementPropertyMetadata.php
new file mode 100644
index 0000000..5f196b5
--- /dev/null
+++ b/src/Attribute/Processor/ElementPropertyMetadata.php
@@ -0,0 +1,65 @@
+
+ */
+ public readonly string $elementType,
+
+ /**
+ * Attributes attached to this property
+ *
+ * @var list
+ */
+ public private(set) array $attributes,
+ ) {}
+
+ public function addAttribute(ChildBuilderAttributeInterface $attribute): void
+ {
+ $this->attributes[] = $attribute;
+ }
+
+ /**
+ * Check if the property metadata has the given attribute
+ *
+ * @param class-string $class
+ */
+ public function hasAttribute(string $class): bool
+ {
+ foreach ($this->attributes as $attribute) {
+ if ($attribute instanceof $class) {
+ return true;
+ }
+
+ if ($attribute instanceof ElementAttributeChildBuilderAdapter && $attribute->is($class)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/Attribute/Processor/GenerateConfiguratorStrategy.php b/src/Attribute/Processor/GenerateConfiguratorStrategy.php
index 283cdf8..5cf0390 100644
--- a/src/Attribute/Processor/GenerateConfiguratorStrategy.php
+++ b/src/Attribute/Processor/GenerateConfiguratorStrategy.php
@@ -3,24 +3,16 @@
namespace Bdf\Form\Attribute\Processor;
use Bdf\Form\Aggregate\FormBuilderInterface;
-use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Button\ButtonBuilderAttributeInterface;
-use Bdf\Form\Attribute\ChildBuilderAttributeInterface;
-use Bdf\Form\Attribute\Form\FormBuilderAttributeInterface;
use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator;
-use Bdf\Form\Attribute\Processor\Element\ConstraintAttributeProcessor;
-use Bdf\Form\Attribute\Processor\Element\ElementAttributeProcessorInterface;
-use Bdf\Form\Attribute\Processor\Element\ExtractorAttributeProcessor;
-use Bdf\Form\Attribute\Processor\Element\FilterAttributeProcessor;
-use Bdf\Form\Attribute\Processor\Element\HydratorAttributeProcessor;
-use Bdf\Form\Attribute\Processor\Element\TransformerAttributeProcessor;
use Nette\PhpGenerator\Closure;
use Nette\PhpGenerator\Literal;
use Override;
use ReflectionAttribute;
-use ReflectionClass;
use ReflectionProperty;
+use function is_object;
+
/**
* Strategy for generate the processor class code
*/
@@ -28,11 +20,6 @@ final class GenerateConfiguratorStrategy implements ReflectionStrategyInterface
{
private AttributesProcessorGenerator $generator;
- /**
- * @var list
- */
- private array $elementProcessors = [];
-
/**
* @param non-empty-string $className The class name to generate. Must have a namespace
* @throws \InvalidArgumentException If a namespace is not provided, or if the class name is not valid
@@ -40,21 +27,15 @@ final class GenerateConfiguratorStrategy implements ReflectionStrategyInterface
public function __construct(string $className)
{
$this->generator = new AttributesProcessorGenerator($className);
-
- $this->registerElementAttributeProcessor(new ConstraintAttributeProcessor());
- $this->registerElementAttributeProcessor(new FilterAttributeProcessor());
- $this->registerElementAttributeProcessor(new TransformerAttributeProcessor());
- $this->registerElementAttributeProcessor(new HydratorAttributeProcessor());
- $this->registerElementAttributeProcessor(new ExtractorAttributeProcessor());
}
#[Override]
- public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void
+ public function onFormClass(ProcessorMetadata $metadata, object|string $context, FormBuilderInterface $builder): void
{
$empty = true;
- foreach ($formClass->getAttributes(FormBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
- $attribute->newInstance()->generateCodeForFormBuilder($this->generator, $form);
+ foreach ($metadata->formAttributes as $attribute) {
+ $attribute->generateCodeForFormBuilder($this->generator, $context);
$empty = false;
}
@@ -64,48 +45,39 @@ public function onFormClass(ReflectionClass $formClass, AttributeForm $form, For
}
#[Override]
- public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void
+ public function onButtonProperty(ReflectionProperty $property, string $name, object|string $context, FormBuilderInterface $builder, ProcessorMetadata $metadata): void
{
$this->generator->line('$builder->submit(?)', [$name]);
foreach ($property->getAttributes(ButtonBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
- $attribute->newInstance()->generateCodeForButtonBuilder($this->generator, $form);
+ $attribute->newInstance()->generateCodeForButtonBuilder($this->generator, $context);
}
$this->generator->line(";\n");
}
#[Override]
- public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void
+ public function onElementProperty(ElementPropertyMetadata $metadata, object|string $context, FormBuilderInterface $builder): void
{
- $elementType = $this->generator->useAndSimplifyType($elementType);
+ $name = $metadata->name;
+ $elementType = $this->generator->useAndSimplifyType($metadata->elementType);
$this->generator->line('$? = $builder->add(?, ?::class);', [$name, $name, new Literal($elementType)]);
- foreach ($property->getAttributes() as $attribute) {
- if (is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class)) {
- /** @var ChildBuilderAttributeInterface $attributeInstance */
- $attributeInstance = $attribute->newInstance();
- $attributeInstance->generateCodeForChildBuilder($name, $this->generator, $form);
- continue;
- }
-
- foreach ($this->elementProcessors as $configurator) {
- if (is_subclass_of($attribute->getName(), $configurator->type())) {
- $configurator->generateCode($name, $this->generator, $attribute);
- }
- }
- }
-
- foreach ($metadata->registeredChildAttributes($name) as $attribute) {
- $attribute->generateCodeForChildBuilder($name, $this->generator, $form);
+ foreach ($metadata->attributes as $attribute) {
+ $attribute->generateCodeForChildBuilder($name, $this->generator, $context);
}
$this->generator->line(); // Add empty line
}
#[Override]
- public function onPostConfigure(ProcessorMetadata $metadata, AttributeForm $form): ?PostConfigureInterface
+ public function onPostConfigure(ProcessorMetadata $metadata, object|string $context): ?PostConfigureInterface
{
+ if (!is_object($context)) {
+ $this->generator->line('return null;');
+ return null;
+ }
+
$this->generator->line('return $this;');
$method = $this->generator
@@ -122,7 +94,9 @@ public function onPostConfigure(ProcessorMetadata $metadata, AttributeForm $form
$scopedProperties = [];
- foreach ($elementProperties as $name => $property) {
+ foreach ($elementProperties as $name => $propertyMetadata) {
+ $property = $propertyMetadata->property;
+
if ($property->isPublic()) {
$method->addBody('$form->? = $inner[?]->element();', [$name, $name]);
} else {
@@ -172,18 +146,4 @@ public function code(): string
{
return $this->generator->print();
}
-
- /**
- * Register a new processor for element attributes
- *
- * @param ElementAttributeProcessorInterface $processor
- *
- * @return void
- *
- * @template T as object
- */
- private function registerElementAttributeProcessor(ElementAttributeProcessorInterface $processor): void
- {
- $this->elementProcessors[] = $processor;
- }
}
diff --git a/src/Attribute/Processor/PostConfigureReflectionSetProperties.php b/src/Attribute/Processor/PostConfigureReflectionSetProperties.php
index 876f03b..60110b4 100644
--- a/src/Attribute/Processor/PostConfigureReflectionSetProperties.php
+++ b/src/Attribute/Processor/PostConfigureReflectionSetProperties.php
@@ -17,7 +17,7 @@ public function __construct(
* Properties which store form elements
* The key is the element name, and value is the reflection property
*
- * @var array
+ * @var array
*/
private array $elementProperties,
/**
@@ -32,8 +32,8 @@ public function __construct(
#[Override]
public function postConfigure(AttributeForm $form, FormInterface $inner): void
{
- foreach ($this->elementProperties as $name => $reflection) {
- $reflection->setValue($form, $inner[$name]->element());
+ foreach ($this->elementProperties as $name => $metadata) {
+ $metadata->property->setValue($form, $inner[$name]->element());
}
foreach ($this->buttonProperties as $name => $reflection) {
diff --git a/src/Attribute/Processor/ProcessorMetadata.php b/src/Attribute/Processor/ProcessorMetadata.php
index 4ccf720..580fbad 100644
--- a/src/Attribute/Processor/ProcessorMetadata.php
+++ b/src/Attribute/Processor/ProcessorMetadata.php
@@ -3,8 +3,11 @@
namespace Bdf\Form\Attribute\Processor;
use Bdf\Form\Attribute\ChildBuilderAttributeInterface;
+use Bdf\Form\Attribute\Form\FormBuilderAttributeInterface;
use ReflectionProperty;
+use function array_push;
+
/**
* Store metadata about the form that is currently processed
*
@@ -18,14 +21,33 @@ final class ProcessorMetadata
private array $buttonProperties = [];
/**
- * @var array
+ * @var array
*/
private array $elementProperties = [];
/**
- * @var array>
+ * Lis of attributes declared on the form class / DTO class
+ *
+ * @var list
*/
- private array $childAttributes = [];
+ public private(set) array $formAttributes = [];
+
+ /**
+ * Check if the form has the given attribute on it
+ *
+ * @param class-string $name
+ * @return bool
+ */
+ public function hasFormAttribute(string $name): bool
+ {
+ foreach ($this->formAttributes as $formAttribute) {
+ if ($formAttribute instanceof $name) {
+ return true;
+ }
+ }
+
+ return false;
+ }
/**
* @param non-empty-string $name
@@ -37,19 +59,24 @@ public function addButtonProperty(string $name, ReflectionProperty $property): v
$this->buttonProperties[$name] = $property;
}
+ public function addElementProperty(ElementPropertyMetadata $metadata): void
+ {
+ $this->elementProperties[$metadata->name] = $metadata;
+ }
+
/**
- * @param non-empty-string $name
- * @param ReflectionProperty $property
+ * @param non-empty-string $elementName
+ * @param ChildBuilderAttributeInterface $attribute
* @return void
*/
- public function addElementProperty(string $name, ReflectionProperty $property): void
+ public function addChildAttribute(string $elementName, ChildBuilderAttributeInterface $attribute): void
{
- $this->elementProperties[$name] = $property;
+ $this->elementProperties[$elementName]->addAttribute($attribute);
}
- public function addChildAttribute(string $elementName, ChildBuilderAttributeInterface $attribute): void
+ public function addFormAttribute(FormBuilderAttributeInterface $attribute): void
{
- $this->childAttributes[$elementName][] = $attribute;
+ $this->formAttributes[] = $attribute;
}
/**
@@ -61,7 +88,7 @@ public function buttonProperties(): array
}
/**
- * @return array
+ * @return array
*/
public function elementProperties(): array
{
@@ -78,16 +105,4 @@ 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/Attribute/Processor/ReflectionProcessor.php b/src/Attribute/Processor/ReflectionProcessor.php
index b0f9311..ddf9349 100644
--- a/src/Attribute/Processor/ReflectionProcessor.php
+++ b/src/Attribute/Processor/ReflectionProcessor.php
@@ -4,6 +4,15 @@
use Bdf\Form\Aggregate\FormBuilderInterface;
use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\ChildBuilderAttributeInterface;
+use Bdf\Form\Attribute\Form\FormBuilderAttributeInterface;
+use Bdf\Form\Attribute\Processor\Element\ConstraintAttributeProcessor;
+use Bdf\Form\Attribute\Processor\Element\ElementAttributeChildBuilderAdapter;
+use Bdf\Form\Attribute\Processor\Element\ElementAttributeProcessorInterface;
+use Bdf\Form\Attribute\Processor\Element\ExtractorAttributeProcessor;
+use Bdf\Form\Attribute\Processor\Element\FilterAttributeProcessor;
+use Bdf\Form\Attribute\Processor\Element\HydratorAttributeProcessor;
+use Bdf\Form\Attribute\Processor\Element\TransformerAttributeProcessor;
use Bdf\Form\Button\ButtonInterface;
use Bdf\Form\ElementInterface;
use Override;
@@ -12,6 +21,10 @@
use ReflectionMethod;
use ReflectionNamedType;
+use function assert;
+use function is_string;
+use function is_subclass_of;
+
/**
* Base processor using reflection for extract properties and attributes
*
@@ -20,33 +33,59 @@
*
* @api
*/
-final readonly class ReflectionProcessor implements AttributesProcessorInterface
+final class ReflectionProcessor implements AttributesProcessorInterface
{
/**
- * Strategy to use on each field / class
- *
- * @var ReflectionStrategyInterface
+ * @var list
*/
- private ReflectionStrategyInterface $strategy;
+ private array $elementProcessors = [];
- /**
- * @param ReflectionStrategyInterface $strategy
- */
- public function __construct(ReflectionStrategyInterface $strategy)
- {
- $this->strategy = $strategy;
+ public function __construct(
+ /**
+ * Strategy to use on each field / class
+ *
+ * @var ReflectionStrategyInterface
+ */
+ private readonly ReflectionStrategyInterface $strategy,
+
+ /**
+ * Function for mapping the property type to the corresponding element type
+ * Takes as parameter the property type and returns the mapped property type.
+ *
+ * For example, the function can return `StringElement::class` when the property type
+ * is `string` to register a new form field.
+ *
+ * @var (callable(string):string)|null
+ */
+ private readonly mixed $elementTypeMapper = null,
+
+ /**
+ * Function for post process the metadata after the extraction
+ * This allows to add attributes to properties or on form itself
+ *
+ * Takes as parameter the metadata and context.
+ *
+ * @var (callable(ProcessorMetadata, class-string|object):void)|null
+ */
+ private readonly mixed $metadataPostProcess = null,
+ ) {
+ $this->elementProcessors[] = new ConstraintAttributeProcessor();
+ $this->elementProcessors[] = new FilterAttributeProcessor();
+ $this->elementProcessors[] = new TransformerAttributeProcessor();
+ $this->elementProcessors[] = new HydratorAttributeProcessor();
+ $this->elementProcessors[] = new ExtractorAttributeProcessor();
}
#[Override]
- public function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ public function configureBuilder(string|object $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$metadata = new ProcessorMetadata();
+ $contextClassName = is_string($context) ? $context : $context::class;
- // First iterate over methods to build the metadata
- $this->registerMethodsMetadata($form, $metadata);
-
- foreach ($this->iterateClassHierarchy($form) as $formClass) {
- $this->strategy->onFormClass($formClass, $form, $builder, $metadata);
+ foreach ($this->iterateClassHierarchy($contextClassName) as $formClass) {
+ foreach ($formClass->getAttributes(FormBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
+ $metadata->addFormAttribute($attribute->newInstance());
+ }
foreach ($formClass->getProperties() as $property) {
$name = $property->getName();
@@ -61,32 +100,73 @@ public function configureBuilder(AttributeForm $form, FormBuilderInterface $buil
$elementType = $property->getType()->getName();
+ if ($this->elementTypeMapper !== null) {
+ $elementType = ($this->elementTypeMapper)($elementType);
+ }
+
if ($elementType === ButtonInterface::class) {
$metadata->addButtonProperty($name, $property);
- $this->strategy->onButtonProperty($property, $name, $form, $builder, $metadata);
- } elseif (is_subclass_of($elementType, ElementInterface::class)) {
- $metadata->addElementProperty($name, $property);
- $this->strategy->onElementProperty($property, $name, $elementType, $form, $builder, $metadata);
+ continue;
+ }
+
+ if (is_subclass_of($elementType, ElementInterface::class)) {
+ $attributes = [];
+
+ foreach ($property->getAttributes() as $attribute) {
+ if (is_subclass_of($attribute->name, ChildBuilderAttributeInterface::class)) {
+ /** @var ChildBuilderAttributeInterface */
+ $attributes[] = $attribute->newInstance();
+ continue;
+ }
+
+ foreach ($this->elementProcessors as $configurator) {
+ if (
+ $attribute->name === $configurator->type()
+ || is_subclass_of($attribute->name, $configurator->type())
+ ) {
+ $attributes[] = new ElementAttributeChildBuilderAdapter($configurator, $attribute);
+ }
+ }
+ }
+
+ $metadata->addElementProperty(new ElementPropertyMetadata($name, $property, $elementType, $attributes));
}
}
}
- return $this->strategy->onPostConfigure($metadata, $form);
+ if ($this->metadataPostProcess !== null) {
+ ($this->metadataPostProcess)($metadata, $context);
+ }
+
+ $this->registerMethodsMetadata($contextClassName, $metadata);
+
+ $this->strategy->onFormClass($metadata, $context, $builder);
+
+ foreach ($metadata->elementProperties() as $elementProperty) {
+ $this->strategy->onElementProperty($elementProperty, $context, $builder);
+ }
+
+ foreach ($metadata->buttonProperties() as $buttonProperty) {
+ assert($buttonProperty->name !== '');
+ $this->strategy->onButtonProperty($buttonProperty, $buttonProperty->name, $context, $builder, $metadata);
+ }
+
+ return $this->strategy->onPostConfigure($metadata, $context);
}
/**
* Iterate over the class hierarchy of the annotation form
* The iteration will start with the form class, and end with the AttributeForm class (excluded)
*
- * @param AttributeForm $form
+ * @param class-string $form
*
- * @return iterable>
+ * @return iterable
*
* @psalm-suppress MoreSpecificReturnType
*/
- private function iterateClassHierarchy(AttributeForm $form): iterable
+ private function iterateClassHierarchy(string $form): iterable
{
- for ($reflection = new ReflectionClass($form); $reflection->getName() !== AttributeForm::class; $reflection = $reflection->getParentClass()) {
+ for ($reflection = new ReflectionClass($form); $reflection && $reflection->getName() !== AttributeForm::class; $reflection = $reflection->getParentClass()) {
yield $reflection;
}
}
@@ -94,12 +174,12 @@ private function iterateClassHierarchy(AttributeForm $form): iterable
/**
* Fill the metadata from methods attributes
*
- * @param AttributeForm $form
+ * @param class-string $form
* @param ProcessorMetadata $metadata
*
* @return void
*/
- private function registerMethodsMetadata(AttributeForm $form, ProcessorMetadata $metadata): void
+ private function registerMethodsMetadata(string $form, ProcessorMetadata $metadata): void
{
foreach (new ReflectionClass($form)->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
foreach ($method->getAttributes(MethodChildBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
diff --git a/src/Attribute/Processor/ReflectionStrategyInterface.php b/src/Attribute/Processor/ReflectionStrategyInterface.php
index dd72a91..68be92e 100644
--- a/src/Attribute/Processor/ReflectionStrategyInterface.php
+++ b/src/Attribute/Processor/ReflectionStrategyInterface.php
@@ -3,7 +3,7 @@
namespace Bdf\Form\Attribute\Processor;
use Bdf\Form\Aggregate\FormBuilderInterface;
-use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Aggregate\FormInterface;
use Bdf\Form\ElementInterface;
use ReflectionClass;
use ReflectionProperty;
@@ -22,14 +22,13 @@ interface ReflectionStrategyInterface
* Configure the form builder following the form class
* This method will take the current attribute form class, but also all its ancestors until AttributeForm
*
- * @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
+ * @param object|class-string $context The form instance or the DTO class name
+ * @param FormBuilderInterface $builder Builder to configure
*
* @return void
*/
- public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void;
+ public function onFormClass(ProcessorMetadata $metadata, object|string $context, FormBuilderInterface $builder): void;
/**
* Configure a button following the declared property
@@ -38,35 +37,32 @@ public function onFormClass(ReflectionClass $formClass, AttributeForm $form, For
*
* @param ReflectionProperty $property The property to process
* @param non-empty-string $name The button name
- * @param AttributeForm $form The current form instance
+ * @param object|class-string $context The form instance or the DTO class name
* @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, ProcessorMetadata $metadata): void;
+ public function onButtonProperty(ReflectionProperty $property, string $name, object|string $context, FormBuilderInterface $builder, ProcessorMetadata $metadata): void;
/**
* Configure an element following the declared property
* This method is only called one, even if the property is declared multiple times on ancestors
* Only the child declaration will be processed
*
- * @param ReflectionProperty $property The property to process
- * @param non-empty-string $name The element name
- * @param class-string $elementType The element type (i.e. the property type)
- * @param AttributeForm $form The current form instance
+ * @param ElementPropertyMetadata $metadata Metadata for the element to process
+ * @param object|class-string $context The form instance or the DTO class name
* @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, ProcessorMetadata $metadata): void;
+ public function onElementProperty(ElementPropertyMetadata $metadata, object|string $context, FormBuilderInterface $builder): void;
/**
* @param ProcessorMetadata $metadata Metadata for the current form
- * @param AttributeForm $form The current form instance
+ * @param object|class-string $context The form instance or the DTO class name
*
* @return PostConfigureInterface|null
*/
- public function onPostConfigure(ProcessorMetadata $metadata, AttributeForm $form): ?PostConfigureInterface;
+ public function onPostConfigure(ProcessorMetadata $metadata, object|string $context): ?PostConfigureInterface;
}
diff --git a/src/Registry/Registry.php b/src/Registry/Registry.php
index 817e1f7..cacef9c 100755
--- a/src/Registry/Registry.php
+++ b/src/Registry/Registry.php
@@ -39,6 +39,8 @@
use Bdf\Form\Phone\PhoneChildBuilder;
use Bdf\Form\Phone\PhoneElement;
use Bdf\Form\Phone\PhoneElementBuilder;
+use Bdf\Form\Struct\StructForm;
+use Bdf\Form\Struct\StructFormBuilder;
use InvalidArgumentException;
use Override;
@@ -92,6 +94,9 @@ public function __construct()
/** @psalm-suppress ArgumentTypeCoercion */
return new CustomFormBuilder($formClass, $this->elementBuilder(Form::class));
});
+
+ /** @psalm-suppress ArgumentTypeCoercion */
+ $this->register(StructForm::class, fn (RegistryInterface $registry) => new StructFormBuilder(builder: $registry->elementBuilder(Form::class)));
}
#[Override]
diff --git a/src/Struct/StructAttributesProcessorFactory.php b/src/Struct/StructAttributesProcessorFactory.php
new file mode 100644
index 0000000..6d2c534
--- /dev/null
+++ b/src/Struct/StructAttributesProcessorFactory.php
@@ -0,0 +1,289 @@
+>
+ */
+ private array $mapping = [
+ 'int' => IntegerElement::class,
+ 'string' => StringElement::class,
+ 'array' => ArrayElement::class,
+ 'float' => FloatElement::class,
+ 'boot' => BooleanElement::class,
+ DateTime::class => DateTimeElement::class,
+ DateTimeInterface::class => DateTimeElement::class,
+ PhoneNumber::class => PhoneElement::class,
+ ];
+
+ /**
+ * Map a property type to an element type, using {@see is_subclass_of()} to check for inheritance
+ *
+ * @var array>
+ */
+ private array $mappingInstanceOf = [
+ DateTimeInterface::class => DateTimeElement::class,
+ UnitEnum::class => UnitEnumElement::class,
+ ];
+
+ /**
+ * Associate a post processor for a given form element type.
+ * The exact element type will be used to resolve the processor.
+ *
+ * @var array, Closure(ElementPropertyMetadata, class-string|object):void>
+ */
+ private array $elementPostProcessors = [];
+
+ public function __construct()
+ {
+ $this->elementPostProcessors = [
+ StructForm::class => $this->postProcessStructForm(...),
+ DateTimeElement::class => $this->postProcessDateTimeElement(...),
+ UnitEnumElement::class => $this->postProcessEnumElement(...),
+ ];
+ }
+
+ /**
+ * Create the attributes processor with on the fly form configuration
+ * This processor will not use any cache
+ */
+ public function runtime(): AttributesProcessorInterface
+ {
+ return $this->create(new ConfigureFormBuilderStrategy());
+ }
+
+ /**
+ * Create the attributes processor with the given strategy
+ */
+ public function create(ReflectionStrategyInterface $strategy): AttributesProcessorInterface
+ {
+ return new ReflectionProcessor(
+ $strategy,
+ $this->mapElementType(...),
+ $this->postProcess(...),
+ );
+ }
+
+ /**
+ * Create the attributes processor with code generation for form configuration
+ *
+ * @param Closure(class-string):non-empty-string $classNameResolver
+ * @param Closure(class-string):non-empty-string $fileNameResolver
+ */
+ public function generated(Closure $classNameResolver, Closure $fileNameResolver): AttributesProcessorInterface
+ {
+ return new CompileAttributesProcessor(
+ $classNameResolver,
+ $fileNameResolver,
+ $this->create(...),
+ );
+ }
+
+ public function mapElementType(string $type): string
+ {
+ $mappedType = $this->mapping[$type] ?? null;
+
+ if ($mappedType !== null) {
+ return $mappedType;
+ }
+
+ foreach ($this->mappingInstanceOf as $instanceOf => $mappedType) {
+ if (is_subclass_of($type, $instanceOf)) {
+ return $mappedType;
+ }
+ }
+
+ if (class_exists($type)) {
+ return StructForm::class;
+ }
+
+ return $type;
+ }
+
+ /**
+ * @param ProcessorMetadata $metadata
+ * @param object|class-string $context
+ */
+ public function postProcess(ProcessorMetadata $metadata, object|string $context): void
+ {
+ if (
+ is_string($context)
+ && !$metadata->hasFormAttribute(Generates::class)
+ && !$metadata->hasFormAttribute(CallbackGenerator::class)
+ ) {
+ $metadata->addFormAttribute(new Generates($context));
+ }
+
+ foreach ($metadata->elementProperties() as $property) {
+ $hasGetSet = $property->hasAttribute(GetSet::class);
+ $hasGetter = $hasGetSet || $property->hasAttribute(ExtractorInterface::class);
+ $hasSetter = $hasGetSet || $property->hasAttribute(HydratorInterface::class);
+
+ if (!$hasGetter && !$hasSetter) {
+ $property->addAttribute(new GetSet());
+ }
+
+ if (
+ !$property->hasAttribute(Required::class)
+ && $this->isRequired($property->property)
+ ) {
+ /** @psalm-suppress InvalidArgument */
+ $property->addAttribute(new Required());
+ }
+
+ if (!$property->hasAttribute(DefaultValue::class)) {
+ $defaultValue = $this->getDefaultValue($property->property);
+
+ if ($defaultValue !== null) {
+ $property->addAttribute(new DefaultValue($defaultValue));
+ }
+ }
+
+ $postProcessor = $this->elementPostProcessors[$property->elementType] ?? null;
+
+ if ($postProcessor !== null) {
+ $postProcessor($property, $context);
+ }
+ }
+ }
+
+ private function isRequired(ReflectionProperty $property): bool
+ {
+ if ($property->hasDefaultValue()) {
+ return false;
+ }
+
+ $type = $property->getType();
+
+ // Types that allows empty values are not considered as required
+ if ($type === null || $type->allowsNull() || (string) $type === 'array') {
+ return false;
+ }
+
+ return !$type->allowsNull();
+ }
+
+ private function getDefaultValue(ReflectionProperty $property): mixed
+ {
+ if ($property->hasDefaultValue()) {
+ return $property->getDefaultValue();
+ }
+
+ if (!$property->isPromoted()) {
+ return null;
+ }
+
+ $constructor = $property->getDeclaringClass()->getConstructor();
+ assert($constructor !== null);
+
+ $parameter = array_find($constructor->getParameters(), static fn (ReflectionParameter $parameter) => $parameter->getName() === $property->getName());
+ assert($parameter !== null);
+
+ if ($parameter->isDefaultValueAvailable()) {
+ return $parameter->getDefaultValue();
+ }
+
+ return null;
+ }
+
+ /**
+ * @param ElementPropertyMetadata $property
+ * @param class-string|object $context
+ */
+ private function postProcessStructForm(ElementPropertyMetadata $property, string|object $context): void
+ {
+ if (!$property->hasAttribute(StructClass::class)) {
+ $type = $property->property->getType();
+ assert($type instanceof ReflectionNamedType);
+
+ $typeName = $type->getName();
+ assert(class_exists($typeName));
+
+ $property->addAttribute(new StructClass($typeName));
+ }
+
+ if (!$property->hasAttribute(Required::class) && !$property->hasAttribute(Optional::class)) {
+ $property->addAttribute(new Optional());
+ }
+ }
+
+ /**
+ * @param ElementPropertyMetadata $property
+ * @param class-string|object $context
+ */
+ private function postProcessDateTimeElement(ElementPropertyMetadata $property, string|object $context): void
+ {
+ if (!$property->hasAttribute(DateTimeClass::class)) {
+ $type = $property->property->getType();
+ assert($type instanceof ReflectionNamedType);
+
+ $typeName = $type->getName();
+
+ if (!in_array($typeName, [DateTime::class, DateTimeInterface::class], true)) {
+ assert(is_subclass_of($typeName, DateTimeInterface::class));
+
+ /** @psalm-suppress InvalidArgument */
+ $property->addAttribute(new DateTimeClass($typeName));
+ }
+ }
+ }
+
+ /**
+ * @param ElementPropertyMetadata $property
+ * @param class-string|object $context
+ */
+ private function postProcessEnumElement(ElementPropertyMetadata $property, string|object $context): void
+ {
+ $type = $property->property->getType();
+ assert($type instanceof ReflectionNamedType);
+
+ $property->addAttribute(new BuilderMethodCall('enumClass', [$type->getName()]));
+ }
+}
diff --git a/src/Struct/StructForm.php b/src/Struct/StructForm.php
new file mode 100644
index 0000000..36a84a8
--- /dev/null
+++ b/src/Struct/StructForm.php
@@ -0,0 +1,45 @@
+
+ */
+final class StructForm extends CustomForm
+{
+ private readonly AttributesProcessorInterface $processor;
+
+ public function __construct(
+ /**
+ * The struct class name
+ *
+ * @var class-string
+ */
+ private readonly string $class,
+ ?FormBuilderInterface $builder = null,
+ ?AttributesProcessorInterface $processor = null,
+ )
+ {
+ parent::__construct($builder);
+
+ $this->processor = $processor ?? new StructAttributesProcessorFactory()->runtime();
+ }
+
+ #[Override]
+ protected function configure(FormBuilderInterface $builder): void
+ {
+ $this->processor->configureBuilder($this->class, $builder);
+ }
+}
diff --git a/src/Struct/StructFormBuilder.php b/src/Struct/StructFormBuilder.php
new file mode 100644
index 0000000..bded919
--- /dev/null
+++ b/src/Struct/StructFormBuilder.php
@@ -0,0 +1,73 @@
+>
+ */
+final class StructFormBuilder implements ElementBuilderInterface
+{
+ use DelegateElementBuilderTrait;
+
+ /**
+ * The struct class name
+ *
+ * @var class-string|null
+ */
+ private ?string $class = null;
+
+ private readonly FormBuilderInterface $builder;
+
+ public function __construct(
+ private readonly ?AttributesProcessorInterface $processor = null,
+ ?FormBuilderInterface $builder = null
+ )
+ {
+ $this->builder = $builder ?? new FormBuilder();
+ }
+
+ /**
+ * Define the struct class name
+ *
+ * @param class-string $class
+ * @return $this
+ * @internal
+ */
+ public function class(string $class): self
+ {
+ if ($this->class !== null) {
+ throw new LogicException('The class has already been set.');
+ }
+
+ $this->class = $class;
+ return $this;
+ }
+
+ #[Override]
+ protected function getElementBuilder(): ElementBuilderInterface
+ {
+ return $this->builder;
+ }
+
+ #[Override]
+ public function buildElement(): StructForm
+ {
+ if ($this->class === null) {
+ throw new LogicException('A struct class is required to build a StructForm');
+ }
+
+ return new StructForm(
+ $this->class,
+ $this->builder,
+ $this->processor,
+ );
+ }
+}
diff --git a/tests/Aggregate/ArrayElementBuilderTest.php b/tests/Aggregate/ArrayElementBuilderTest.php
index 3e09f3a..2a1bdfa 100644
--- a/tests/Aggregate/ArrayElementBuilderTest.php
+++ b/tests/Aggregate/ArrayElementBuilderTest.php
@@ -14,6 +14,8 @@
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\Leaf\StringElementBuilder;
use Bdf\Form\Phone\PhoneElementBuilder;
+use Bdf\Form\Struct\Fixtures\SimpleDto;
+use Bdf\Form\Struct\StructFormBuilder;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;
@@ -196,6 +198,27 @@ public function test_enum()
$this->assertSame([MyStringEnum::Bar], $values);
}
+ public function test_struct()
+ {
+ $element = $this->builder->struct(SimpleDto::class, function (StructFormBuilder $builder) {})->buildElement();
+
+ $values = $element->submit([
+ [
+ 'name' => 'foo',
+ 'value' => 5,
+ ],
+ [
+ 'name' => 'bar',
+ 'value' => 6,
+ ],
+ ])->value();
+
+ $this->assertEquals([
+ new SimpleDto('foo', 5),
+ new SimpleDto('bar', 6),
+ ], $values);
+ }
+
/**
*
*/
diff --git a/tests/Aggregate/FormBuilderTest.php b/tests/Aggregate/FormBuilderTest.php
index 9e2dced..58d4623 100644
--- a/tests/Aggregate/FormBuilderTest.php
+++ b/tests/Aggregate/FormBuilderTest.php
@@ -20,6 +20,8 @@
use Bdf\Form\Phone\FormattedPhoneElement;
use Bdf\Form\Phone\PhoneChildBuilder;
use Bdf\Form\Phone\PhoneElement;
+use Bdf\Form\Struct\Fixtures\SimpleDto;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Validator\Constraints\Count;
@@ -177,6 +179,25 @@ public function test_enum()
$this->assertSame(MyStringEnum::Foo, $form['value']->element()->value());
}
+ /**
+ *
+ */
+ public function test_struct()
+ {
+ $this->assertInstanceOf(ChildBuilder::class, $this->builder->struct('value', SimpleDto::class));
+
+ $form = $this->builder->buildElement();
+
+ $this->assertInstanceOf(Form::class, $form);
+ $this->assertInstanceOf(StructForm::class, $form['value']->element());
+
+ $form->submit(['value' => [
+ 'name' => 'foo',
+ 'value' => '42'
+ ]]);
+ $this->assertEquals(new SimpleDto('foo', 42), $form['value']->element()->value());
+ }
+
/**
*
*/
diff --git a/tests/Aggregate/FormTest.php b/tests/Aggregate/FormTest.php
index b45835b..416ffcd 100644
--- a/tests/Aggregate/FormTest.php
+++ b/tests/Aggregate/FormTest.php
@@ -1129,6 +1129,20 @@ public function test_optional_patch()
$this->assertFalse($this->form->patch(0)->valid());
$this->assertFalse($this->form->patch(false)->valid());
}
+
+ public function test_value_using_dto_constructor()
+ {
+ $this->form->attach(PersonImmutable::class);
+
+ $this->form->submit([
+ 'firstName' => 'John',
+ 'lastName' => 'Doe',
+ 'id' => 4,
+ ]);
+
+ $this->assertTrue($this->form->valid());
+ $this->assertEquals(new PersonImmutable(4, 'John', 'Doe'), $this->form->value());
+ }
}
class Person
@@ -1137,3 +1151,12 @@ class Person
public $firstName;
public $lastName;
}
+
+readonly class PersonImmutable
+{
+ public function __construct(
+ public int $id,
+ public string $firstName,
+ public string $lastName,
+ ) {}
+}
diff --git a/tests/Aggregate/Value/ClosureValueGeneratorTest.php b/tests/Aggregate/Value/ClosureValueGeneratorTest.php
new file mode 100644
index 0000000..523241d
--- /dev/null
+++ b/tests/Aggregate/Value/ClosureValueGeneratorTest.php
@@ -0,0 +1,35 @@
+ ['foo' => 'bar']);
+
+ $value = $generator->generate($this->createMock(ElementInterface::class));
+ $this->assertSame(['foo' => 'bar'], $value);
+ $this->assertSame($value, $generator->finalize($value));
+ }
+
+ #[Test]
+ public function generate_with_attachment()
+ {
+ $generator = new ClosureValueGenerator(fn (ElementInterface $e) => ['foo' => 'bar']);
+ $attachment = new stdClass();
+ $generator->attach($attachment);
+
+ $value = $generator->generate($this->createMock(ElementInterface::class));
+ $this->assertSame($attachment, $value);
+ $this->assertSame($value, $generator->finalize($value));
+ }
+}
diff --git a/tests/Aggregate/Value/ConstructorValueGeneratorTest.php b/tests/Aggregate/Value/ConstructorValueGeneratorTest.php
new file mode 100644
index 0000000..5bb99ce
--- /dev/null
+++ b/tests/Aggregate/Value/ConstructorValueGeneratorTest.php
@@ -0,0 +1,67 @@
+generate($this->createMock(ElementInterface::class));
+
+ $this->assertSame([], $value);
+ $this->assertEquals(new MyDto('bar'), $generator->finalize(['foo' => 'bar']));
+ }
+
+ #[Test]
+ public function generateWithAttachmentObject()
+ {
+ $generator = new ConstructorValueGenerator(MyDto::class);
+ $attachment = new MyDto('bar');
+
+ $generator->attach($attachment);
+
+ $value = $generator->generate($this->createMock(ElementInterface::class));
+
+ $this->assertSame($attachment, $value);
+ $this->assertSame($value, $generator->finalize($value));
+ }
+
+ #[Test]
+ public function generateWithAttachmentArray()
+ {
+ $generator = new ConstructorValueGenerator(MyDto::class);
+ $attachment = ['foo' => 'bar'];
+
+ $generator->attach($attachment);
+
+ $value = $generator->generate($this->createMock(ElementInterface::class));
+
+ $this->assertSame($attachment, $value);
+ $this->assertEquals(new MyDto('bar'), $generator->finalize($value));
+ }
+
+ #[
+ Test,
+ TestWith([MyDto::class]),
+ TestWith([new stdClass()]),
+ ]
+ public function attachOnyAllowSameType($value)
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ $generator = new ConstructorValueGenerator(MyDto::class);
+ $generator->attach($value);
+ }
+}
diff --git a/tests/Aggregate/Value/DefaultConstructorValueGeneratorTest.php b/tests/Aggregate/Value/DefaultConstructorValueGeneratorTest.php
new file mode 100644
index 0000000..a3c2ec7
--- /dev/null
+++ b/tests/Aggregate/Value/DefaultConstructorValueGeneratorTest.php
@@ -0,0 +1,55 @@
+generate($this->createMock(ElementInterface::class));
+
+ $this->assertInstanceOf(MyEntity::class, $value);
+ $this->assertSame($value, $generator->finalize($value));
+ }
+
+ #[Test]
+ public function generateWithAttachment()
+ {
+ $generator = new DefaultConstructorValueGenerator(MyEntity::class);
+ $attachment = new MyEntity();
+ $attachment->foo = 'bar';
+
+ $generator->attach($attachment);
+
+ $value = $generator->generate($this->createMock(ElementInterface::class));
+
+ $this->assertSame($attachment, $value);
+ $this->assertSame($value, $generator->finalize($value));
+ }
+
+ #[
+ Test,
+ TestWith([MyEntity::class]),
+ TestWith([new stdClass()]),
+ TestWith([[]]),
+ ]
+ public function attachOnyAllowSameType($value)
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ $generator = new DefaultConstructorValueGenerator(MyEntity::class);
+ $generator->attach($value);
+ }
+}
diff --git a/tests/Aggregate/Value/Fixtures/MyDto.php b/tests/Aggregate/Value/Fixtures/MyDto.php
new file mode 100644
index 0000000..4db7c7c
--- /dev/null
+++ b/tests/Aggregate/Value/Fixtures/MyDto.php
@@ -0,0 +1,12 @@
+foo = 'bar';
+
+ $value = $generator->generate($this->createMock(ElementInterface::class));
+
+ $this->assertEquals($o, $value);
+ $this->assertNotSame($o, $value);
+ $this->assertSame($value, $generator->finalize($value));
+ }
+
+ #[Test]
+ public function generateWithAttachmentObject()
+ {
+ $generator = new ObjectValueGenerator(new MyEntity());
+ $attachment = new MyEntity();
+ $attachment->foo = 'bar';
+
+ $generator->attach($attachment);
+
+ $value = $generator->generate($this->createMock(ElementInterface::class));
+
+ $this->assertSame($attachment, $value);
+ $this->assertSame($value, $generator->finalize($value));
+ }
+
+ #[
+ Test,
+ TestWith([MyDto::class]),
+ TestWith([[]]),
+ TestWith([new stdClass()]),
+ ]
+ public function attachOnyAllowSameType($value)
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ $generator = new ObjectValueGenerator(new MyEntity());
+ $generator->attach($value);
+ }
+}
diff --git a/tests/Aggregate/Value/SimpleValueGeneratorTest.php b/tests/Aggregate/Value/SimpleValueGeneratorTest.php
new file mode 100644
index 0000000..dc4723a
--- /dev/null
+++ b/tests/Aggregate/Value/SimpleValueGeneratorTest.php
@@ -0,0 +1,51 @@
+foo = 'bar';
+
+ $value = $generator->generate($this->createMock(ElementInterface::class));
+
+ $this->assertSame($o, $value);
+ $this->assertSame($value, $generator->finalize($value));
+ }
+
+ #[Test]
+ public function generateWithoutAttachmentArray()
+ {
+ $generator = new SimpleValueGenerator(['foo' => 'bar']);
+
+ $value = $generator->generate($this->createMock(ElementInterface::class));
+
+ $this->assertSame(['foo' => 'bar'], $value);
+ $this->assertSame($value, $generator->finalize($value));
+ }
+
+ #[Test]
+ public function generateWithAttachmentObject()
+ {
+ $generator = new SimpleValueGenerator(new MyEntity());
+ $attachment = new MyEntity();
+ $attachment->foo = 'bar';
+
+ $generator->attach($attachment);
+
+ $value = $generator->generate($this->createMock(ElementInterface::class));
+
+ $this->assertSame($attachment, $value);
+ $this->assertSame($value, $generator->finalize($value));
+ }
+}
diff --git a/tests/Aggregate/Value/ValueGeneratorTest.php b/tests/Aggregate/Value/ValueGeneratorTest.php
index a77783d..0cc8411 100644
--- a/tests/Aggregate/Value/ValueGeneratorTest.php
+++ b/tests/Aggregate/Value/ValueGeneratorTest.php
@@ -3,11 +3,13 @@
namespace Bdf\Form\Aggregate\Value;
use Bdf\Form\ElementInterface;
+use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
/**
* Class ValueGeneratorTest
*/
+#[AllowMockObjectsWithoutExpectations]
class ValueGeneratorTest extends TestCase
{
/**
@@ -16,14 +18,19 @@ class ValueGeneratorTest extends TestCase
public function test_generate_default()
{
$this->assertSame([], (new ValueGenerator())->generate($this->createMock(ElementInterface::class)));
+ $this->assertSame([], (new ValueGenerator())->finalize([]));
}
/**
*
*/
- public function test_generate_with_className()
+ public function test_generate_with_className_no_constructor()
{
- $this->assertInstanceOf(MyEntity::class, (new ValueGenerator(MyEntity::class))->generate($this->createMock(ElementInterface::class)));
+ $generator = new ValueGenerator(Fixtures\MyEntity::class);
+ $value = $generator->generate($this->createMock(ElementInterface::class));
+
+ $this->assertInstanceOf(Fixtures\MyEntity::class, $value);
+ $this->assertSame($value, $generator->finalize($value));
}
/**
@@ -31,13 +38,16 @@ public function test_generate_with_className()
*/
public function test_generate_with_entity()
{
- $entity = new MyEntity();
+ $entity = new Fixtures\MyEntity();
$entity->foo = 'bar';
- $generated = (new ValueGenerator($entity))->generate($this->createMock(ElementInterface::class));
+ $generator = (new ValueGenerator($entity));
+
+ $generated = $generator->generate($this->createMock(ElementInterface::class));
$this->assertEquals($entity, $generated);
$this->assertNotSame($entity, $generated);
+ $this->assertSame($generated, $generator->finalize($generated));
}
/**
@@ -45,19 +55,21 @@ public function test_generate_with_entity()
*/
public function test_generate_with_callback()
{
- $entity = new MyEntity();
+ $entity = new Fixtures\MyEntity();
$entity->foo = 'bar';
-
- $element = $this->createMock(ElementInterface::class);
-
- $generated = (new ValueGenerator(function ($element) use(&$param) {
+ $generator = new ValueGenerator(function ($element) use(&$param) {
$param = $element;
return ['foo' => 'bar'];
- }))->generate($element);
+ });
+
+ $element = $this->createMock(ElementInterface::class);
+
+ $generated = $generator->generate($element);
$this->assertEquals(['foo' => 'bar'], $generated);
$this->assertSame($element, $param);
+ $this->assertSame($generated, $generator->finalize($generated));
}
/**
@@ -65,7 +77,7 @@ public function test_generate_with_callback()
*/
public function test_attach_entity_should_return_the_same_instance()
{
- $entity = new MyEntity();
+ $entity = new Fixtures\MyEntity();
$entity->foo = 'bar';
$element = $this->createMock(ElementInterface::class);
@@ -75,6 +87,7 @@ public function test_attach_entity_should_return_the_same_instance()
$generated = $generator->generate($element);
$this->assertSame($entity, $generated);
+ $this->assertSame($generated, $generator->finalize($generated));
}
/**
@@ -82,7 +95,7 @@ public function test_attach_entity_should_return_the_same_instance()
*/
public function test_attach_with_callback()
{
- $entity = new MyEntity();
+ $entity = new Fixtures\MyEntity();
$entity->foo = 'bar';
$element = $this->createMock(ElementInterface::class);
@@ -97,11 +110,17 @@ public function test_attach_with_callback()
$this->assertEquals(['foo' => 'bar'], $generated);
$this->assertSame($element, $param);
+ $this->assertSame($generated, $generator->finalize($generated));
}
-}
+ public function test_with_dto_constructor()
+ {
+ $element = $this->createMock(ElementInterface::class);
+ $generator = new ValueGenerator();
-class MyEntity
-{
- public $foo;
+ $generator->attach(Fixtures\MyDto::class);
+
+ $this->assertSame([], $generator->generate($element));
+ $this->assertEquals(new Fixtures\MyDto('bar'), $generator->finalize(['foo' => 'bar']));
+ }
}
diff --git a/tests/Attribute/Aggregate/ArrayConstraintTest.php b/tests/Attribute/Aggregate/ArrayConstraintTest.php
index 335ae09..a0b421a 100644
--- a/tests/Attribute/Aggregate/ArrayConstraintTest.php
+++ b/tests/Attribute/Aggregate/ArrayConstraintTest.php
@@ -9,7 +9,9 @@
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy;
use Bdf\Form\Attribute\Processor\ReflectionProcessor;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Symfony\Component\Validator\Constraints\Unique;
use Tests\Form\Attribute\TestCase;
@@ -31,6 +33,19 @@ public function test(AttributesProcessorInterface $processor)
$this->assertTrue($form->valid());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestArrayConstraintStruct::class, processor: $processor);
+
+ $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());
+ }
+
/**
* @return void
*/
@@ -57,7 +72,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$values = $builder->add('values', ArrayElement::class);
$values->arrayConstraint(new Unique(message: 'Not unique', groups: ['Default']));
@@ -77,4 +92,48 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
PHP
, $form);
}
+
+ /**
+ * @return void
+ */
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Symfony\Component\Validator\Constraints\Unique;
+use Tests\Form\Attribute\Aggregate\TestArrayConstraintStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestArrayConstraintStruct::class);
+
+ $values = $builder->add('values', ArrayElement::class);
+ $values->arrayConstraint(new Unique(message: 'Not unique', groups: ['Default']));
+ $values->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestArrayConstraintStruct::class);
+ }
+}
+
+class TestArrayConstraintStruct
+{
+ #[ArrayConstraint(new Unique(message: 'Not unique'))]
+ public array $values;
}
diff --git a/tests/Attribute/Aggregate/ArrayTransformerTest.php b/tests/Attribute/Aggregate/ArrayTransformerTest.php
index 7ada9f3..a5d8f15 100644
--- a/tests/Attribute/Aggregate/ArrayTransformerTest.php
+++ b/tests/Attribute/Aggregate/ArrayTransformerTest.php
@@ -7,8 +7,10 @@
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\ElementInterface;
+use Bdf\Form\Struct\StructForm;
use Bdf\Form\Transformer\TransformerInterface;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class ArrayTransformerTest extends TestCase
@@ -28,6 +30,18 @@ public function test(AttributesProcessorInterface $processor)
$this->assertEquals(['A_A', 'A-A'], $view['foo']->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestArrayTransformerStruct::class, processor: $processor);
+
+ $form->submit(['foo' => ['_', '-']]);
+ $this->assertEquals(['A_', 'A-'], $form['foo']->element()->value());
+
+ $view = $form->view();
+ $this->assertEquals(['A_A', 'A-A'], $view['foo']->value());
+ }
+
public function test_code_generator()
{
$form = new class extends AttributeForm {
@@ -51,7 +65,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', ArrayElement::class);
$foo->arrayTransformer(new AArrayTransformer('A'));
@@ -70,6 +84,42 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
PHP
, $form
+);
+ }
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Tests\Form\Attribute\Aggregate\AArrayTransformer;
+use Tests\Form\Attribute\Aggregate\TestArrayTransformerStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestArrayTransformerStruct::class);
+
+ $foo = $builder->add('foo', ArrayElement::class);
+ $foo->arrayTransformer(new AArrayTransformer('A'));
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestArrayTransformerStruct::class,
);
}
}
@@ -91,3 +141,9 @@ public function transformFromHttp($value, ElementInterface $input)
return array_map(fn($v) => $this->c . $v, $value);
}
}
+
+class TestArrayTransformerStruct
+{
+ #[ArrayTransformer(AArrayTransformer::class, ['A'])]
+ public array $foo;
+}
diff --git a/tests/Attribute/Aggregate/AsArrayConstraintTest.php b/tests/Attribute/Aggregate/AsArrayConstraintTest.php
index 3a41782..11af25a 100644
--- a/tests/Attribute/Aggregate/AsArrayConstraintTest.php
+++ b/tests/Attribute/Aggregate/AsArrayConstraintTest.php
@@ -7,7 +7,9 @@
use Bdf\Form\Attribute\Aggregate\CallbackArrayConstraint;
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class AsArrayConstraintTest extends TestCase
@@ -48,6 +50,32 @@ public function validateFoo(array $value): bool
$this->assertNull($form->bar->error()->global());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestAsArrayConstraintStruct::class, processor: $processor);
+
+ $form->submit(['foo' => ['a']]);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals('Foo size must be a multiple of 2', $form['foo']->element()->error()->global());
+
+ $form->submit(['foo' => ['a', 'b']]);
+
+ $this->assertTrue($form->valid());
+ $this->assertNull($form['foo']->element()->error()->global());
+
+ $form->submit(['bar' => ['a']]);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals('The value is invalid', $form['bar']->element()->error()->global());
+
+ $form->submit(['bar' => ['a', 'b']]);
+
+ $this->assertTrue($form->valid());
+ $this->assertNull($form['bar']->element()->error()->global());
+ }
+
public function test_code_generator()
{
$form = new class extends AttributeForm {
@@ -79,13 +107,13 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', ArrayElement::class);
- $foo->arrayConstraint(new ClosureConstraint($form->validateFoo(...), 'Foo size must be a multiple of 2'));
+ $foo->arrayConstraint(new ClosureConstraint($context->validateFoo(...), 'Foo size must be a multiple of 2'));
$bar = $builder->add('bar', ArrayElement::class);
- $bar->arrayConstraint(new ClosureConstraint($form->validateFoo(...)));
+ $bar->arrayConstraint(new ClosureConstraint($context->validateFoo(...)));
return $this;
}
@@ -104,4 +132,57 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Constraint\Closure as ClosureConstraint;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Tests\Form\Attribute\Aggregate\TestAsArrayConstraintStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestAsArrayConstraintStruct::class);
+
+ $foo = $builder->add('foo', ArrayElement::class);
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+ $foo->arrayConstraint(new ClosureConstraint(\Tests\Form\Attribute\Aggregate\TestAsArrayConstraintStruct::validateFoo(...), 'Foo size must be a multiple of 2'));
+
+ $bar = $builder->add('bar', ArrayElement::class);
+ $bar->hydrator(new Setter(null))->extractor(new Getter(null));
+ $bar->arrayConstraint(new ClosureConstraint(\Tests\Form\Attribute\Aggregate\TestAsArrayConstraintStruct::validateFoo(...)));
+
+ return null;
+ }
+}
+
+PHP
+ , TestAsArrayConstraintStruct::class
+ );
+ }
+}
+
+class TestAsArrayConstraintStruct
+{
+ public array $foo;
+ public array $bar;
+
+ #[AsArrayConstraint('foo', message: 'Foo size must be a multiple of 2')]
+ #[AsArrayConstraint('bar')]
+ public static function validateFoo(array $value): bool
+ {
+ return count($value) % 2 === 0;
+ }
}
diff --git a/tests/Attribute/Aggregate/CallbackArrayConstraintTest.php b/tests/Attribute/Aggregate/CallbackArrayConstraintTest.php
index 77bc0ae..46efda7 100644
--- a/tests/Attribute/Aggregate/CallbackArrayConstraintTest.php
+++ b/tests/Attribute/Aggregate/CallbackArrayConstraintTest.php
@@ -6,7 +6,9 @@
use Bdf\Form\Attribute\Aggregate\CallbackArrayConstraint;
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class CallbackArrayConstraintTest extends TestCase
@@ -47,6 +49,31 @@ public function validateFoo(array $value): bool
$this->assertTrue($form->valid());
$this->assertNull($form->bar->error()->global());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestCallbackArrayConstraintStruct::class, processor: $processor);
+
+ $form->submit(['foo' => ['a']]);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals('Foo size must be a multiple of 2', $form['foo']->element()->error()->global());
+
+ $form->submit(['foo' => ['a', 'b']]);
+
+ $this->assertTrue($form->valid());
+ $this->assertNull($form['foo']->element()->error()->global());
+
+ $form->submit(['bar' => ['a']]);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals('The value is invalid', $form['bar']->element()->error()->global());
+
+ $form->submit(['bar' => ['a', 'b']]);
+
+ $this->assertTrue($form->valid());
+ $this->assertNull($form['bar']->element()->error()->global());
+ }
public function test_code_generator()
{
@@ -79,13 +106,13 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', ArrayElement::class);
- $foo->arrayConstraint(new ClosureConstraint($form->validateFoo(...), 'Foo size must be a multiple of 2'));
+ $foo->arrayConstraint(new ClosureConstraint($context->validateFoo(...), 'Foo size must be a multiple of 2'));
$bar = $builder->add('bar', ArrayElement::class);
- $bar->arrayConstraint(new ClosureConstraint($form->validateFoo(...)));
+ $bar->arrayConstraint(new ClosureConstraint($context->validateFoo(...)));
return $this;
}
@@ -104,4 +131,58 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Constraint\Closure as ClosureConstraint;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Tests\Form\Attribute\Aggregate\TestCallbackArrayConstraintStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestCallbackArrayConstraintStruct::class);
+
+ $foo = $builder->add('foo', ArrayElement::class);
+ $foo->arrayConstraint(new ClosureConstraint(\Tests\Form\Attribute\Aggregate\TestCallbackArrayConstraintStruct::validateFoo(...), 'Foo size must be a multiple of 2'));
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ $bar = $builder->add('bar', ArrayElement::class);
+ $bar->arrayConstraint(new ClosureConstraint(\Tests\Form\Attribute\Aggregate\TestCallbackArrayConstraintStruct::validateFoo(...)));
+ $bar->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestCallbackArrayConstraintStruct::class
+ );
+ }
+}
+
+class TestCallbackArrayConstraintStruct
+{
+ #[CallbackArrayConstraint('validateFoo', message: 'Foo size must be a multiple of 2')]
+ public array $foo;
+
+ #[CallbackArrayConstraint('validateFoo')]
+ public array $bar;
+
+ public static function validateFoo(array $value): bool
+ {
+ return count($value) % 2 === 0;
+ }
}
diff --git a/tests/Attribute/Aggregate/CountTest.php b/tests/Attribute/Aggregate/CountTest.php
index 25946d1..afb1a71 100644
--- a/tests/Attribute/Aggregate/CountTest.php
+++ b/tests/Attribute/Aggregate/CountTest.php
@@ -9,7 +9,9 @@
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy;
use Bdf\Form\Attribute\Processor\ReflectionProcessor;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class CountTest extends TestCase
@@ -34,6 +36,23 @@ public function test(AttributesProcessorInterface $processor)
$this->assertTrue($form->valid());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestCountStruct::class, processor: $processor);
+
+ $form->submit([]);
+ $this->assertFalse($form->valid());
+ $this->assertEquals(['values' => 'This collection should contain 3 elements or more.'], $form->error()->toArray());
+
+ $form->submit(['values' => ['aaa', 'bbb', 'ccc', 'ddd', 'eee', 'fff']]);
+ $this->assertFalse($form->valid());
+ $this->assertEquals(['values' => 'This collection should contain 5 elements or less.'], $form->error()->toArray());
+
+ $form->submit(['values' => ['aaa', 'bbb', 'ccc']]);
+ $this->assertTrue($form->valid());
+ }
+
/**
* @return void
*/
@@ -60,7 +79,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$values = $builder->add('values', ArrayElement::class);
$values->arrayConstraint(new Count(min: 3, max: 5));
@@ -80,4 +99,48 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
PHP
, $form);
}
+
+ /**
+ * @return void
+ */
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Symfony\Component\Validator\Constraints\Count;
+use Tests\Form\Attribute\Aggregate\TestCountStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestCountStruct::class);
+
+ $values = $builder->add('values', ArrayElement::class);
+ $values->arrayConstraint(new Count(min: 3, max: 5));
+ $values->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestCountStruct::class);
+ }
+}
+
+class TestCountStruct
+{
+ #[Count(min: 3, max: 5)]
+ public array $values;
}
diff --git a/tests/Attribute/Aggregate/ElementTypeTest.php b/tests/Attribute/Aggregate/ElementTypeTest.php
index 97bda4c..fb91b97 100644
--- a/tests/Attribute/Aggregate/ElementTypeTest.php
+++ b/tests/Attribute/Aggregate/ElementTypeTest.php
@@ -14,7 +14,9 @@
use Bdf\Form\Leaf\IntegerElementBuilder;
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\PropertyAccess\Setter;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class ElementTypeTest extends TestCase
@@ -33,6 +35,17 @@ public function test_simple(AttributesProcessorInterface $processor)
$this->assertSame(['values' => [123, 456, 789]], $form->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function simple_struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestElementTypeSimpleStruct::class, processor: $processor);
+
+ $form->submit(['values' => ['123', '456', '789']]);
+ $this->assertTrue($form->valid());
+
+ $this->assertSame([123, 456, 789], $form->value()->values);
+ }
+
#[DataProvider('provideAttributesProcessor')]
public function test_with_configurator(AttributesProcessorInterface $processor)
{
@@ -52,6 +65,17 @@ public function configureField(IntegerElementBuilder $builder): void
$this->assertEquals(['values' => [0 => 'This value should be greater than or equal to 200.']], $form->error()->toArray());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function test_with_configurator_struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestElementTypeConfiguratorStruct::class, processor: $processor);
+
+ $form->submit(['values' => ['123', '456', '789']]);
+ $this->assertFalse($form->valid());
+
+ $this->assertEquals(['values' => [0 => 'This value should be greater than or equal to 200.']], $form->error()->toArray());
+ }
+
#[DataProvider('provideAttributesProcessor')]
public function test_with_embedded(AttributesProcessorInterface $processor)
{
@@ -98,10 +122,10 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$values = $builder->add('values', ArrayElement::class);
- $values->element(IntegerElement::class, [$form, 'configureField']);
+ $values->element(IntegerElement::class, $context->configureField(...));
$values->hydrator(new Setter());
return $this;
@@ -119,6 +143,44 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
PHP
, $form);
}
+
+ /**
+ * @return void
+ */
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+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;
+use Tests\Form\Attribute\Aggregate\TestElementTypeConfiguratorStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestElementTypeConfiguratorStruct::class);
+
+ $values = $builder->add('values', ArrayElement::class);
+ $values->element(IntegerElement::class, TestElementTypeConfiguratorStruct::configureField(...));
+ $values->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestElementTypeConfiguratorStruct::class);
+ }
}
#[Generates(Struct::class)]
@@ -137,3 +199,20 @@ public function __construct(
public ?string $b = null,
) {}
}
+
+class TestElementTypeSimpleStruct
+{
+ #[ElementType(IntegerElement::class)]
+ public array $values;
+}
+
+class TestElementTypeConfiguratorStruct
+{
+ #[ElementType(IntegerElement::class, "configureField")]
+ public array $values;
+
+ public static function configureField(IntegerElementBuilder $builder): void
+ {
+ $builder->min(200);
+ }
+}
diff --git a/tests/Attribute/Aggregate/OptionalTest.php b/tests/Attribute/Aggregate/OptionalTest.php
new file mode 100644
index 0000000..a27867b
--- /dev/null
+++ b/tests/Attribute/Aggregate/OptionalTest.php
@@ -0,0 +1,156 @@
+submit([]);
+ $this->assertTrue($form->valid());
+ $this->assertNull($form->value()['inner']);
+
+ $form->submit(['inner' => ['id' => 42]]);
+ $this->assertTrue($form->valid());
+ $this->assertEquals(['id' => 42], $form->value()['inner']);
+ }
+
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestOptionalStruct::class, processor: $processor);
+
+ $form->submit(['values' => [
+ ['id' => 1],
+ ['id' => 2],
+ ]]);
+
+ $form->submit([]);
+ $this->assertTrue($form->valid());
+ $this->assertEquals(new TestOptionalStruct(null), $form->value());
+
+ $form->submit(['inner' => ['id' => 42]]);
+ $this->assertTrue($form->valid());
+
+ $this->assertEquals(new TestOptionalStruct(new InnerElementStruct2(42)), $form->value());
+ }
+
+ #[Test]
+ public function codeGeneration()
+ {
+ $form = new class extends AttributeForm {
+ #[Optional, Setter]
+ public InnerElementForm $inner;
+ };
+
+ $this->assertGenerated(<<<'PHP'
+ namespace Generated;
+
+ use Bdf\Form\Aggregate\FormBuilderInterface;
+ use Bdf\Form\Aggregate\FormInterface;
+ use Bdf\Form\Attribute\Aggregate\InnerElementForm;
+ use Bdf\Form\Attribute\AttributeForm;
+ use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+ use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+ use Bdf\Form\PropertyAccess\Setter;
+
+ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+ {
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $inner = $builder->add('inner', InnerElementForm::class);
+ $inner->optional();
+ $inner->hydrator(new Setter());
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->inner = $inner['inner']->element();
+ }
+ }
+
+ PHP, $form);
+ }
+
+ #[Test]
+ public function codeGenerationStruct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+ namespace Generated;
+
+ use Bdf\Form\Aggregate\FormBuilderInterface;
+ use Bdf\Form\Attribute\Aggregate\TestOptionalStruct;
+ use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+ use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+ use Bdf\Form\PropertyAccess\Getter;
+ use Bdf\Form\PropertyAccess\Setter;
+ use Bdf\Form\Struct\StructForm;
+
+ class GeneratedConfigurator implements AttributesProcessorInterface
+ {
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestOptionalStruct::class);
+
+ $inner = $builder->add('inner', StructForm::class);
+ $inner->optional();
+ $inner->hydrator(new Setter(null))->extractor(new Getter(null));
+ $inner->class('Bdf\Form\Attribute\Aggregate\InnerElementStruct2');
+
+ return null;
+ }
+ }
+
+ PHP, TestOptionalStruct::class);
+ }
+}
+
+class InnerElementForm extends AttributeForm
+{
+ #[GetSet]
+ public IntegerElement $id;
+}
+
+class InnerElementStruct2
+{
+ public function __construct(
+ public int $id,
+ ) {}
+}
+
+class TestOptionalStruct
+{
+ public function __construct(
+ #[Optional, GetSet]
+ public ?InnerElementStruct2 $inner,
+ ) {}
+}
diff --git a/tests/Attribute/Aggregate/StructElementTest.php b/tests/Attribute/Aggregate/StructElementTest.php
new file mode 100644
index 0000000..fec6bf9
--- /dev/null
+++ b/tests/Attribute/Aggregate/StructElementTest.php
@@ -0,0 +1,145 @@
+submit(['values' => [
+ ['id' => 1],
+ ['id' => 2],
+ ]]);
+
+ $this->assertTrue($form->valid());
+ $this->assertEquals([
+ new InnerElementStruct(1),
+ new InnerElementStruct(2),
+ ], $form->value()['values']);
+ }
+
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestStructElementStruct::class, processor: $processor);
+
+ $form->submit(['values' => [
+ ['id' => 1],
+ ['id' => 2],
+ ]]);
+
+ $this->assertTrue($form->valid());
+ $this->assertEquals(new TestStructElementStruct([
+ new InnerElementStruct(1),
+ new InnerElementStruct(2),
+ ]), $form->value());
+ }
+
+ #[Test]
+ public function codeGeneration()
+ {
+ $form = new class extends AttributeForm {
+ #[StructElement(InnerElementStruct::class), Setter]
+ 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 Bdf\Form\PropertyAccess\Setter;
+
+ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+ {
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $values = $builder->add('values', ArrayElement::class);
+ $values->struct('Bdf\Form\Attribute\Aggregate\InnerElementStruct');
+ $values->hydrator(new Setter());
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->values = $inner['values']->element();
+ }
+ }
+
+ PHP, $form);
+ }
+
+ #[Test]
+ public function codeGenerationStruct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+ namespace Generated;
+
+ use Bdf\Form\Aggregate\ArrayElement;
+ use Bdf\Form\Aggregate\FormBuilderInterface;
+ use Bdf\Form\Attribute\Aggregate\TestStructElementStruct;
+ use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+ use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+ use Bdf\Form\PropertyAccess\Setter;
+
+ class GeneratedConfigurator implements AttributesProcessorInterface
+ {
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestStructElementStruct::class);
+
+ $values = $builder->add('values', ArrayElement::class);
+ $values->struct('Bdf\Form\Attribute\Aggregate\InnerElementStruct');
+ $values->hydrator(new Setter());
+
+ return null;
+ }
+ }
+
+ PHP, TestStructElementStruct::class);
+ }
+}
+
+class InnerElementStruct
+{
+ public function __construct(
+ public int $id,
+ ) {}
+}
+
+class TestStructElementStruct
+{
+ public function __construct(
+ #[StructElement(InnerElementStruct::class), Setter]
+ public array $values,
+ ) {}
+}
diff --git a/tests/Attribute/Button/GroupsTest.php b/tests/Attribute/Button/GroupsTest.php
index 49dba86..8c4c1f1 100644
--- a/tests/Attribute/Button/GroupsTest.php
+++ b/tests/Attribute/Button/GroupsTest.php
@@ -53,7 +53,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$builder->submit('btn')
->groups(['foo', 'bar'])
diff --git a/tests/Attribute/Button/ValueTest.php b/tests/Attribute/Button/ValueTest.php
index 884d280..3a2b665 100644
--- a/tests/Attribute/Button/ValueTest.php
+++ b/tests/Attribute/Button/ValueTest.php
@@ -51,7 +51,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$builder->submit('button')
->value('foo')
diff --git a/tests/Attribute/Child/AsFilterTest.php b/tests/Attribute/Child/AsFilterTest.php
index bf02108..9947044 100644
--- a/tests/Attribute/Child/AsFilterTest.php
+++ b/tests/Attribute/Child/AsFilterTest.php
@@ -10,9 +10,13 @@
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\PropertyAccess\Getter;
use Bdf\Form\PropertyAccess\Setter;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
+use function base64_decode;
+
class AsFilterTest extends TestCase
{
#[DataProvider('provideAttributesProcessor')]
@@ -33,6 +37,15 @@ public function aFilter($value, Child $input, $default)
$this->assertEquals('foo', $form->a->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestAsFilterStruct::class, processor: $processor);
+
+ $form->submit(['foo' => 'Zm9v']);
+ $this->assertEquals('foo', $form->value()->foo);
+ }
+
/**
*
*/
@@ -67,15 +80,15 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
$foo->extractor(new Getter());
$foo->hydrator(new Setter());
- $foo->filter([$form, 'aFilter']);
+ $foo->filter($context->aFilter(...));
$bar = $builder->add('bar', StringElement::class);
- $bar->filter([$form, 'aFilter']);
+ $bar->filter($context->aFilter(...));
return $this;
}
@@ -94,4 +107,60 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ /**
+ *
+ */
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+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;
+use Tests\Form\Attribute\Child\TestAsFilterStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestAsFilterStruct::class);
+
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+ $foo->required(null);
+ $foo->filter(TestAsFilterStruct::aFilter(...));
+
+ $bar = $builder->add('bar', StringElement::class);
+ $bar->hydrator(new Setter(null))->extractor(new Getter(null));
+ $bar->required(null);
+ $bar->filter(TestAsFilterStruct::aFilter(...));
+
+ return null;
+ }
+}
+
+PHP
+ , TestAsFilterStruct::class
+);
+ }
+}
+
+class TestAsFilterStruct
+{
+ public string $foo;
+ public string $bar;
+
+ #[AsFilter('foo', 'bar')]
+ public static function aFilter($value, Child $input, $default)
+ {
+ return base64_decode($value);
+ }
}
diff --git a/tests/Attribute/Child/AsModelTransformerTest.php b/tests/Attribute/Child/AsModelTransformerTest.php
index a6cd85c..00e7e59 100644
--- a/tests/Attribute/Child/AsModelTransformerTest.php
+++ b/tests/Attribute/Child/AsModelTransformerTest.php
@@ -10,9 +10,14 @@
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\PropertyAccess\Getter;
use Bdf\Form\PropertyAccess\Setter;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
+use function base64_decode;
+use function base64_encode;
+
class AsModelTransformerTest extends TestCase
{
#[DataProvider('provideAttributesProcessor')]
@@ -36,6 +41,18 @@ public function aTransformer($value, StringElement $input, bool $toPhp)
$this->assertEquals('Hello World !', $form->a->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestAsModelTransformerStruct::class, processor: $processor);
+
+ $form->submit(['a' => 'foo']);
+ $this->assertEquals(new TestAsModelTransformerStruct(a: 'Zm9v'), $form->value());
+
+ $form->import(new TestAsModelTransformerStruct(a: 'SGVsbG8gV29ybGQgIQ=='));
+ $this->assertEquals('Hello World !', $form['a']->element()->value());
+ }
+
/**
*
*/
@@ -69,12 +86,12 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', IntegerElement::class);
$foo->extractor(new Getter());
$foo->hydrator(new Setter());
- $foo->modelTransformer([$form, 't']);
+ $foo->modelTransformer($context->t(...));
return $this;
}
@@ -92,4 +109,58 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ /**
+ *
+ */
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+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;
+use Tests\Form\Attribute\Child\TestAsModelTransformerStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestAsModelTransformerStruct::class);
+
+ $a = $builder->add('a', StringElement::class);
+ $a->extractor(new Getter());
+ $a->hydrator(new Setter());
+ $a->required(null);
+ $a->modelTransformer(TestAsModelTransformerStruct::aTransformer(...));
+
+ return null;
+ }
+}
+
+PHP
+ , TestAsModelTransformerStruct::class
+);
+ }
+}
+
+class TestAsModelTransformerStruct
+{
+ public function __construct(
+ #[Getter, Setter]
+ public string $a,
+ ) {}
+
+ #[AsModelTransformer('a')]
+ public static function aTransformer($value, StringElement $input, bool $toPhp)
+ {
+ return $toPhp ? base64_encode($value) : base64_decode($value);
+ }
}
diff --git a/tests/Attribute/Child/CallbackFilterTest.php b/tests/Attribute/Child/CallbackFilterTest.php
index 5fda5d1..064664a 100644
--- a/tests/Attribute/Child/CallbackFilterTest.php
+++ b/tests/Attribute/Child/CallbackFilterTest.php
@@ -9,9 +9,13 @@
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\PropertyAccess\Getter;
use Bdf\Form\PropertyAccess\Setter;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
+use function base64_decode;
+
class CallbackFilterTest extends TestCase
{
#[DataProvider('provideAttributesProcessor')]
@@ -31,6 +35,15 @@ public function aFilter($value, Child $input, $default)
$this->assertEquals('foo', $form->a->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestCallbackFilterStruct::class, processor: $processor);
+
+ $form->submit(['a' => 'Zm9v']);
+ $this->assertEquals('foo', $form->value()->a);
+ }
+
/**
*
*/
@@ -63,10 +76,10 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
- $foo->filter([$form, 'aFilter']);
+ $foo->filter($context->aFilter(...));
$foo->extractor(new Getter());
$foo->hydrator(new Setter());
@@ -86,4 +99,55 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ /**
+ *
+ */
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+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;
+use Tests\Form\Attribute\Child\TestCallbackFilterStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestCallbackFilterStruct::class);
+
+ $a = $builder->add('a', StringElement::class);
+ $a->filter(TestCallbackFilterStruct::aFilter(...));
+ $a->extractor(new Getter());
+ $a->hydrator(new Setter());
+ $a->required(null);
+
+ return null;
+ }
+}
+
+PHP
+ , TestCallbackFilterStruct::class
+);
+ }
+}
+
+class TestCallbackFilterStruct
+{
+ #[CallbackFilter('aFilter'), Getter, Setter]
+ public string $a;
+
+ public static function aFilter($value, Child $input, $default)
+ {
+ return base64_decode($value);
+ }
}
diff --git a/tests/Attribute/Child/CallbackModelTransformerTest.php b/tests/Attribute/Child/CallbackModelTransformerTest.php
index 2dd7d48..109daee 100644
--- a/tests/Attribute/Child/CallbackModelTransformerTest.php
+++ b/tests/Attribute/Child/CallbackModelTransformerTest.php
@@ -10,7 +10,9 @@
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\PropertyAccess\Getter;
use Bdf\Form\PropertyAccess\Setter;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class CallbackModelTransformerTest extends TestCase
@@ -48,6 +50,19 @@ public function bToInput($value, IntegerElement $input)
$this->assertEquals(10, $form->b->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestCallbackModelTransformerStruct::class, processor: $processor);
+
+ $form->submit(['a' => 'foo', 'b' => '15']);
+ $this->assertEquals(new TestCallbackModelTransformerStruct(a: 'Zm9v', b: 'f'), $form->value());
+
+ $form->import(new TestCallbackModelTransformerStruct(a: 'SGVsbG8gV29ybGQgIQ==', b: 'a'));
+ $this->assertEquals('Hello World !', $form['a']->element()->value());
+ $this->assertEquals(10, $form['b']->element()->value());
+ }
+
#[DataProvider('provideAttributesProcessor')]
public function test_with_only_one_transformation_method(AttributesProcessorInterface $processor)
{
@@ -74,6 +89,19 @@ public function t($value, $input)
$this->assertSame(6, $form->bar->value());
}
+ #[DataProvider('provideStructAttributesProcessor')]
+ public function test_with_only_one_transformation_method_struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestCallbackModelTransformerOneMethodStruct::class, processor: $processor);
+
+ $form->submit(['foo' => '5', 'bar' => '5']);
+ $this->assertEquals(new TestCallbackModelTransformerOneMethodStruct(foo: 6, bar: 5), $form->value());
+
+ $form->import(new TestCallbackModelTransformerOneMethodStruct(foo: 5, bar: 5));
+ $this->assertEquals(5, $form['foo']->element()->value());
+ $this->assertEquals(6, $form['bar']->element()->value());
+ }
+
/**
*
*/
@@ -110,10 +138,10 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', IntegerElement::class);
- $foo->modelTransformer(new class ($form) implements TransformerInterface {
+ $foo->modelTransformer(new class ($context) implements TransformerInterface {
/**
* {@inheritdoc}
*/
@@ -127,11 +155,11 @@ function transformToHttp(mixed $value, ElementInterface $input): mixed
*/
function transformFromHttp(mixed $value, ElementInterface $input): mixed
{
- return $this->form->t($value, $input);
+ return $this->context->t($value, $input);
}
public function __construct(
- private $form,
+ private $context,
) {
}
});
@@ -139,13 +167,13 @@ public function __construct(
$foo->hydrator(new Setter());
$bar = $builder->add('bar', IntegerElement::class);
- $bar->modelTransformer(new class ($form) implements TransformerInterface {
+ $bar->modelTransformer(new class ($context) implements TransformerInterface {
/**
* {@inheritdoc}
*/
function transformToHttp(mixed $value, ElementInterface $input): mixed
{
- return $this->form->t($value, $input);
+ return $this->context->t($value, $input);
}
/**
@@ -157,7 +185,7 @@ function transformFromHttp(mixed $value, ElementInterface $input): mixed
}
public function __construct(
- private $form,
+ private $context,
) {
}
});
@@ -181,4 +209,115 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ /**
+ *
+ */
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\ElementInterface;
+use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Bdf\Form\Transformer\TransformerInterface;
+use Tests\Form\Attribute\Child\TestCallbackModelTransformerStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestCallbackModelTransformerStruct::class);
+
+ $a = $builder->add('a', StringElement::class);
+ $a->modelTransformer(TestCallbackModelTransformerStruct::aTransformer(...));
+ $a->extractor(new Getter());
+ $a->hydrator(new Setter());
+ $a->required(null);
+
+ $b = $builder->add('b', StringElement::class);
+ $b->modelTransformer(new class ($context) implements TransformerInterface {
+ /**
+ * {@inheritdoc}
+ */
+ function transformToHttp(mixed $value, ElementInterface $input): mixed
+ {
+ return $this->context::bToInput($value, $input);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function transformFromHttp(mixed $value, ElementInterface $input): mixed
+ {
+ return $this->context::bToEntity($value, $input);
+ }
+
+ public function __construct(
+ private $context,
+ ) {
+ }
+ });
+ $b->extractor(new Getter());
+ $b->hydrator(new Setter());
+ $b->required(null);
+
+ return null;
+ }
+}
+
+PHP
+ , TestCallbackModelTransformerStruct::class
+);
+ }
+}
+
+class TestCallbackModelTransformerStruct
+{
+ public function __construct(
+ #[CallbackModelTransformer('aTransformer'), Getter, Setter]
+ public string $a,
+
+ #[CallbackModelTransformer(toEntity: 'bToEntity', toInput: 'bToInput'), Getter, Setter]
+ public string $b,
+ ) {}
+
+ public static function aTransformer($value, StringElement $input, bool $toPhp)
+ {
+ return $toPhp ? base64_encode($value) : base64_decode($value);
+ }
+
+ public static function bToEntity($value, StringElement $input)
+ {
+ return dechex($value);
+ }
+
+ public static function bToInput($value, StringElement $input)
+ {
+ return hexdec($value);
+ }
+}
+
+class TestCallbackModelTransformerOneMethodStruct
+{
+ public function __construct(
+ #[CallbackModelTransformer(toEntity: 't'), Getter, Setter]
+ public int $foo,
+
+ #[CallbackModelTransformer(toInput: 't'), Getter, Setter]
+ public int $bar,
+ ) {}
+
+ public static function t($value, $input)
+ {
+ return $value + 1;
+ }
}
diff --git a/tests/Attribute/Child/ConfigureTest.php b/tests/Attribute/Child/ConfigureTest.php
index 502f942..6375e89 100644
--- a/tests/Attribute/Child/ConfigureTest.php
+++ b/tests/Attribute/Child/ConfigureTest.php
@@ -8,7 +8,9 @@
use Bdf\Form\Child\ChildBuilderInterface;
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\Leaf\StringElementBuilder;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class ConfigureTest extends TestCase
@@ -37,6 +39,19 @@ public function configureFoo(ChildBuilderInterface $builder): void
$this->assertTrue($form->valid());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestConfigureOnPropertyStruct::class, processor: $processor);
+
+ $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());
+ }
+
#[DataProvider('provideAttributesProcessor')]
public function test_on_method(AttributesProcessorInterface $processor)
{
@@ -61,6 +76,19 @@ public function configureFoo(ChildBuilderInterface $builder): void
$this->assertTrue($form->valid());
}
+ #[DataProvider('provideStructAttributesProcessor')]
+ public function test_on_method_struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestConfigureOnMethodStruct::class, processor: $processor);
+
+ $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 {
@@ -91,10 +119,10 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
- $form->configureFoo($foo);
+ $context->configureFoo($foo);
return $this;
}
@@ -113,6 +141,42 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
);
}
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+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;
+use Tests\Form\Attribute\Child\TestConfigureOnPropertyStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestConfigureOnPropertyStruct::class);
+
+ $foo = $builder->add('foo', StringElement::class);
+ TestConfigureOnPropertyStruct::configureFoo($foo);
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+ $foo->required(null);
+
+ return null;
+ }
+}
+
+PHP
+ , TestConfigureOnPropertyStruct::class
+);
+ }
+
public function test_code_generator_on_method()
{
$form = new class extends AttributeForm {
@@ -143,10 +207,10 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
- $form->configureFoo($foo);
+ $context->configureFoo($foo);
return $this;
}
@@ -164,4 +228,68 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ public function test_code_generator_on_method_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+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;
+use Tests\Form\Attribute\Child\TestConfigureOnMethodStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestConfigureOnMethodStruct::class);
+
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+ $foo->required(null);
+ TestConfigureOnMethodStruct::configureFoo($foo);
+
+ return null;
+ }
+}
+
+PHP
+ , TestConfigureOnMethodStruct::class
+ );
+ }
+}
+
+class TestConfigureOnPropertyStruct
+{
+ #[Configure('configureFoo')]
+ public string $foo;
+
+ /**
+ * @param ChildBuilderInterface|StringElementBuilder $builder
+ */
+ public static function configureFoo(ChildBuilderInterface $builder): void
+ {
+ $builder->length(min: 3);
+ }
+}
+
+class TestConfigureOnMethodStruct
+{
+ public string $foo;
+
+ /**
+ * @param ChildBuilderInterface|StringElementBuilder $builder
+ */
+ #[Configure('foo')]
+ public static function configureFoo(ChildBuilderInterface $builder): void
+ {
+ $builder->length(min: 3);
+ }
}
diff --git a/tests/Attribute/Child/DefaultValueTest.php b/tests/Attribute/Child/DefaultValueTest.php
index 2bced97..e8fbede 100644
--- a/tests/Attribute/Child/DefaultValueTest.php
+++ b/tests/Attribute/Child/DefaultValueTest.php
@@ -13,7 +13,9 @@
use Bdf\Form\Leaf\IntegerElement;
use Bdf\Form\Leaf\IntegerElementBuilder;
use Bdf\Form\PropertyAccess\Setter;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class DefaultValueTest extends TestCase
@@ -30,6 +32,15 @@ public function test(AttributesProcessorInterface $processor)
$this->assertSame(42, $form->v->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestDefaultValueStruct::class, processor: $processor);
+
+ $form->submit([]);
+ $this->assertSame(42, $form['v']->element()->value());
+ }
+
/**
* @return void
*/
@@ -55,7 +66,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$v = $builder->add('v', IntegerElement::class);
$v->default(42);
@@ -75,4 +86,48 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
PHP
, $form);
}
+
+ /**
+ * @return void
+ */
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+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;
+use Tests\Form\Attribute\Child\TestDefaultValueStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestDefaultValueStruct::class);
+
+ $v = $builder->add('v', IntegerElement::class);
+ $v->default(42);
+ $v->hydrator(new Setter(null))->extractor(new Getter(null));
+ $v->required(null);
+
+ return null;
+ }
+}
+
+PHP
+ , TestDefaultValueStruct::class);
+ }
+}
+
+class TestDefaultValueStruct
+{
+ #[DefaultValue(42)]
+ public int $v;
}
diff --git a/tests/Attribute/Child/DependenciesTest.php b/tests/Attribute/Child/DependenciesTest.php
index 407010b..79ac869 100644
--- a/tests/Attribute/Child/DependenciesTest.php
+++ b/tests/Attribute/Child/DependenciesTest.php
@@ -15,7 +15,10 @@
use Bdf\Form\Leaf\IntegerElementBuilder;
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\PropertyAccess\Setter;
+use Bdf\Form\Struct\StructForm;
+use Bdf\Form\Util\FieldPath;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class DependenciesTest extends TestCase
@@ -39,6 +42,15 @@ public function bazTransformer($value)
$this->assertSame('acb', $form->baz->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestDependenciesStruct::class, processor: $processor);
+
+ $form->submit(['foo' => 'a', 'bar' => 'b', 'baz' => 'c']);
+ $this->assertSame('acb', $form->value()->baz);
+ }
+
/**
* @return void
*/
@@ -66,7 +78,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
@@ -92,4 +104,87 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
PHP
, $form);
}
+
+ /**
+ * @return void
+ */
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\ElementInterface;
+use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Bdf\Form\Transformer\TransformerInterface;
+use Tests\Form\Attribute\Child\TestDependenciesStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestDependenciesStruct::class);
+
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+ $foo->required(null);
+
+ $baz = $builder->add('baz', StringElement::class);
+ $baz->depends('foo', 'bar');
+ $baz->transformer(new class ($context) implements TransformerInterface {
+ /**
+ * {@inheritdoc}
+ */
+ function transformToHttp(mixed $value, ElementInterface $input): mixed
+ {
+ return $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function transformFromHttp(mixed $value, ElementInterface $input): mixed
+ {
+ return $this->context::bazTransformer($value, $input);
+ }
+
+ public function __construct(
+ private $context,
+ ) {
+ }
+ });
+ $baz->hydrator(new Setter(null))->extractor(new Getter(null));
+ $baz->required(null);
+
+ $bar = $builder->add('bar', StringElement::class);
+ $bar->hydrator(new Setter(null))->extractor(new Getter(null));
+ $bar->required(null);
+
+ return null;
+ }
+}
+
+PHP
+ , TestDependenciesStruct::class);
+ }
+}
+
+class TestDependenciesStruct
+{
+ public string $foo;
+ #[Dependencies('foo', 'bar'), CallbackTransformer(fromHttp: 'bazTransformer')]
+ public string $baz;
+ public string $bar;
+
+ public static function bazTransformer($value, $input)
+ {
+ return FieldPath::parse('../foo')->value($input) . $value . FieldPath::parse('../bar')->value($input);
+ }
}
diff --git a/tests/Attribute/Child/GetSetTest.php b/tests/Attribute/Child/GetSetTest.php
index 68b106b..0a1fcd0 100644
--- a/tests/Attribute/Child/GetSetTest.php
+++ b/tests/Attribute/Child/GetSetTest.php
@@ -14,7 +14,9 @@
use Bdf\Form\Leaf\IntegerElementBuilder;
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\PropertyAccess\Setter;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class GetSetTest extends TestCase
@@ -38,6 +40,19 @@ public function test(AttributesProcessorInterface $processor)
$this->assertSame('ccc', $form->b->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestGetSetStruct::class, processor: $processor);
+
+ $form->submit(['a' => 'z', 'b' => 'e']);
+ $this->assertEquals(new TestGetSetStruct(a: 'z', b: 'e'), $form->value());
+
+ $form->import(new TestGetSetStruct(a: 'aaa', b: 'ccc'));
+ $this->assertSame('aaa', $form['a']->element()->value());
+ $this->assertSame('ccc', $form['b']->element()->value());
+ }
+
/**
* @return void
*/
@@ -65,7 +80,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$b = $builder->add('b', StringElement::class);
$b->hydrator(new Setter('c'))->extractor(new Getter('c'));
@@ -85,4 +100,56 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
PHP
, $form);
}
+
+ /**
+ * @return void
+ */
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+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;
+use Tests\Form\Attribute\Child\TestGetSetStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestGetSetStruct::class);
+
+ $a = $builder->add('a', StringElement::class);
+ $a->hydrator(new Setter(null))->extractor(new Getter(null));
+ $a->required(null);
+
+ $b = $builder->add('b', StringElement::class);
+ $b->hydrator(new Setter('b'))->extractor(new Getter('b'));
+ $b->required(null);
+
+ return null;
+ }
+}
+
+PHP
+ , TestGetSetStruct::class);
+ }
+}
+
+class TestGetSetStruct
+{
+ public function __construct(
+ #[GetSet]
+ public string $a,
+
+ #[GetSet('b')]
+ public string $b,
+ ) {}
}
diff --git a/tests/Attribute/Child/HttpFieldTest.php b/tests/Attribute/Child/HttpFieldTest.php
index 2a3884a..1980aa0 100644
--- a/tests/Attribute/Child/HttpFieldTest.php
+++ b/tests/Attribute/Child/HttpFieldTest.php
@@ -14,7 +14,9 @@
use Bdf\Form\Leaf\IntegerElement;
use Bdf\Form\Leaf\IntegerElementBuilder;
use Bdf\Form\PropertyAccess\Setter;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class HttpFieldTest extends TestCase
@@ -32,6 +34,16 @@ public function test(AttributesProcessorInterface $processor)
$this->assertSame(['_v_' => '42'], $form->httpValue());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestHttpFieldStruct::class, processor: $processor);
+
+ $form->submit(['_v_' => 42]);
+ $this->assertSame(42, $form->value()->v);
+ $this->assertSame(['_v_' => '42'], $form->httpValue());
+ }
+
/**
* @return void
*/
@@ -58,7 +70,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$v = $builder->add('v', IntegerElement::class);
$v->httpFields(new ArrayOffsetHttpFields('_v_'));
@@ -78,4 +90,49 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
PHP
, $form);
}
+
+ /**
+ * @return void
+ */
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Child\Http\ArrayOffsetHttpFields;
+use Bdf\Form\Leaf\IntegerElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Tests\Form\Attribute\Child\TestHttpFieldStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestHttpFieldStruct::class);
+
+ $v = $builder->add('v', IntegerElement::class);
+ $v->httpFields(new ArrayOffsetHttpFields('_v_'));
+ $v->hydrator(new Setter(null))->extractor(new Getter(null));
+ $v->required(null);
+
+ return null;
+ }
+}
+
+PHP
+ , TestHttpFieldStruct::class);
+ }
+}
+
+class TestHttpFieldStruct
+{
+ #[HttpField('_v_')]
+ public int $v;
}
diff --git a/tests/Attribute/Child/ModelTransformerTest.php b/tests/Attribute/Child/ModelTransformerTest.php
index 1917aff..e423141 100644
--- a/tests/Attribute/Child/ModelTransformerTest.php
+++ b/tests/Attribute/Child/ModelTransformerTest.php
@@ -12,8 +12,10 @@
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\PropertyAccess\Getter;
use Bdf\Form\PropertyAccess\Setter;
+use Bdf\Form\Struct\StructForm;
use Bdf\Form\Transformer\TransformerInterface;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class ModelTransformerTest extends TestCase
@@ -45,6 +47,25 @@ public function test(AttributesProcessorInterface $processor)
$this->assertEquals('abc', $form->c->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestModelTransformerStruct::class, processor: $processor);
+
+ $form->submit(['a' => 'foo', 'b' => '15']);
+ $this->assertEquals(new TestModelTransformerStruct(a: 'Zm9v', b: 'f', c: 'foo_'), $form->value());
+
+ $form->import(new TestModelTransformerStruct(a: 'SGVsbG8gV29ybGQgIQ==', b: 'a'));
+ $this->assertEquals('Hello World !', $form['a']->element()->value());
+ $this->assertEquals(10, $form['b']->element()->value());
+
+ $form->submit(['c' => 'bar']);
+ $this->assertEquals('foo_bar', $form->value()->c);
+
+ $form->import(new TestModelTransformerStruct(c: 'foo_abc'));
+ $this->assertEquals('abc', $form['c']->element()->value());
+ }
+
public function test_code_generator()
{
$form = new #[Generates(Struct::class)] class extends AttributeForm {
@@ -79,7 +100,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$builder->generates(Struct::class);
@@ -113,6 +134,54 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
PHP
, $form
+);
+ }
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+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;
+use Tests\Form\Attribute\Child\ATransformer;
+use Tests\Form\Attribute\Child\BTransformer;
+use Tests\Form\Attribute\Child\TestModelTransformerStruct;
+use Tests\Form\Attribute\Child\TransformerWithArguments;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestModelTransformerStruct::class);
+
+ $a = $builder->add('a', StringElement::class);
+ $a->modelTransformer(new ATransformer());
+ $a->extractor(new Getter());
+ $a->hydrator(new Setter());
+
+ $b = $builder->add('b', StringElement::class);
+ $b->modelTransformer(new BTransformer());
+ $b->extractor(new Getter());
+ $b->hydrator(new Setter());
+
+ $c = $builder->add('c', StringElement::class);
+ $c->modelTransformer(new TransformerWithArguments('foo_'));
+ $c->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestModelTransformerStruct::class
);
}
}
@@ -126,6 +195,20 @@ public function __construct(
) {}
}
+class TestModelTransformerStruct
+{
+ public function __construct(
+ #[ModelTransformer(ATransformer::class), Getter, Setter]
+ public ?string $a = null,
+
+ #[ModelTransformer(BTransformer::class), Getter, Setter]
+ public ?string $b = null,
+
+ #[ModelTransformer(TransformerWithArguments::class, ['foo_']), GetSet]
+ public ?string $c = null,
+ ) {}
+}
+
class ATransformer implements TransformerInterface
{
public function transformToHttp($value, ElementInterface $input)
diff --git a/tests/Attribute/Constraint/AsConstraintTest.php b/tests/Attribute/Constraint/AsConstraintTest.php
index 389e8a9..84311e2 100644
--- a/tests/Attribute/Constraint/AsConstraintTest.php
+++ b/tests/Attribute/Constraint/AsConstraintTest.php
@@ -6,9 +6,13 @@
use Bdf\Form\Attribute\Constraint\AsConstraint;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
+use function strlen;
+
class AsConstraintTest extends TestCase
{
#[DataProvider('provideAttributesProcessor')]
@@ -47,6 +51,32 @@ public function validateFoo($value): bool
$this->assertNull($form->bar->error()->global());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestAsConstraintStruct::class, processor: $processor);
+
+ $form->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 {
@@ -75,10 +105,10 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
- $foo->satisfy(new ClosureConstraint($form->validateFoo(...), 'Foo length must be a multiple of 2'));
+ $foo->satisfy(new ClosureConstraint($context->validateFoo(...), 'Foo length must be a multiple of 2'));
return $this;
}
@@ -96,4 +126,57 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Constraint\Closure as ClosureConstraint;
+use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Tests\Form\Attribute\Constraint\TestAsConstraintStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestAsConstraintStruct::class);
+
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+ $foo->satisfy(new ClosureConstraint(TestAsConstraintStruct::validateFoo(...), 'Foo length must be a multiple of 2'));
+
+ $bar = $builder->add('bar', StringElement::class);
+ $bar->hydrator(new Setter(null))->extractor(new Getter(null));
+ $bar->satisfy(new ClosureConstraint(TestAsConstraintStruct::validateFoo(...)));
+
+ return null;
+ }
+}
+
+PHP
+ , TestAsConstraintStruct::class
+);
+ }
+}
+
+class TestAsConstraintStruct
+{
+ public ?string $foo;
+ public ?string $bar;
+
+ #[AsConstraint('foo', message: 'Foo length must be a multiple of 2')]
+ #[AsConstraint('bar')]
+ public static function validateFoo($value): bool
+ {
+ return strlen((string) $value) % 2 === 0;
+ }
}
diff --git a/tests/Attribute/Constraint/CallbackConstraintTest.php b/tests/Attribute/Constraint/CallbackConstraintTest.php
index ab44e4c..f96fc3c 100644
--- a/tests/Attribute/Constraint/CallbackConstraintTest.php
+++ b/tests/Attribute/Constraint/CallbackConstraintTest.php
@@ -6,7 +6,9 @@
use Bdf\Form\Attribute\Constraint\CallbackConstraint;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class CallbackConstraintTest extends TestCase
@@ -48,6 +50,32 @@ public function validateFoo($value): bool
$this->assertNull($form->bar->error()->global());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestCallbackConstraintStruct::class, processor: $processor);
+
+ $form->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 {
@@ -76,10 +104,10 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
- $foo->satisfy(new ClosureConstraint($form->validateFoo(...), 'Foo length must be a multiple of 2'));
+ $foo->satisfy(new ClosureConstraint($context->validateFoo(...), 'Foo length must be a multiple of 2'));
return $this;
}
@@ -97,4 +125,58 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Constraint\Closure as ClosureConstraint;
+use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Tests\Form\Attribute\Constraint\TestCallbackConstraintStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestCallbackConstraintStruct::class);
+
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->satisfy(new ClosureConstraint(TestCallbackConstraintStruct::validateFoo(...), 'Foo length must be a multiple of 2'));
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ $bar = $builder->add('bar', StringElement::class);
+ $bar->satisfy(new ClosureConstraint(TestCallbackConstraintStruct::validateFoo(...)));
+ $bar->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestCallbackConstraintStruct::class
+);
+ }
+}
+
+class TestCallbackConstraintStruct
+{
+ #[CallbackConstraint('validateFoo', message: 'Foo length must be a multiple of 2')]
+ public ?string $foo;
+
+ #[CallbackConstraint('validateFoo')]
+ public ?string $bar;
+
+ public static function validateFoo($value): bool
+ {
+ return strlen((string) $value) % 2 === 0;
+ }
}
diff --git a/tests/Attribute/Constraint/SatisfyTest.php b/tests/Attribute/Constraint/SatisfyTest.php
index 66255fa..dbea5f7 100644
--- a/tests/Attribute/Constraint/SatisfyTest.php
+++ b/tests/Attribute/Constraint/SatisfyTest.php
@@ -6,7 +6,9 @@
use Bdf\Form\Attribute\Constraint\Satisfy;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
use Symfony\Component\Validator\Constraints\Length;
@@ -28,6 +30,19 @@ public function test(AttributesProcessorInterface $processor)
$this->assertTrue($form->valid());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestSatisfyStruct::class, processor: $processor);
+
+ $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 {
@@ -51,7 +66,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
$foo->satisfy(new Length(min: 3, groups: ['Default']));
@@ -72,4 +87,46 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+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;
+use Symfony\Component\Validator\Constraints\Length;
+use Tests\Form\Attribute\Constraint\TestSatisfyStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestSatisfyStruct::class);
+
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->satisfy(new Length(min: 3, groups: ['Default']));
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestSatisfyStruct::class
+);
+ }
+}
+
+class TestSatisfyStruct
+{
+ #[Satisfy(new Length(min: 3))]
+ public ?string $foo;
}
diff --git a/tests/Attribute/Element/AsTransformerTest.php b/tests/Attribute/Element/AsTransformerTest.php
index 3dfb156..bc0c1e8 100644
--- a/tests/Attribute/Element/AsTransformerTest.php
+++ b/tests/Attribute/Element/AsTransformerTest.php
@@ -6,9 +6,13 @@
use Bdf\Form\Attribute\Element\AsTransformer;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
+use function json_encode;
+
class AsTransformerTest extends TestCase
{
#[DataProvider('provideAttributesProcessor')]
@@ -33,6 +37,19 @@ public function fooTransformer($value, StringElement $input, bool $toPhp)
$this->assertEquals('["[\"a\",true]",false]', $view['foo']->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestAsTransformerStruct::class, processor: $processor);
+
+ $form->submit(['foo' => 'a']);
+
+ $this->assertEquals('["a",true]', $form->value()->foo);
+
+ $view = $form->view();
+
+ $this->assertEquals('["[\"a\",true]",false]', $view['foo']->value());
+ }
public function test_code_generator()
{
@@ -61,10 +78,10 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
- $foo->transformer([$form, 'fooTransformer']);
+ $foo->transformer($context->fooTransformer(...));
return $this;
}
@@ -81,4 +98,49 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
PHP
, $form);
}
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+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;
+use Tests\Form\Attribute\Element\TestAsTransformerStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestAsTransformerStruct::class);
+
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+ $foo->transformer(TestAsTransformerStruct::fooTransformer(...));
+
+ return null;
+ }
+}
+
+PHP
+ , TestAsTransformerStruct::class);
+ }
+}
+
+class TestAsTransformerStruct
+{
+ public ?string $foo;
+
+ #[AsTransformer('foo')]
+ public static function fooTransformer($value, StringElement $input, bool $toPhp)
+ {
+ return json_encode([$value, $toPhp]);
+ }
}
diff --git a/tests/Attribute/Element/CallbackTransformerTest.php b/tests/Attribute/Element/CallbackTransformerTest.php
index 04c5c12..3e52844 100644
--- a/tests/Attribute/Element/CallbackTransformerTest.php
+++ b/tests/Attribute/Element/CallbackTransformerTest.php
@@ -8,7 +8,9 @@
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Leaf\IntegerElement;
use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class CallbackTransformerTest extends TestCase
@@ -50,6 +52,21 @@ public function outTransformer($value, StringElement $input)
$this->assertEquals('["out","[\"in\",\"b\"]"]', $view['bar']->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestCallbackTransformerStruct::class, processor: $processor);
+
+ $form->submit(['foo' => 'a', 'bar' => 'b']);
+
+ $this->assertEquals('["a",true]', $form->value()->foo);
+ $this->assertEquals('["in","b"]', $form->value()->bar);
+
+ $view = $form->view();
+
+ $this->assertEquals('["[\"a\",true]",false]', $view['foo']->value());
+ $this->assertEquals('["out","[\"in\",\"b\"]"]', $view['bar']->value());
+ }
#[DataProvider('provideAttributesProcessor')]
public function test_with_only_one_transformation_method(AttributesProcessorInterface $processor)
@@ -120,19 +137,19 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
- $foo->transformer([$form, 'fooTransformer']);
+ $foo->transformer($context->fooTransformer(...));
$bar = $builder->add('bar', StringElement::class);
- $bar->transformer(new class ($form) implements TransformerInterface {
+ $bar->transformer(new class ($context) implements TransformerInterface {
/**
* {@inheritdoc}
*/
function transformToHttp(mixed $value, ElementInterface $input): mixed
{
- return $this->form->outTransformer($value, $input);
+ return $this->context->outTransformer($value, $input);
}
/**
@@ -140,11 +157,11 @@ function transformToHttp(mixed $value, ElementInterface $input): mixed
*/
function transformFromHttp(mixed $value, ElementInterface $input): mixed
{
- return $this->form->inTransformer($value, $input);
+ return $this->context->inTransformer($value, $input);
}
public function __construct(
- private $form,
+ private $context,
) {
}
});
@@ -165,4 +182,89 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
PHP
, $form);
}
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\ElementInterface;
+use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Bdf\Form\Transformer\TransformerInterface;
+use Tests\Form\Attribute\Element\TestCallbackTransformerStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestCallbackTransformerStruct::class);
+
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->transformer(TestCallbackTransformerStruct::fooTransformer(...));
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ $bar = $builder->add('bar', StringElement::class);
+ $bar->transformer(new class ($context) implements TransformerInterface {
+ /**
+ * {@inheritdoc}
+ */
+ function transformToHttp(mixed $value, ElementInterface $input): mixed
+ {
+ return $this->context::outTransformer($value, $input);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function transformFromHttp(mixed $value, ElementInterface $input): mixed
+ {
+ return $this->context::inTransformer($value, $input);
+ }
+
+ public function __construct(
+ private $context,
+ ) {
+ }
+ });
+ $bar->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestCallbackTransformerStruct::class);
+ }
+}
+
+class TestCallbackTransformerStruct
+{
+ #[CallbackTransformer('fooTransformer')]
+ public ?string $foo;
+
+ #[CallbackTransformer(fromHttp: 'inTransformer', toHttp: 'outTransformer')]
+ public ?string $bar;
+
+ public static function fooTransformer($value, StringElement $input, bool $toPhp)
+ {
+ return json_encode([$value, $toPhp]);
+ }
+
+ public static function inTransformer($value, StringElement $input)
+ {
+ return json_encode(['in', $value]);
+ }
+
+ public static function outTransformer($value, StringElement $input)
+ {
+ return json_encode(['out', $value]);
+ }
}
diff --git a/tests/Attribute/Element/ChoicesTest.php b/tests/Attribute/Element/ChoicesTest.php
index aa393a7..5861e97 100644
--- a/tests/Attribute/Element/ChoicesTest.php
+++ b/tests/Attribute/Element/ChoicesTest.php
@@ -8,7 +8,9 @@
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Choice\ArrayChoice;
use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class ChoicesTest extends TestCase
@@ -46,6 +48,25 @@ public function generateChoices()
$this->assertEquals(['bar' => 'You must select at least 2 choices.'], $form->error()->toArray());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestChoicesStruct::class, processor: $processor);
+
+ $form->submit(['foo' => 'a', 'bar' => ['b'], 'baz' => 'c']);
+
+ $this->assertEquals(new ArrayChoice(['foo', 'bar']), $form['foo']->element()->choices());
+ $this->assertEquals(new ArrayChoice(['foo', 'bar', 'baz']), $form['bar']->element()->choices());
+ $this->assertEquals(['aaa', 'bbb', 'ccc'], $form['baz']->element()->choices()->values());
+ $this->assertEquals(['foo' => 'my error', 'bar' => 'my error', 'baz' => 'The value you selected is not a valid choice.'], $form->error()->toArray());
+
+ $form->submit(['foo' => 'bar', 'bar' => ['bar', 'foo'], 'baz' => 'ccc']);
+ $this->assertTrue($form->valid());
+
+ $form->submit(['foo' => 'bar', 'bar' => ['bar'], 'baz' => 'ccc']);
+ $this->assertEquals(['bar' => 'You must select at least 2 choices.'], $form->error()->toArray());
+ }
+
public function test_code_generator()
{
$form = new class extends AttributeForm {
@@ -81,7 +102,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
$foo->choices(['foo', 'bar'], message: 'my error');
@@ -90,7 +111,7 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?
$bar->choices(['foo', 'bar', 'baz'], min: 2, message: 'my error');
$baz = $builder->add('baz', StringElement::class);
- $baz->choices(new LazyChoice($form->generateChoices(...)), );
+ $baz->choices(new LazyChoice($context->generateChoices(...)), );
return $this;
}
@@ -110,4 +131,66 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Choice\LazyChoice;
+use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Tests\Form\Attribute\Element\TestChoicesStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestChoicesStruct::class);
+
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->choices(['foo', 'bar'], message: 'my error');
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ $bar = $builder->add('bar', ArrayElement::class);
+ $bar->choices(['foo', 'bar', 'baz'], min: 2, message: 'my error');
+ $bar->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ $baz = $builder->add('baz', StringElement::class);
+ $baz->choices(new LazyChoice(TestChoicesStruct::generateChoices(...)), );
+ $baz->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestChoicesStruct::class
+);
+ }
+}
+
+class TestChoicesStruct
+{
+ #[Choices(choices: ['foo', 'bar'], message: 'my error')]
+ public ?string $foo;
+
+ #[Choices(choices: ['foo', 'bar', 'baz'], message: 'my error', options: ['min' => 2])]
+ public array $bar;
+
+ #[Choices('generateChoices')]
+ public ?string $baz;
+
+ public static function generateChoices()
+ {
+ return ['aaa', 'bbb', 'ccc'];
+ }
}
diff --git a/tests/Attribute/Element/Date/AfterFieldTest.php b/tests/Attribute/Element/Date/AfterFieldTest.php
index 6db6219..86a55cc 100644
--- a/tests/Attribute/Element/Date/AfterFieldTest.php
+++ b/tests/Attribute/Element/Date/AfterFieldTest.php
@@ -7,7 +7,10 @@
use Bdf\Form\Attribute\Element\Date\AfterField;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Leaf\Date\DateTimeElement;
+use Bdf\Form\Struct\StructForm;
+use DateTime;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class AfterFieldTest extends TestCase
@@ -39,7 +42,28 @@ public function test(AttributesProcessorInterface $processor)
], self::normalizeSpace($form->error()->toArray()));
}
-
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestAfterFieldStruct::class, processor: $processor);
+
+ $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:50Z',
+ ]);
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'foo' => 'my error',
+ ], self::normalizeSpace($form->error()->toArray()));
+ }
+
#[DataProvider('provideAttributesProcessor')]
public function test_with_message(AttributesProcessorInterface $processor)
{
@@ -118,7 +142,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', DateTimeElement::class);
$foo->depends('bar');
@@ -144,6 +168,45 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
);
}
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\Date\DateTimeElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Tests\Form\Attribute\Element\Date\TestAfterFieldStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestAfterFieldStruct::class);
+
+ $foo = $builder->add('foo', DateTimeElement::class);
+ $foo->depends('bar');
+ $foo->afterField('bar', 'my error', true);
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ $bar = $builder->add('bar', DateTimeElement::class);
+ $bar->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestAfterFieldStruct::class
+ );
+ }
+
public static function normalizeSpace(array|string $value): array|string
{
if (\is_array($value)) {
@@ -153,3 +216,10 @@ public static function normalizeSpace(array|string $value): array|string
return \preg_replace('/\p{Zs}+/u', ' ', $value);
}
}
+
+class TestAfterFieldStruct
+{
+ #[Dependencies('bar'), AfterField('bar', 'my error', true)]
+ public ?DateTime $foo;
+ public ?DateTime $bar;
+}
diff --git a/tests/Attribute/Element/Date/BeforeFieldTest.php b/tests/Attribute/Element/Date/BeforeFieldTest.php
index d56ba6d..4b16db7 100644
--- a/tests/Attribute/Element/Date/BeforeFieldTest.php
+++ b/tests/Attribute/Element/Date/BeforeFieldTest.php
@@ -8,7 +8,10 @@
use Bdf\Form\Attribute\Element\Date\DateFormat;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Leaf\Date\DateTimeElement;
+use Bdf\Form\Struct\StructForm;
+use DateTime;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class BeforeFieldTest extends TestCase
@@ -40,7 +43,28 @@ public function test(AttributesProcessorInterface $processor)
], self::normalizeSpace($form->error()->toArray()));
}
-
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestBeforeFieldStruct::class, processor: $processor);
+
+ $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',
+ ], self::normalizeSpace($form->error()->toArray()));
+ }
+
#[DataProvider('provideAttributesProcessor')]
public function test_with_message(AttributesProcessorInterface $processor)
{
@@ -119,7 +143,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', DateTimeElement::class);
$foo->depends('bar');
@@ -145,6 +169,45 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
);
}
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\Date\DateTimeElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Tests\Form\Attribute\Element\Date\TestBeforeFieldStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestBeforeFieldStruct::class);
+
+ $foo = $builder->add('foo', DateTimeElement::class);
+ $foo->depends('bar');
+ $foo->beforeField('bar', 'my error', true);
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ $bar = $builder->add('bar', DateTimeElement::class);
+ $bar->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestBeforeFieldStruct::class
+ );
+ }
+
public static function normalizeSpace(array|string $value): array|string
{
if (\is_array($value)) {
@@ -154,3 +217,11 @@ public static function normalizeSpace(array|string $value): array|string
return \preg_replace('/\p{Zs}+/u', ' ', $value);
}
}
+
+class TestBeforeFieldStruct
+{
+ #[Dependencies('bar'), BeforeField('bar', 'my error', true)]
+ public ?DateTime $foo;
+ public ?DateTime $bar;
+}
+
diff --git a/tests/Attribute/Element/Date/DateFormatTest.php b/tests/Attribute/Element/Date/DateFormatTest.php
index 6a0d565..d52f0f4 100644
--- a/tests/Attribute/Element/Date/DateFormatTest.php
+++ b/tests/Attribute/Element/Date/DateFormatTest.php
@@ -6,7 +6,10 @@
use Bdf\Form\Attribute\Element\Date\DateFormat;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Leaf\Date\DateTimeElement;
+use Bdf\Form\Struct\StructForm;
+use DateTime;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class DateFormatTest extends TestCase
@@ -24,6 +27,16 @@ public function test(AttributesProcessorInterface $processor)
$this->assertEquals(new MyCustomDate('2020-11-02T15:21:00'), $form->foo->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestDateFormatStruct::class, processor: $processor);
+
+ $form->submit(['foo' => '02/11/2020 15:21']);
+
+ $this->assertEquals(new DateTime('2020-11-02T15:21:00'), $form->value()->foo);
+ }
+
public function test_code_generator()
{
$form = new class extends AttributeForm {
@@ -46,7 +59,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', DateTimeElement::class);
$foo->format('d/m/Y H:i');
@@ -67,4 +80,44 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\Date\DateTimeElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Tests\Form\Attribute\Element\Date\TestDateFormatStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestDateFormatStruct::class);
+
+ $foo = $builder->add('foo', DateTimeElement::class);
+ $foo->format('d/m/Y H:i');
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestDateFormatStruct::class
+ );
+ }
+}
+class TestDateFormatStruct
+{
+ #[DateFormat('d/m/Y H:i')]
+ public ?DateTime $foo;
}
diff --git a/tests/Attribute/Element/Date/DateTimeClassTest.php b/tests/Attribute/Element/Date/DateTimeClassTest.php
index 0f8d6c9..3e94a70 100644
--- a/tests/Attribute/Element/Date/DateTimeClassTest.php
+++ b/tests/Attribute/Element/Date/DateTimeClassTest.php
@@ -8,7 +8,10 @@
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Leaf\Date\DateTimeElement;
use Bdf\Form\Leaf\FloatElement;
+use Bdf\Form\Struct\StructForm;
+use DateTime;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class DateTimeClassTest extends TestCase
@@ -27,6 +30,17 @@ public function test(AttributesProcessorInterface $processor)
$this->assertInstanceOf(MyCustomDate::class, $form->foo->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestDateTimeStruct::class, processor: $processor);
+
+ $form->submit(['foo' => '2020-11-02T15:21:31+0100']);
+
+ $this->assertEquals(new MyCustomDate('2020-11-02T15:21:31'), $form->value()->foo);
+ $this->assertInstanceOf(MyCustomDate::class, $form->value()->foo);
+ }
+
public function test_code_generator()
{
$form = new class extends AttributeForm {
@@ -50,7 +64,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', DateTimeElement::class);
$foo->className(MyCustomDate::class);
@@ -71,8 +85,50 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\Date\DateTimeElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Tests\Form\Attribute\Element\Date\MyCustomDate;
+use Tests\Form\Attribute\Element\Date\TestDateTimeStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestDateTimeStruct::class);
+
+ $foo = $builder->add('foo', DateTimeElement::class);
+ $foo->className(MyCustomDate::class);
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestDateTimeStruct::class
+ );
+ }
}
class MyCustomDate extends \DateTime
{
}
+
+class TestDateTimeStruct
+{
+ #[DateTimeClass(MyCustomDate::class)]
+ public ?DateTime $foo;
+}
diff --git a/tests/Attribute/Element/Date/ImmtableDateTimeTest.php b/tests/Attribute/Element/Date/ImmtableDateTimeTest.php
index 09b3b58..554aa6d 100644
--- a/tests/Attribute/Element/Date/ImmtableDateTimeTest.php
+++ b/tests/Attribute/Element/Date/ImmtableDateTimeTest.php
@@ -51,7 +51,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', DateTimeElement::class);
$foo->immutable();
diff --git a/tests/Attribute/Element/Date/TimezoneTest.php b/tests/Attribute/Element/Date/TimezoneTest.php
index 18b0af0..366a167 100644
--- a/tests/Attribute/Element/Date/TimezoneTest.php
+++ b/tests/Attribute/Element/Date/TimezoneTest.php
@@ -7,8 +7,10 @@
use Bdf\Form\Attribute\Element\Date\Timezone;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Leaf\Date\DateTimeElement;
+use Bdf\Form\Struct\StructForm;
use DateTime;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class TimezoneTest extends TestCase
@@ -28,6 +30,18 @@ public function test(AttributesProcessorInterface $processor)
$this->assertEquals(5 * 3600, $form->foo->value()->getOffset());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestTimezoneStruct::class, processor: $processor);
+
+ $form->submit(['foo' => '2020-11-02T15:21:00+0200']);
+
+ $this->assertEquals(new DateTime('2020-11-02T18:21:00+0500'), $form->value()->foo);
+ $this->assertEquals(new \DateTimeZone('+0500'), $form->value()->foo->getTimezone());
+ $this->assertEquals(5 * 3600, $form->value()->foo->getOffset());
+ }
+
public function test_code_generator()
{
$form = new class extends AttributeForm {
@@ -50,7 +64,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', DateTimeElement::class);
$foo->timezone('+0500');
@@ -71,4 +85,45 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\Date\DateTimeElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Tests\Form\Attribute\Element\Date\TestTimezoneStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestTimezoneStruct::class);
+
+ $foo = $builder->add('foo', DateTimeElement::class);
+ $foo->timezone('+0500');
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestTimezoneStruct::class
+ );
+ }
+}
+
+class TestTimezoneStruct
+{
+ #[Timezone('+0500')]
+ public ?DateTime $foo;
}
diff --git a/tests/Attribute/Element/IgnoreTransformerExceptionTest.php b/tests/Attribute/Element/IgnoreTransformerExceptionTest.php
index 24a1e54..995e04c 100644
--- a/tests/Attribute/Element/IgnoreTransformerExceptionTest.php
+++ b/tests/Attribute/Element/IgnoreTransformerExceptionTest.php
@@ -7,7 +7,9 @@
use Bdf\Form\Attribute\Element\IgnoreTransformerException;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class IgnoreTransformerExceptionTest extends TestCase
@@ -34,6 +36,17 @@ public function transform()
$this->assertEquals(['bar' => 'My error'], $form->error()->toArray());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestIgnoreTransformerExceptionStruct::class, processor: $processor);
+
+ $form->submit(['foo' => 'a', 'bar' => 'b']);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals(['bar' => 'My error'], $form->error()->toArray());
+ }
+
public function test_code_generator()
{
$form = new class extends AttributeForm {
@@ -64,15 +77,15 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
$foo->ignoreTransformerException(true);
- $foo->transformer([$form, 'transform']);
+ $foo->transformer($context->transform(...));
$bar = $builder->add('bar', StringElement::class);
$bar->ignoreTransformerException(false);
- $bar->transformer([$form, 'transform']);
+ $bar->transformer($context->transform(...));
return $this;
}
@@ -91,4 +104,59 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+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;
+use Tests\Form\Attribute\Element\TestIgnoreTransformerExceptionStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestIgnoreTransformerExceptionStruct::class);
+
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->ignoreTransformerException(true);
+ $foo->transformer(TestIgnoreTransformerExceptionStruct::transform(...));
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ $bar = $builder->add('bar', StringElement::class);
+ $bar->ignoreTransformerException(false);
+ $bar->transformer(TestIgnoreTransformerExceptionStruct::transform(...));
+ $bar->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestIgnoreTransformerExceptionStruct::class
+);
+ }
+}
+
+class TestIgnoreTransformerExceptionStruct
+{
+ #[IgnoreTransformerException, CallbackTransformer('transform')]
+ public ?string $foo;
+
+ #[IgnoreTransformerException(false), CallbackTransformer('transform')]
+ public ?string $bar;
+
+ public static function transform()
+ {
+ throw new \Exception('My error');
+ }
}
diff --git a/tests/Attribute/Element/RawTest.php b/tests/Attribute/Element/RawTest.php
index 33f3e9a..d9ca46b 100644
--- a/tests/Attribute/Element/RawTest.php
+++ b/tests/Attribute/Element/RawTest.php
@@ -6,7 +6,9 @@
use Bdf\Form\Attribute\Element\Raw;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Leaf\FloatElement;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class RawTest extends TestCase
@@ -23,7 +25,6 @@ protected function tearDown(): void
{
\Locale::setDefault($this->lastLocale);
}
-
#[DataProvider('provideAttributesProcessor')]
public function test(AttributesProcessorInterface $processor)
@@ -41,6 +42,17 @@ public function test(AttributesProcessorInterface $processor)
$this->assertSame(1.23, $form->bar->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestRawStruct::class, processor: $processor);
+
+ $form->submit(['foo' => '1,23', 'bar' => '1,23']);
+
+ $this->assertSame(1.0, $form->value()->foo);
+ $this->assertSame(1.23, $form->value()->bar);
+ }
+
public function test_code_generator()
{
$form = new class extends AttributeForm {
@@ -65,7 +77,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', FloatElement::class);
$foo->raw(true);
@@ -90,4 +102,52 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\FloatElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Tests\Form\Attribute\Element\TestRawStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestRawStruct::class);
+
+ $foo = $builder->add('foo', FloatElement::class);
+ $foo->raw(true);
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ $bar = $builder->add('bar', FloatElement::class);
+ $bar->raw(false);
+ $bar->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestRawStruct::class
+ );
+ }
+}
+
+class TestRawStruct
+{
+ #[Raw]
+ public ?float $foo;
+
+ #[Raw(false)]
+ public ?float $bar;
}
diff --git a/tests/Attribute/Element/RequiredTest.php b/tests/Attribute/Element/RequiredTest.php
index 002614f..8588e1f 100644
--- a/tests/Attribute/Element/RequiredTest.php
+++ b/tests/Attribute/Element/RequiredTest.php
@@ -6,7 +6,9 @@
use Bdf\Form\Attribute\Element\Required;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Leaf\FloatElement;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class RequiredTest extends TestCase
@@ -32,6 +34,21 @@ public function test(AttributesProcessorInterface $processor)
$this->assertTrue($form->valid());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestRequiredStruct::class, processor: $processor);
+
+ $form->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 {
@@ -56,7 +73,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', FloatElement::class);
$foo->required(null);
@@ -81,4 +98,52 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\FloatElement;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Tests\Form\Attribute\Element\TestRequiredStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestRequiredStruct::class);
+
+ $foo = $builder->add('foo', FloatElement::class);
+ $foo->required(null);
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ $bar = $builder->add('bar', FloatElement::class);
+ $bar->required('my message');
+ $bar->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestRequiredStruct::class
+ );
+ }
+}
+
+class TestRequiredStruct
+{
+ #[Required]
+ public ?float $foo;
+
+ #[Required('my message')]
+ public ?float $bar;
}
diff --git a/tests/Attribute/Element/TransformerErrorTest.php b/tests/Attribute/Element/TransformerErrorTest.php
index df91419..b271d60 100644
--- a/tests/Attribute/Element/TransformerErrorTest.php
+++ b/tests/Attribute/Element/TransformerErrorTest.php
@@ -7,9 +7,11 @@
use Bdf\Form\Attribute\Element\TransformerError;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\Struct\StructForm;
use Bdf\Form\Validator\TransformerExceptionConstraint;
use http\Message;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class TransformerErrorTest extends TestCase
@@ -35,7 +37,17 @@ public function transformer()
$this->assertEquals('BAR_ERROR', $form->error()->children()['bar']->code());
}
-
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestTransformerErrorStruct::class, processor: $processor);
+
+ $form->submit(['foo' => 'a', 'bar' => 'b']);
+
+ $this->assertEquals(['foo' => 'my message', 'bar' => 'bar'], $form->error()->toArray());
+ $this->assertEquals('BAR_ERROR', $form->error()->children()['bar']->code());
+ }
+
#[DataProvider('provideAttributesProcessor')]
public function test_with_callback(AttributesProcessorInterface $processor)
{
@@ -69,6 +81,19 @@ public function handleError($value, TransformerExceptionConstraint $constraint)
$this->assertEquals('FOO', $form->error()->children()['foo']->code());
}
+ #[DataProvider('provideStructAttributesProcessor')]
+ public function test_with_callback_struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestTransformerErrorCallbackStruct::class, processor: $processor);
+
+ $form->submit(['foo' => 'a']);
+ $this->assertTrue($form->valid());
+
+ $form->submit(['foo' => 'b']);
+ $this->assertEquals(['foo' => 'bbbbb'], $form->error()->toArray());
+ $this->assertEquals('FOO', $form->error()->children()['foo']->code());
+ }
+
public function test_code_generator()
{
$form = new class extends AttributeForm {
@@ -98,16 +123,16 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
- $foo->transformer([$form, 'transformer']);
+ $foo->transformer($context->transformer(...));
$foo
->transformerErrorMessage('my message')
;
$bar = $builder->add('bar', StringElement::class);
- $bar->transformer([$form, 'transformer']);
+ $bar->transformer($context->transformer(...));
$bar
->transformerErrorMessage('bar')
->transformerErrorCode('BAR_ERROR')
@@ -130,4 +155,86 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
, $form
);
}
+
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+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;
+use Tests\Form\Attribute\Element\TestTransformerErrorStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestTransformerErrorStruct::class);
+
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->transformer(TestTransformerErrorStruct::transformer(...));
+ $foo
+ ->transformerErrorMessage('my message')
+ ;
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ $bar = $builder->add('bar', StringElement::class);
+ $bar->transformer(TestTransformerErrorStruct::transformer(...));
+ $bar
+ ->transformerErrorMessage('bar')
+ ->transformerErrorCode('BAR_ERROR')
+ ;
+ $bar->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestTransformerErrorStruct::class
+);
+ }
+}
+class TestTransformerErrorStruct
+{
+ #[CallbackTransformer('transformer'), TransformerError('my message')]
+ public ?string $foo;
+
+ #[CallbackTransformer('transformer'), TransformerError(message: 'bar', code: 'BAR_ERROR')]
+ public ?string $bar;
+
+ public static function transformer()
+ {
+ throw new \Exception('My error');
+ }
+}
+
+class TestTransformerErrorCallbackStruct
+{
+ #[CallbackTransformer('transformer'), TransformerError(validationCallback: 'handleError')]
+ public ?string $foo;
+
+ public static function transformer()
+ {
+ throw new \Exception('My error');
+ }
+
+ public static function handleError($value, TransformerExceptionConstraint $constraint)
+ {
+ if ($value === 'a') {
+ return false;
+ }
+
+ $constraint->message = str_repeat($value, 5);
+ $constraint->code = 'FOO';
+
+ return true;
+ }
}
diff --git a/tests/Attribute/Element/TransformerTest.php b/tests/Attribute/Element/TransformerTest.php
index db220de..82beb76 100644
--- a/tests/Attribute/Element/TransformerTest.php
+++ b/tests/Attribute/Element/TransformerTest.php
@@ -8,8 +8,10 @@
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\ElementInterface;
use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\Struct\StructForm;
use Bdf\Form\Transformer\TransformerInterface;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class TransformerTest extends TestCase
@@ -29,6 +31,17 @@ public function test(AttributesProcessorInterface $processor)
$this->assertEquals('A_A', $view['foo']->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestTransformerStruct::class, processor: $processor);
+
+ $form->submit(['foo' => '_']);
+ $this->assertEquals('A_', $form->value()->foo);
+
+ $view = $form->view();
+ $this->assertEquals('A_A', $view['foo']->value());
+ }
#[DataProvider('provideAttributesProcessor')]
public function testWithArray(AttributesProcessorInterface $processor)
@@ -45,6 +58,18 @@ public function testWithArray(AttributesProcessorInterface $processor)
$this->assertEquals(['A_A', 'A-A'], $view['foo']->value());
}
+ #[DataProvider('provideStructAttributesProcessor')]
+ public function testWithArrayStruct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestTransformerArrayStruct::class, processor: $processor);
+
+ $form->submit(['foo' => ['_', '-']]);
+ $this->assertEquals(['A_', 'A-'], $form->value()->foo);
+
+ $view = $form->view();
+ $this->assertEquals(['A_A', 'A-A'], $view['foo']->value());
+ }
+
public function test_code_generator()
{
$form = new class extends AttributeForm {
@@ -68,7 +93,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
$foo->transformer(new ATransformer('A'));
@@ -90,6 +115,42 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
);
}
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+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;
+use Tests\Form\Attribute\Element\ATransformer;
+use Tests\Form\Attribute\Element\TestTransformerStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestTransformerStruct::class);
+
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->transformer(new ATransformer('A'));
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestTransformerStruct::class
+);
+ }
+
public function test_code_generator_with_array()
{
$form = new class extends AttributeForm {
@@ -113,7 +174,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', ArrayElement::class);
$foo->arrayTransformer(new AArrayTransformer('A'));
@@ -132,6 +193,42 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
PHP
, $form
+);
+ }
+
+ public function test_code_generator_with_array_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\PropertyAccess\Getter;
+use Bdf\Form\PropertyAccess\Setter;
+use Tests\Form\Attribute\Element\AArrayTransformer;
+use Tests\Form\Attribute\Element\TestTransformerArrayStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestTransformerArrayStruct::class);
+
+ $foo = $builder->add('foo', ArrayElement::class);
+ $foo->arrayTransformer(new AArrayTransformer('A'));
+ $foo->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+}
+
+PHP
+ , TestTransformerArrayStruct::class
);
}
}
@@ -171,3 +268,15 @@ public function transformFromHttp($value, ElementInterface $input)
return array_map(fn($v) => $this->c . $v, $value);
}
}
+
+class TestTransformerStruct
+{
+ #[Transformer(ATransformer::class, ['A'])]
+ public ?string $foo;
+}
+
+class TestTransformerArrayStruct
+{
+ #[Transformer(AArrayTransformer::class, ['A'], array: true)]
+ public array $foo;
+}
diff --git a/tests/Attribute/Form/CallbackGeneratorTest.php b/tests/Attribute/Form/CallbackGeneratorTest.php
index 9611a84..de8a38d 100644
--- a/tests/Attribute/Form/CallbackGeneratorTest.php
+++ b/tests/Attribute/Form/CallbackGeneratorTest.php
@@ -2,6 +2,7 @@
namespace Tests\Form\Attribute\Form;
+use AllowDynamicProperties;
use Bdf\Form\Aggregate\FormInterface;
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Form\CallbackGenerator;
@@ -11,7 +12,9 @@
use Bdf\Form\Attribute\Processor\ReflectionProcessor;
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\PropertyAccess\Setter;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Tests\Form\Attribute\TestCase;
class CallbackGeneratorTest extends TestCase
@@ -33,6 +36,18 @@ public function generate(FormInterface $form)
$this->assertEquals((object) ['foo' => 'b', 'bar' => 'a'], $form->value());
}
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestCallbackGeneratorStruct::class, processor: $processor);
+
+ $form->submit(['foo' => 'b']);
+ $expected = new TestCallbackGeneratorStruct();
+ $expected->foo = 'b';
+ $expected->bar = 'a';
+ $this->assertEquals($expected, $form->value());
+ }
+
/**
* @return void
*/
@@ -59,7 +74,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$builder->generates([$this, 'generate']);
@@ -77,4 +92,56 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
PHP
, $form);
}
+
+ /**
+ * @return void
+ */
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\PropertyAccess\Setter;
+use Tests\Form\Attribute\Form\TestCallbackGeneratorStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(TestCallbackGeneratorStruct::generate(...));
+
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->hydrator(new Setter());
+
+ return null;
+ }
+}
+
+PHP
+ , TestCallbackGeneratorStruct::class);
+ }
+}
+
+#[CallbackGenerator('generate'), AllowDynamicProperties]
+class TestCallbackGeneratorStruct
+{
+ #[Setter]
+ public ?string $foo;
+
+ public static function generate(FormInterface $form)
+ {
+ $o = new self();
+
+ $o->foo = null;
+ $o->bar = 'a';
+
+ return $o;
+ }
}
diff --git a/tests/Attribute/Form/CsrfTest.php b/tests/Attribute/Form/CsrfTest.php
index 083441f..fc3c3b1 100644
--- a/tests/Attribute/Form/CsrfTest.php
+++ b/tests/Attribute/Form/CsrfTest.php
@@ -6,7 +6,9 @@
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Form\Csrf;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Symfony\Component\Security\Csrf\CsrfTokenManager;
use Tests\Form\Attribute\TestCase;
@@ -26,7 +28,19 @@ public function test(AttributesProcessorInterface $processor)
$this->assertTrue($form->valid());
}
-
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestCsrfStruct::class, processor: $processor);
+
+ $form->submit([]);
+ $this->assertFalse($form->valid());
+ $this->assertEquals(['_token' => 'my error'], $form->error()->toArray());
+
+ $form->submit(['_token' => $form['_token']->view()->value()]);
+ $this->assertTrue($form->valid());
+ }
+
#[DataProvider('provideAttributesProcessor')]
public function test_message(AttributesProcessorInterface $processor)
{
@@ -106,7 +120,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$builder->csrf('_token')->tokenId('my_token')->message('my error')->invalidate(true);
@@ -124,4 +138,40 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
PHP
, $form);
}
+
+ /**
+ * @return void
+ */
+ public function test_code_generator_struct()
+ {
+ $this->assertGeneratedStruct(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Tests\Form\Attribute\Form\TestCsrfStruct;
+
+class GeneratedConfigurator implements AttributesProcessorInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->csrf('_token')->tokenId('my_token')->message('my error')->invalidate(true);
+ $builder->generates(TestCsrfStruct::class);
+
+ return null;
+ }
+}
+
+PHP
+ , TestCsrfStruct::class);
+ }
+}
+
+#[Csrf(tokenId: 'my_token', message: 'my error', invalidate: true)]
+class TestCsrfStruct
+{
}
diff --git a/tests/Attribute/Form/GeneratesTest.php b/tests/Attribute/Form/GeneratesTest.php
index a731fa9..c4546d6 100644
--- a/tests/Attribute/Form/GeneratesTest.php
+++ b/tests/Attribute/Form/GeneratesTest.php
@@ -3,7 +3,7 @@
namespace Tests\Form\Attribute\Form;
use Bdf\Form\Aggregate\FormBuilderInterface;
-use Bdf\Form\Aggregate\Value\MyEntity;
+use Bdf\Form\Aggregate\Value\Fixtures\MyEntity;
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Form\Generates;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
@@ -66,7 +66,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$builder->generates(Person::class);
diff --git a/tests/Attribute/FunctionalTest.php b/tests/Attribute/FunctionalTest.php
index 449963d..f6a027b 100644
--- a/tests/Attribute/FunctionalTest.php
+++ b/tests/Attribute/FunctionalTest.php
@@ -139,7 +139,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$bar = $builder->add('bar', IntegerElement::class);
$bar->satisfy(new NotBlank());
@@ -203,7 +203,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$bar = $builder->add('bar', IntegerElement::class);
$bar->satisfy(new NotBlank());
@@ -214,7 +214,7 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?
$foo = $builder->add('foo', StringElement::class);
$foo->extractor(new Getter());
$foo->hydrator(new Setter());
- $foo->transformer([$form, 'transform']);
+ $foo->transformer($context->transform(...));
return $this;
}
diff --git a/tests/Attribute/Php81/Aggregate/ArrayConstraintTest.php b/tests/Attribute/Php81/Aggregate/ArrayConstraintTest.php
index 6865919..9d5043b 100644
--- a/tests/Attribute/Php81/Aggregate/ArrayConstraintTest.php
+++ b/tests/Attribute/Php81/Aggregate/ArrayConstraintTest.php
@@ -57,7 +57,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$values = $builder->add('values', ArrayElement::class);
$values->arrayConstraint(new Unique(message: 'Not unique', groups: ['Default']));
diff --git a/tests/Attribute/Php81/Aggregate/CallbackArrayConstraintTest.php b/tests/Attribute/Php81/Aggregate/CallbackArrayConstraintTest.php
index 96dae0f..f1c938e 100644
--- a/tests/Attribute/Php81/Aggregate/CallbackArrayConstraintTest.php
+++ b/tests/Attribute/Php81/Aggregate/CallbackArrayConstraintTest.php
@@ -79,13 +79,13 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', ArrayElement::class);
- $foo->arrayConstraint(new ClosureConstraint($form->validateFoo(...), 'Foo size must be a multiple of 2'));
+ $foo->arrayConstraint(new ClosureConstraint($context->validateFoo(...), 'Foo size must be a multiple of 2'));
$bar = $builder->add('bar', ArrayElement::class);
- $bar->arrayConstraint(new ClosureConstraint($form->validateFoo(...)));
+ $bar->arrayConstraint(new ClosureConstraint($context->validateFoo(...)));
return $this;
}
diff --git a/tests/Attribute/Php81/Constraint/SatisfyTest.php b/tests/Attribute/Php81/Constraint/SatisfyTest.php
index 05585f0..7c2d25c 100644
--- a/tests/Attribute/Php81/Constraint/SatisfyTest.php
+++ b/tests/Attribute/Php81/Constraint/SatisfyTest.php
@@ -51,7 +51,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
$foo->satisfy(new Length(min: 3, groups: ['Default']));
diff --git a/tests/Attribute/Processor/CodeGenerator/AttributesProcessorGeneratorTest.php b/tests/Attribute/Processor/CodeGenerator/AttributesProcessorGeneratorTest.php
index 6be5591..074131e 100644
--- a/tests/Attribute/Processor/CodeGenerator/AttributesProcessorGeneratorTest.php
+++ b/tests/Attribute/Processor/CodeGenerator/AttributesProcessorGeneratorTest.php
@@ -30,7 +30,6 @@ public function test_empty()
namespace Generated;
use Bdf\Form\Aggregate\FormBuilderInterface;
-use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Attribute\Processor\PostConfigureInterface;
@@ -39,7 +38,7 @@ class Processor implements AttributesProcessorInterface
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
}
}
@@ -62,7 +61,6 @@ public function test_line()
namespace Generated;
use Bdf\Form\Aggregate\FormBuilderInterface;
-use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Attribute\Processor\PostConfigureInterface;
@@ -71,7 +69,7 @@ class Processor implements AttributesProcessorInterface
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo->bar(1, 2, 3);
}
@@ -98,7 +96,6 @@ public function test_new()
namespace Generated;
use Bdf\Form\Aggregate\FormBuilderInterface;
-use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Attribute\Processor\PostConfigureInterface;
use PHPUnit\Framework\Constraint\Count;
@@ -108,7 +105,7 @@ class Processor implements AttributesProcessorInterface
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$builder->satisfy(new Count(min: 3, max: 6));
}
diff --git a/tests/Attribute/Processor/CompileAttributesProcessorTest.php b/tests/Attribute/Processor/CompileAttributesProcessorTest.php
index 39feed4..deff4a9 100644
--- a/tests/Attribute/Processor/CompileAttributesProcessorTest.php
+++ b/tests/Attribute/Processor/CompileAttributesProcessorTest.php
@@ -31,7 +31,7 @@ public function test_compile_and_write_file_if_not_exists_and_load_class()
clearstatcache();
$processor = new CompileAttributesProcessor(
- fn (AttributeForm $form) => 'Generated\\' . get_class($form) . 'Configurator',
+ fn (string $className) => 'Generated\\' . $className . 'Configurator',
fn (string $className) => '/tmp' . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php'
);
@@ -67,17 +67,17 @@ class MyFormConfigurator implements AttributesProcessorInterface, PostConfigureI
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$builder->generates(Person::class);
$firstName = $builder->add('firstName', StringElement::class);
$firstName->satisfy(new NotBlank());
- $firstName->satisfy(new ClosureConstraint($form->validateName(...)));
+ $firstName->satisfy(new ClosureConstraint($context->validateName(...)));
$firstName->hydrator(new Setter(null))->extractor(new Getter(null));
$lastName = $builder->add('lastName', StringElement::class);
- $lastName->satisfy(new ClosureConstraint($form->validateName(...)));
+ $lastName->satisfy(new ClosureConstraint($context->validateName(...)));
$lastName->hydrator(new Setter(null))->extractor(new Getter(null));
$age = $builder->add('age', IntegerElement::class);
@@ -129,7 +129,7 @@ class Configurator implements AttributesProcessorInterface, PostConfigureInterfa
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
return $this;
}
@@ -146,7 +146,7 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void
);
$processor = new CompileAttributesProcessor(
- fn (AttributeForm $form) => 'Generated\\Configurator',
+ fn (string $className) => 'Generated\\Configurator',
fn (string $className) => $file
);
@@ -178,7 +178,7 @@ class InvalidConfigurator
);
$processor = new CompileAttributesProcessor(
- fn (AttributeForm $form) => 'Generated\\InvalidConfigurator',
+ fn (string $className) => 'Generated\\InvalidConfigurator',
fn (string $className) => $file
);
@@ -209,7 +209,7 @@ public function test_file_already_exists_but_without_class_on_file()
);
$processor = new CompileAttributesProcessor(
- fn (AttributeForm $form) => 'Generated\\NotAClass',
+ fn (string $className) => 'Generated\\NotAClass',
fn (string $className) => $file
);
@@ -232,7 +232,7 @@ public function test_generate()
file_put_contents($filename, 'invalid php file');
$processor = new CompileAttributesProcessor(
- fn (AttributeForm $form) => 'Generated\ManualConfigurator',
+ fn (string $className) => 'Generated\ManualConfigurator',
fn (string $className) => $filename
);
@@ -268,17 +268,17 @@ class ManualConfigurator implements AttributesProcessorInterface, PostConfigureI
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$builder->generates(Person::class);
$firstName = $builder->add('firstName', StringElement::class);
$firstName->satisfy(new NotBlank());
- $firstName->satisfy(new ClosureConstraint($form->validateName(...)));
+ $firstName->satisfy(new ClosureConstraint($context->validateName(...)));
$firstName->hydrator(new Setter(null))->extractor(new Getter(null));
$lastName = $builder->add('lastName', StringElement::class);
- $lastName->satisfy(new ClosureConstraint($form->validateName(...)));
+ $lastName->satisfy(new ClosureConstraint($context->validateName(...)));
$lastName->hydrator(new Setter(null))->extractor(new Getter(null));
$age = $builder->add('age', IntegerElement::class);
diff --git a/tests/Attribute/Processor/Element/ConstraintAttributeProcessorTest.php b/tests/Attribute/Processor/Element/ConstraintAttributeProcessorTest.php
index 18de5f9..d2836cf 100644
--- a/tests/Attribute/Processor/Element/ConstraintAttributeProcessorTest.php
+++ b/tests/Attribute/Processor/Element/ConstraintAttributeProcessorTest.php
@@ -58,7 +58,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
$foo->satisfy(new Length(min: 5));
diff --git a/tests/Attribute/Processor/Element/ExtractorAttributeProcessorTest.php b/tests/Attribute/Processor/Element/ExtractorAttributeProcessorTest.php
index 516078f..2d8e08c 100644
--- a/tests/Attribute/Processor/Element/ExtractorAttributeProcessorTest.php
+++ b/tests/Attribute/Processor/Element/ExtractorAttributeProcessorTest.php
@@ -46,7 +46,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
$foo->extractor(new Getter('bar'));
diff --git a/tests/Attribute/Processor/Element/FilterAttributeProcessorTest.php b/tests/Attribute/Processor/Element/FilterAttributeProcessorTest.php
index 88674a8..68652e5 100644
--- a/tests/Attribute/Processor/Element/FilterAttributeProcessorTest.php
+++ b/tests/Attribute/Processor/Element/FilterAttributeProcessorTest.php
@@ -50,7 +50,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
$foo->filter(new AFilter());
diff --git a/tests/Attribute/Processor/Element/HydratorAttributeProcessorTest.php b/tests/Attribute/Processor/Element/HydratorAttributeProcessorTest.php
index 3301a52..cf133aa 100644
--- a/tests/Attribute/Processor/Element/HydratorAttributeProcessorTest.php
+++ b/tests/Attribute/Processor/Element/HydratorAttributeProcessorTest.php
@@ -46,7 +46,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
$foo->hydrator(new Setter('bar'));
diff --git a/tests/Attribute/Processor/Element/TransformerAttributeProcessorTest.php b/tests/Attribute/Processor/Element/TransformerAttributeProcessorTest.php
index 9390485..4bc0045 100644
--- a/tests/Attribute/Processor/Element/TransformerAttributeProcessorTest.php
+++ b/tests/Attribute/Processor/Element/TransformerAttributeProcessorTest.php
@@ -50,7 +50,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
$foo->transformer(new ATransformer());
diff --git a/tests/Attribute/Processor/GenerateConfiguratorStrategyTest.php b/tests/Attribute/Processor/GenerateConfiguratorStrategyTest.php
index 7ad7fc7..c016200 100644
--- a/tests/Attribute/Processor/GenerateConfiguratorStrategyTest.php
+++ b/tests/Attribute/Processor/GenerateConfiguratorStrategyTest.php
@@ -3,7 +3,7 @@
namespace Tests\Form\Attribute\Processor;
use Bdf\Form\Aggregate\FormBuilder;
-use Bdf\Form\Aggregate\Value\MyEntity;
+use Bdf\Form\Aggregate\Value\Fixtures\MyEntity;
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Button\Groups;
use Bdf\Form\Attribute\Button\Value;
@@ -35,7 +35,7 @@ public function test_form_attributes()
use Bdf\Form\Aggregate\FormBuilderInterface;
use Bdf\Form\Aggregate\FormInterface;
-use Bdf\Form\Aggregate\Value\MyEntity;
+use Bdf\Form\Aggregate\Value\Fixtures\MyEntity;
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Attribute\Processor\PostConfigureInterface;
@@ -45,7 +45,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$builder->generates(MyEntity::class);
@@ -92,7 +92,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$builder->submit('foo')
;
@@ -152,7 +152,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$foo = $builder->add('foo', StringElement::class);
@@ -204,17 +204,17 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu
/**
* {@inheritdoc}
*/
- function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
{
$d = $builder->add('d', StringElement::class);
- $builder->submit('b')
- ;
-
$c = $builder->add('c', StringElement::class);
$a = $builder->add('a', StringElement::class);
+ $builder->submit('b')
+ ;
+
return $this;
}
diff --git a/tests/Attribute/Processor/ReflectionProcessorTest.php b/tests/Attribute/Processor/ReflectionProcessorTest.php
index 70f6b12..7bf1833 100644
--- a/tests/Attribute/Processor/ReflectionProcessorTest.php
+++ b/tests/Attribute/Processor/ReflectionProcessorTest.php
@@ -4,6 +4,7 @@
use Bdf\Form\Aggregate\FormBuilder;
use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\ElementPropertyMetadata;
use Bdf\Form\Attribute\Processor\ProcessorMetadata;
use Bdf\Form\Attribute\Processor\ReflectionProcessor;
use Bdf\Form\Attribute\Processor\ReflectionStrategyInterface;
@@ -21,12 +22,7 @@ public function test_should_iterate_hierarchy()
$form = new B();
$builder = new FormBuilder();
- $strategy->expects($matcher = $this->exactly(2))->method('onFormClass')->willReturnCallback(function (...$args) use ($matcher, $form, $builder) {
- match ($matcher->numberOfInvocations()) {
- 1 => $this->assertEquals([new \ReflectionClass(B::class), $form, $builder, $args[3]], $args),
- 2 => $this->assertEquals([new \ReflectionClass(A::class), $form, $builder, $args[3]], $args),
- };
- });
+ $strategy->expects($this->once())->method('onFormClass');
$processor->configureBuilder($form, $builder);
}
@@ -40,7 +36,7 @@ public function test_should_not_configure_twice_same_element_property()
$builder = new FormBuilder();
$strategy->expects($this->once())->method('onElementProperty')
- ->with(new \ReflectionProperty(B::class, 'foo'), 'foo', StringElement::class, $form, $builder)
+ ->with(new ElementPropertyMetadata('foo', new \ReflectionProperty(B::class, 'foo'), StringElement::class, []), $form, $builder)
;
$processor->configureBuilder($form, $builder);
diff --git a/tests/Attribute/TestCase.php b/tests/Attribute/TestCase.php
index c18deeb..9499816 100644
--- a/tests/Attribute/TestCase.php
+++ b/tests/Attribute/TestCase.php
@@ -9,6 +9,7 @@
use Bdf\Form\Attribute\Processor\ConfigureFormBuilderStrategy;
use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy;
use Bdf\Form\Attribute\Processor\ReflectionProcessor;
+use Bdf\Form\Struct\StructAttributesProcessorFactory;
class TestCase extends \PHPUnit\Framework\TestCase
{
@@ -26,6 +27,22 @@ public static function provideAttributesProcessor(): array
];
}
+ /**
+ * @return AttributesProcessorInterface[]
+ */
+ public static function provideStructAttributesProcessor(): array
+ {
+ $factory = new StructAttributesProcessorFactory();
+
+ return [
+ 'reflection' => [$factory->runtime()],
+ 'compile' => [$factory->generated(
+ fn ($form) => 'Generated\\G' . bin2hex(random_bytes(16)),
+ fn ($className) => sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'Generated_' . str_replace('\\', '_', $className) . '.php'
+ )],
+ ];
+ }
+
public function assertGenerated(string $expected, AttributeForm $form): void
{
$generator = new GenerateConfiguratorStrategy('Generated\GeneratedConfigurator');
@@ -34,4 +51,13 @@ public function assertGenerated(string $expected, AttributeForm $form): void
$processor->configureBuilder($form, new FormBuilder());
$this->assertEquals($expected, $generator->code());
}
+
+ public function assertGeneratedStruct(string $expected, string $structClass): void
+ {
+ $generator = new GenerateConfiguratorStrategy('Generated\GeneratedConfigurator');
+ $processor = new StructAttributesProcessorFactory()->create($generator);
+
+ $processor->configureBuilder($structClass, new FormBuilder());
+ $this->assertEquals($expected, $generator->code());
+ }
}
diff --git a/tests/Registry/RegistryTest.php b/tests/Registry/RegistryTest.php
index 97e8656..57f6881 100644
--- a/tests/Registry/RegistryTest.php
+++ b/tests/Registry/RegistryTest.php
@@ -45,6 +45,8 @@
use Bdf\Form\Phone\PhoneChildBuilder;
use Bdf\Form\Phone\PhoneElement;
use Bdf\Form\Phone\PhoneElementBuilder;
+use Bdf\Form\Struct\StructForm;
+use Bdf\Form\Struct\StructFormBuilder;
use Bdf\Form\Transformer\ClosureTransformer;
use Bdf\Form\Transformer\DataTransformerAdapter;
use Bdf\Form\Transformer\TransformerAggregate;
@@ -110,6 +112,7 @@ public function test_elementBuilder()
$this->assertInstanceOf(FormBuilder::class, $this->registry->elementBuilder(Form::class));
$this->assertInstanceOf(ArrayElementBuilder::class, $this->registry->elementBuilder(ArrayElement::class));
$this->assertInstanceOf(CustomFormBuilder::class, $this->registry->elementBuilder(MyCustomForm::class));
+ $this->assertInstanceOf(StructFormBuilder::class, $this->registry->elementBuilder(StructForm::class));
$this->assertInstanceOf(MyCustomForm::class, $this->registry->elementBuilder(MyCustomForm::class)->buildElement());
$builder = $this->createMock(ElementBuilderInterface::class);
diff --git a/tests/Struct/Fixtures/ConstraintDto.php b/tests/Struct/Fixtures/ConstraintDto.php
new file mode 100644
index 0000000..1cb324f
--- /dev/null
+++ b/tests/Struct/Fixtures/ConstraintDto.php
@@ -0,0 +1,19 @@
+builder = new StructFormBuilder();
+ }
+
+ #[Test]
+ public function simple()
+ {
+ $form = $this->builder->class(SimpleDto::class)->buildElement();
+
+ $this->assertInstanceOf(StructForm::class, $form);
+ $this->assertEquals(new SimpleDto('foo', 42), $form->submit(['name' => 'foo', 'value' => 42])->value());
+ }
+
+ #[Test]
+ public function missingClass()
+ {
+ $this->expectException(\LogicException::class);
+ $this->builder->buildElement();
+ }
+
+ #[Test]
+ public function cannotResetClass()
+ {
+ $this->expectException(\LogicException::class);
+ $this->builder->class(SimpleDto::class)->class(OptionalDto::class);
+ }
+
+ /**
+ *
+ */
+ public function test_satisfy()
+ {
+ $this->builder->class(OptionalDto::class);
+ $form = $this->builder->satisfy(function ($value, $form) {
+ if ($form['name']->element()->value() == 'bar') {
+ return 'error';
+ }
+ })->buildElement();
+
+ $this->assertEquals('error', $form->submit(['name' => 'bar'])->error()->global());
+ $this->assertTrue($form->submit(['name' => 'baz'])->valid());
+ }
+
+ /**
+ *
+ */
+ public function test_transformer()
+ {
+ $this->builder->class(OptionalDto::class);
+
+ $form = $this->builder->transformer(function (array $value) {
+ return array_map(strtoupper(...), $value);
+ })->buildElement();
+
+ $this->assertEquals(new OptionalDto('FOO'), $form->submit(['name' => 'foo'])->value());
+ }
+}
diff --git a/tests/Struct/StructFormTest.php b/tests/Struct/StructFormTest.php
new file mode 100644
index 0000000..4b23a17
--- /dev/null
+++ b/tests/Struct/StructFormTest.php
@@ -0,0 +1,341 @@
+submit([]);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'name' => 'This value should not be blank.',
+ 'value' => 'This value should not be blank.',
+ ], $form->error()->toArray());
+
+ $form->submit([
+ 'name' => 'bar',
+ 'value' => 42
+ ]);
+
+ $this->assertTrue($form->valid());
+ $this->assertEquals(new SimpleDto('bar', 42), $form->value());
+ }
+
+ #[Test, DataProvider('provideAttributesProcessor')]
+ public function simpleDtoWithOptionalFields(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(OptionalDto::class, processor: $processor);
+
+ $form->submit([]);
+
+ $this->assertTrue($form->valid());
+ $this->assertEquals(new OptionalDto(null), $form->value());
+
+ $form->submit([
+ 'name' => 'bar',
+ 'value' => 42
+ ]);
+
+ $this->assertTrue($form->valid());
+ $this->assertEquals(new OptionalDto('bar', 42), $form->value());
+ }
+
+ #[Test, DataProvider('provideAttributesProcessor')]
+ public function dtoWithConstraints(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(ConstraintDto::class, processor: $processor);
+
+ $form->submit([]);
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'name' => 'This value should not be blank.',
+ 'value' => 'This value should not be blank.',
+ ], $form->error()->toArray());
+
+ $form->submit([
+ 'name' => 'b',
+ 'value' => -5,
+ ]);
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'name' => 'This value is not valid.',
+ 'value' => 'This value should be positive.',
+ ], $form->error()->toArray());
+
+ $form->submit([
+ 'name' => 'bar',
+ 'value' => 50000,
+ ]);
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'value' => 'This value should be less than 250.',
+ ], $form->error()->toArray());
+
+ $form->submit([
+ 'name' => ' bar ',
+ 'value' => 42
+ ]);
+
+ $this->assertTrue($form->valid());
+ $this->assertEquals(new ConstraintDto('bar', 42), $form->value());
+ }
+
+ #[Test, DataProvider('provideAttributesProcessor')]
+ public function withEmbedded(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(StructWithEmbedded::class, processor: $processor);
+
+ $form->submit([]);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'id' => 'This value should not be blank.',
+ 'embedded' => [
+ 'name' => 'This value should not be blank.',
+ 'value' => 'This value should not be blank.',
+ ],
+ ], $form->error()->toArray());
+
+ $form->submit([
+ 'id' => 158,
+ 'embedded' => [
+ 'name' => 'bob',
+ 'value' => 41,
+ ],
+ ]);
+
+ $this->assertTrue($form->valid());
+ $this->assertEquals(new StructWithEmbedded(158, new SimpleDto('bob', 41)), $form->value());
+ }
+
+ #[Test, DataProvider('provideAttributesProcessor')]
+ public function withOptionalEmbedded(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(StructWithOptionalEmbedded::class, processor: $processor);
+
+ $form->submit([]);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'id' => 'This value should not be blank.',
+ ], $form->error()->toArray());
+
+ $form->submit(['id' => 745]);
+ $this->assertTrue($form->valid());
+ $this->assertEquals(new StructWithOptionalEmbedded(745, null), $form->value());
+
+ $form->submit(['id' => 745, 'embedded' => ['value' => '']]);
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'embedded' => [
+ 'name' => 'This value should not be blank.',
+ 'value' => 'This value should not be blank.',
+ ]
+ ], $form->error()->toArray());
+
+ $form->submit([
+ 'id' => 158,
+ 'embedded' => [
+ 'name' => 'bob',
+ 'value' => 41,
+ ],
+ ]);
+
+ $this->assertTrue($form->valid());
+ $this->assertEquals(new StructWithOptionalEmbedded(158, new SimpleDto('bob', 41)), $form->value());
+ }
+
+ #[Test, DataProvider('provideAttributesProcessor')]
+ public function withArrayOfStruct(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(Shape::class, processor: $processor);
+
+ $form->submit([]);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'color' => [
+ 'red' => 'This value should not be blank.',
+ 'green' => 'This value should not be blank.',
+ 'blue' => 'This value should not be blank.',
+ ],
+ 'points' => 'This collection should contain 3 elements or more.',
+ ], $form->error()->toArray());
+
+ $form->submit([
+ 'color' => [
+ 'red' => 42,
+ 'green' => 33,
+ 'blue' => 0,
+ ],
+ 'points' => [
+ ['x' => 3, 'y' => 4],
+ ['x' => 5, 'y' => 6],
+ ['x' => 7, 'y' => 8],
+ ]
+ ]);
+
+ $this->assertTrue($form->valid());
+ $this->assertEquals(new Shape(
+ new Color(42, 33, 0),
+ [
+ new Point(3, 4),
+ new Point(5, 6),
+ new Point(7, 8),
+ ]
+ ), $form->value());
+ }
+
+ #[Test]
+ public function codeGenerator()
+ {
+ $this->assertGenerated(
+ <<<'PHP'
+ namespace Generated;
+
+ use Bdf\Form\Aggregate\ArrayElement;
+ use Bdf\Form\Aggregate\FormBuilderInterface;
+ use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+ use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+ use Bdf\Form\PropertyAccess\Getter;
+ use Bdf\Form\PropertyAccess\Setter;
+ use Bdf\Form\Struct\Fixtures\Shape;
+ use Bdf\Form\Struct\StructForm;
+ use Symfony\Component\Validator\Constraints\Count;
+
+ class GeneratedConfigurator implements AttributesProcessorInterface
+ {
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $builder->generates(Shape::class);
+
+ $color = $builder->add('color', StructForm::class);
+ $color->hydrator(new Setter(null))->extractor(new Getter(null));
+ $color->required(null);
+ $color->class('Bdf\Form\Struct\Fixtures\Color');
+
+ $points = $builder->add('points', ArrayElement::class);
+ $points->arrayConstraint(new Count(min: 3));
+ $points->struct('Bdf\Form\Struct\Fixtures\Point');
+ $points->hydrator(new Setter(null))->extractor(new Getter(null));
+
+ return null;
+ }
+ }
+
+ PHP,
+ Shape::class,
+ );
+ }
+
+ #[Test, DataProvider('provideAttributesProcessor')]
+ public function withDate(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(DtoWithDate::class, processor: $processor);
+
+ $form->submit([]);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'start' => 'This value should not be blank.',
+ 'end' => 'This value should not be blank.',
+ ], $form->error()->toArray());
+
+ $form->submit([
+ 'start' => '2019-01-01',
+ 'end' => '2019-01-31',
+ ]);
+
+ $this->assertTrue($form->valid());
+ $this->assertEquals(new DtoWithDate(
+ new DateTimeImmutable('2019-01-01'),
+ new CustomDate('2019-01-31'),
+ ), $form->value());
+ }
+
+ #[Test, DataProvider('provideAttributesProcessor')]
+ public function withEnum(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(StructWithEnum::class, processor: $processor);
+
+ $form->submit([
+ 'i' => 42,
+ 's' => 'invalid',
+ ]);
+
+ $this->assertFalse($form->valid());
+ $this->assertEquals([
+ 'i' => 'This value should not be blank.',
+ 's' => 'This value should not be blank.',
+ ], $form->error()->toArray());
+
+ $form->submit([
+ 'i' => 2,
+ 's' => 'One',
+ ]);
+
+ $this->assertEquals(new StructWithEnum(
+ i: IntEnum::Bar,
+ s: SimpleEnum::One,
+ ), $form->value());
+ }
+
+ public function assertGenerated(string $expected, string $structClass): void
+ {
+ $generator = new GenerateConfiguratorStrategy('Generated\GeneratedConfigurator');
+ $processor = new StructAttributesProcessorFactory()->create($generator);
+
+ $processor->configureBuilder($structClass, new FormBuilder());
+ $this->assertEquals($expected, $generator->code());
+ }
+
+ /**
+ * @return AttributesProcessorInterface[]
+ */
+ public static function provideAttributesProcessor(): array
+ {
+ $factory = new StructAttributesProcessorFactory();
+
+ return [
+ 'reflection' => [$factory->runtime()],
+ 'compile' => [$factory->generated(
+ fn ($form) => 'Generated\\G' . bin2hex(random_bytes(16)),
+ fn ($className) => sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'Generated_' . str_replace('\\', '_', $className) . '.php'
+ )],
+ ];
+ }
+}