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' + )], + ]; + } +}